Skip to content

Commit 77b53a7

Browse files
feat: Initial migration for Workspaces and pipeline step
Related: #454 We noticed most of the incoming-requests which contain a code-snippet only list a relative path with respect to where the code editor is opened. This would make difficult to accurately distinguish between repositories in Codegate. For example, a user could open 2 different code Python repositorites in different session and both repositories contain a `pyproject.toml`. It would be impossible for Codegate to determine the real repository of the file only using the relative path. Hence, the initial implementation of Workspaces will rely on a pipeline step that is able to take commands a process them. Some commands could be: - List workspaces - Add workspace - Switch active workspace - Delete workspace It would be up to the user to select the desired active workspace. This PR introduces an initial migration for Workspaces and the pipeline step with the `list` command.
1 parent b4d719f commit 77b53a7

File tree

7 files changed

+170
-0
lines changed

7 files changed

+170
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
"""introduce workspaces
2+
3+
Revision ID: 5c2f3eee5f90
4+
Revises: 30d0144e1a50
5+
Create Date: 2025-01-15 19:27:08.230296
6+
7+
"""
8+
from typing import Sequence, Union
9+
10+
from alembic import op
11+
import sqlalchemy as sa
12+
13+
14+
# revision identifiers, used by Alembic.
15+
revision: str = '5c2f3eee5f90'
16+
down_revision: Union[str, None] = '30d0144e1a50'
17+
branch_labels: Union[str, Sequence[str], None] = None
18+
depends_on: Union[str, Sequence[str], None] = None
19+
20+
21+
def upgrade() -> None:
22+
# Workspaces table
23+
op.execute(
24+
"""
25+
CREATE TABLE workspaces (
26+
id TEXT PRIMARY KEY, -- UUID stored as TEXT
27+
name TEXT NOT NULL,
28+
is_active BOOLEAN NOT NULL DEFAULT 0
29+
);
30+
"""
31+
)
32+
op.execute("INSERT INTO workspaces (id, name, is_active) VALUES ('1', 'default', 1);")
33+
# Alter table prompts
34+
op.execute("ALTER TABLE prompts ADD COLUMN workspace_id TEXT REFERENCES workspaces(id);")
35+
op.execute("UPDATE prompts SET workspace_id = '1';")
36+
# Create index for workspace_id
37+
op.execute("CREATE INDEX idx_prompts_workspace_id ON prompts (workspace_id);")
38+
39+
40+
def downgrade() -> None:
41+
pass

src/codegate/db/connection.py

+13
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
GetPromptWithOutputsRow,
1919
Output,
2020
Prompt,
21+
Workspace,
2122
)
2223
from codegate.pipeline.base import PipelineContext
2324

@@ -286,6 +287,18 @@ async def get_alerts_with_prompt_and_output(self) -> List[GetAlertsWithPromptAnd
286287
prompts = await self._execute_select_pydantic_model(GetAlertsWithPromptAndOutputRow, sql)
287288
return prompts
288289

290+
async def get_workspaces(self) -> List[Workspace]:
291+
sql = text(
292+
"""
293+
SELECT
294+
id, name, is_active
295+
FROM workspaces
296+
ORDER BY is_active DESC
297+
"""
298+
)
299+
workspaces = await self._execute_select_pydantic_model(Workspace, sql)
300+
return workspaces
301+
289302

290303
def init_db_sync(db_path: Optional[str] = None):
291304
"""DB will be initialized in the constructor in case it doesn't exist."""

src/codegate/db/models.py

+6
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ class Prompt(pydantic.BaseModel):
2626
provider: Optional[Any]
2727
request: Any
2828
type: Any
29+
workspace_id: Optional[Any]
2930

3031

3132
class Setting(pydantic.BaseModel):
@@ -37,6 +38,11 @@ class Setting(pydantic.BaseModel):
3738
other_settings: Optional[Any]
3839

3940

41+
class Workspace(pydantic.BaseModel):
42+
id: Any
43+
name: str
44+
is_active: bool = False
45+
4046
# Models for select queries
4147

4248

src/codegate/pipeline/base.py

+1
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ def add_input_request(
135135
provider=provider,
136136
type="fim" if is_fim_request else "chat",
137137
request=request_str,
138+
workspace_id="1", # TODO: This is a placeholder for now, using default workspace
138139
)
139140
# Uncomment the below to debug the input
140141
# logger.debug(f"Added input request to context: {self.input_request}")

src/codegate/pipeline/factory.py

+2
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
)
1515
from codegate.pipeline.system_prompt.codegate import SystemPrompt
1616
from codegate.pipeline.version.version import CodegateVersion
17+
from codegate.pipeline.workspace.workspace import CodegateWorkspace
1718

1819

1920
class PipelineFactory:
@@ -28,6 +29,7 @@ def create_input_pipeline(self) -> SequentialPipelineProcessor:
2829
# later steps
2930
CodegateSecrets(),
3031
CodegateVersion(),
32+
CodegateWorkspace(),
3133
CodeSnippetExtractor(),
3234
CodegateContextRetriever(),
3335
SystemPrompt(Config.get_config().prompts.default_chat),

src/codegate/pipeline/workspace/__init__.py

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import asyncio
2+
3+
from litellm import ChatCompletionRequest
4+
5+
from codegate.db.connection import DbReader
6+
from codegate.pipeline.base import (
7+
PipelineContext,
8+
PipelineResponse,
9+
PipelineResult,
10+
PipelineStep,
11+
)
12+
13+
14+
class WorkspaceCommands:
15+
16+
def __init__(self):
17+
self._db_recorder = DbReader()
18+
self.commands = {
19+
"list": self._list_workspaces,
20+
}
21+
22+
async def _list_workspaces(self, *args):
23+
"""
24+
List all workspaces
25+
"""
26+
workspaces = await self._db_recorder.get_workspaces()
27+
print(workspaces)
28+
respond_str = ""
29+
for workspace in workspaces:
30+
respond_str += f"{workspace.id} - {workspace.name}"
31+
if workspace.is_active:
32+
respond_str += " (active)"
33+
respond_str += "\n"
34+
return respond_str
35+
36+
async def execute(self, command: str, *args) -> str:
37+
"""
38+
Execute the given command
39+
40+
Args:
41+
command (str): The command to execute
42+
"""
43+
command_to_execute = self.commands.get(command)
44+
if command_to_execute is not None:
45+
return await command_to_execute(*args)
46+
else:
47+
return "Command not found"
48+
49+
async def parse_execute_cmd(self, last_user_message: str) -> str:
50+
"""
51+
Parse the last user message and execute the command
52+
53+
Args:
54+
last_user_message (str): The last user message
55+
"""
56+
command_and_args = last_user_message.split("codegate-workspace ")[1]
57+
command, *args = command_and_args.split(" ")
58+
return await self.execute(command, *args)
59+
60+
61+
class CodegateWorkspace(PipelineStep):
62+
"""Pipeline step that handles workspace information requests."""
63+
64+
@property
65+
def name(self) -> str:
66+
"""
67+
Returns the name of this pipeline step.
68+
69+
Returns:
70+
str: The identifier 'codegate-workspace'
71+
"""
72+
return "codegate-workspace"
73+
74+
async def process(
75+
self, request: ChatCompletionRequest, context: PipelineContext
76+
) -> PipelineResult:
77+
"""
78+
Checks if the last user message contains "codegate-workspace" and
79+
responds with command specified.
80+
This short-circuits the pipeline if the message is found.
81+
82+
Args:
83+
request (ChatCompletionRequest): The chat completion request to process
84+
context (PipelineContext): The current pipeline context
85+
86+
Returns:
87+
PipelineResult: Contains workspace response if triggered, otherwise continues
88+
pipeline
89+
"""
90+
last_user_message = self.get_last_user_message(request)
91+
92+
if last_user_message is not None:
93+
last_user_message_str, _ = last_user_message
94+
if "codegate-workspace" in last_user_message_str.lower():
95+
context.shortcut_response = True
96+
command_output = await WorkspaceCommands().parse_execute_cmd(last_user_message_str)
97+
return PipelineResult(
98+
response=PipelineResponse(
99+
step_name=self.name,
100+
content=command_output,
101+
model=request["model"],
102+
),
103+
context=context,
104+
)
105+
106+
# Fall through
107+
return PipelineResult(request=request, context=context)

0 commit comments

Comments
 (0)