Skip to content
This repository was archived by the owner on Jun 5, 2025. It is now read-only.

Commit a574bc1

Browse files
Adds cache to cli-chat commands
Closes: #732 Copilot sends at least 2 requests with the same `last_user_message`. Hence we execute the same command 2 times and reply to the last one. The last behaviour will cause that if we create a Workspace with Copilot the cli will respond that the workspace already exists. This PR implements a workaround to cache the commands and it's outputs. That way we can reply the same to subsequent requests sent by Copilot
1 parent 184ad5e commit a574bc1

File tree

1 file changed

+59
-3
lines changed

1 file changed

+59
-3
lines changed

src/codegate/pipeline/cli/commands.py

Lines changed: 59 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1+
import datetime
12
from abc import ABC, abstractmethod
2-
from typing import Awaitable, Callable, Dict, List, Tuple
3+
from typing import Awaitable, Callable, Dict, List, Optional, Tuple
34

4-
from pydantic import ValidationError
5+
from pydantic import BaseModel, ValidationError
56

67
from codegate import __version__
78
from codegate.db.connection import AlreadyExistsError
@@ -16,6 +17,15 @@ class NoSubcommandError(Exception):
1617
pass
1718

1819

20+
class ExecCommand(BaseModel):
21+
22+
cmd_out: str
23+
exec_time: datetime.datetime
24+
25+
26+
command_cache: Dict[str, ExecCommand] = {}
27+
28+
1929
class CodegateCommand(ABC):
2030
@abstractmethod
2131
async def run(self, args: List[str]) -> str:
@@ -31,10 +41,56 @@ def command_name(self) -> str:
3141
def help(self) -> str:
3242
pass
3343

44+
async def _get_full_command(self, args: List[str]) -> str:
45+
"""
46+
Get the full command string with the command name and args.
47+
"""
48+
joined_args = " ".join(args)
49+
return f"{self.command_name} {joined_args}"
50+
51+
async def _record_in_cache(self, args: List[str], cmd_out: str) -> None:
52+
"""
53+
Record the command in the cache.
54+
"""
55+
full_command = await self._get_full_command(args)
56+
command_cache[full_command] = ExecCommand(
57+
cmd_out=cmd_out, exec_time=datetime.datetime.now(datetime.timezone.utc)
58+
)
59+
60+
async def _cache_lookup(self, args: List[str]) -> Optional[str]:
61+
"""
62+
Look up the command in the cache. If the command was executed less than 1 second ago,
63+
return the cached output.
64+
"""
65+
full_command = await self._get_full_command(args)
66+
if full_command in command_cache:
67+
exec_command = command_cache[full_command]
68+
time_since_last_exec = (
69+
datetime.datetime.now(datetime.timezone.utc) - exec_command.exec_time
70+
)
71+
# 1 second cache. 1 second is to be short enough to not affect UX but long enough to
72+
# reply the same to concurrent requests. Needed for Copilot.
73+
if time_since_last_exec.total_seconds() < 1:
74+
return exec_command.cmd_out
75+
return None
76+
3477
async def exec(self, args: List[str]) -> str:
78+
"""
79+
Execute the command and cache the output. The cache is invalidated after 1 second.
80+
81+
1. Check if the command is help. If it is, return the help text.
82+
2. Check if the command is in the cache. If it is, return the cached output.
83+
3. Run the command and cache the output.
84+
4. Return the output.
85+
"""
3586
if args and args[0] == "-h":
3687
return self.help
37-
return await self.run(args)
88+
cached_out = await self._cache_lookup(args)
89+
if cached_out:
90+
return cached_out
91+
cmd_out = await self.run(args)
92+
await self._record_in_cache(args, cmd_out)
93+
return cmd_out
3894

3995

4096
class Version(CodegateCommand):

0 commit comments

Comments
 (0)