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

Commit ae612ab

Browse files
author
Luke Hinds
committed
Merge remote-tracking branch 'origin/sqlite-vec' into sqlite-vec
2 parents 4ba9c4b + bb63b3c commit ae612ab

File tree

19 files changed

+555
-113
lines changed

19 files changed

+555
-113
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ jobs:
1414
python-version: ["3.12"]
1515

1616
steps:
17-
- name: Checkout github repo
17+
- name: Checkout
1818
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
1919
with:
2020
lfs: true
@@ -47,9 +47,6 @@ jobs:
4747
- name: Run linting
4848
run: make lint
4949

50-
- name: Run formatting
51-
run: make format
52-
5350
- name: Run tests
5451
run: make test
5552

.github/workflows/integration-tests.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ jobs:
2929
with:
3030
lfs: true
3131

32+
- name: Checkout LFS objects
33+
run: git lfs pull
34+
3235
- name: Ensure file permissions for mounted volume
3336
run: |
3437
mkdir -p ./codegate_volume/certs ./codegate_volume/models ./codegate_volume/db

Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ format:
1919
poetry run ruff check --fix .
2020

2121
lint:
22+
poetry run black --check .
2223
poetry run ruff check .
2324

2425
test:

api/openapi.json

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,9 @@
172172
"description": "Successful Response",
173173
"content": {
174174
"application/json": {
175-
"schema": {}
175+
"schema": {
176+
"$ref": "#/components/schemas/Workspace"
177+
}
176178
}
177179
}
178180
},
@@ -283,8 +285,13 @@
283285
}
284286
],
285287
"responses": {
286-
"204": {
287-
"description": "Successful Response"
288+
"200": {
289+
"description": "Successful Response",
290+
"content": {
291+
"application/json": {
292+
"schema": {}
293+
}
294+
}
288295
},
289296
"422": {
290297
"description": "Validation Error",
Binary file not shown.
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
"""add soft delete
2+
3+
Revision ID: 8e4b4b8d1a88
4+
Revises: 5c2f3eee5f90
5+
Create Date: 2025-01-20 14:08:40.851647
6+
7+
"""
8+
9+
from typing import Sequence, Union
10+
11+
from alembic import op
12+
13+
# revision identifiers, used by Alembic.
14+
revision: str = "8e4b4b8d1a88"
15+
down_revision: Union[str, None] = "5c2f3eee5f90"
16+
branch_labels: Union[str, Sequence[str], None] = None
17+
depends_on: Union[str, Sequence[str], None] = None
18+
19+
20+
def upgrade() -> None:
21+
op.execute(
22+
"""
23+
ALTER TABLE workspaces
24+
ADD COLUMN deleted_at DATETIME DEFAULT NULL;
25+
"""
26+
)
27+
28+
29+
def downgrade() -> None:
30+
op.execute("ALTER TABLE workspaces DROP COLUMN deleted_at;")
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
"""add_workspace_system_prompt
2+
3+
Revision ID: a692c8b52308
4+
Revises: 5c2f3eee5f90
5+
Create Date: 2025-01-17 16:33:58.464223
6+
7+
"""
8+
9+
from typing import Sequence, Union
10+
11+
from alembic import op
12+
13+
# revision identifiers, used by Alembic.
14+
revision: str = "a692c8b52308"
15+
down_revision: Union[str, None] = "5c2f3eee5f90"
16+
branch_labels: Union[str, Sequence[str], None] = None
17+
depends_on: Union[str, Sequence[str], None] = None
18+
19+
20+
def upgrade() -> None:
21+
# Add column to workspaces table
22+
op.execute("ALTER TABLE workspaces ADD COLUMN system_prompt TEXT DEFAULT NULL;")
23+
24+
25+
def downgrade() -> None:
26+
op.execute("ALTER TABLE workspaces DROP COLUMN system_prompt;")
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
"""merging system prompt and soft-deletes
2+
3+
Revision ID: e6227073183d
4+
Revises: 8e4b4b8d1a88, a692c8b52308
5+
Create Date: 2025-01-20 16:08:40.645298
6+
7+
"""
8+
9+
from typing import Sequence, Union
10+
11+
# revision identifiers, used by Alembic.
12+
revision: str = "e6227073183d"
13+
down_revision: Union[str, None] = ("8e4b4b8d1a88", "a692c8b52308")
14+
branch_labels: Union[str, Sequence[str], None] = None
15+
depends_on: Union[str, Sequence[str], None] = None
16+
17+
18+
def upgrade() -> None:
19+
pass
20+
21+
22+
def downgrade() -> None:
23+
pass

src/codegate/api/v1.py

Lines changed: 28 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,15 @@
1-
from fastapi import APIRouter, Response
2-
from fastapi.exceptions import HTTPException
1+
from fastapi import APIRouter, HTTPException, Response
32
from fastapi.routing import APIRoute
43
from pydantic import ValidationError
54

65
from codegate.api import v1_models
7-
from codegate.db.connection import AlreadyExistsError
8-
from codegate.workspaces.crud import WorkspaceCrud
96
from codegate.api.dashboard.dashboard import dashboard_router
7+
from codegate.db.connection import AlreadyExistsError
8+
from codegate.workspaces import crud
109

1110
v1 = APIRouter()
1211
v1.include_router(dashboard_router)
13-
14-
wscrud = WorkspaceCrud()
12+
wscrud = crud.WorkspaceCrud()
1513

1614

1715
def uniq_name(route: APIRoute):
@@ -44,40 +42,51 @@ async def list_active_workspaces() -> v1_models.ListActiveWorkspacesResponse:
4442
@v1.post("/workspaces/active", tags=["Workspaces"], generate_unique_id_function=uniq_name)
4543
async def activate_workspace(request: v1_models.ActivateWorkspaceRequest, status_code=204):
4644
"""Activate a workspace by name."""
47-
activated = await wscrud.activate_workspace(request.name)
48-
49-
# TODO: Refactor
50-
if not activated:
45+
try:
46+
await wscrud.activate_workspace(request.name)
47+
except crud.WorkspaceAlreadyActiveError:
5148
return HTTPException(status_code=409, detail="Workspace already active")
49+
except crud.WorkspaceDoesNotExistError:
50+
return HTTPException(status_code=404, detail="Workspace does not exist")
51+
except Exception:
52+
return HTTPException(status_code=500, detail="Internal server error")
5253

5354
return Response(status_code=204)
5455

5556

5657
@v1.post("/workspaces", tags=["Workspaces"], generate_unique_id_function=uniq_name, status_code=201)
57-
async def create_workspace(request: v1_models.CreateWorkspaceRequest):
58+
async def create_workspace(request: v1_models.CreateWorkspaceRequest) -> v1_models.Workspace:
5859
"""Create a new workspace."""
5960
# Input validation is done in the model
6061
try:
61-
created = await wscrud.add_workspace(request.name)
62+
_ = await wscrud.add_workspace(request.name)
6263
except AlreadyExistsError:
6364
raise HTTPException(status_code=409, detail="Workspace already exists")
6465
except ValidationError:
65-
raise HTTPException(status_code=400,
66-
detail=("Invalid workspace name. "
67-
"Please use only alphanumeric characters and dashes"))
66+
raise HTTPException(
67+
status_code=400,
68+
detail=(
69+
"Invalid workspace name. " "Please use only alphanumeric characters and dashes"
70+
),
71+
)
6872
except Exception:
6973
raise HTTPException(status_code=500, detail="Internal server error")
7074

71-
if created:
72-
return v1_models.Workspace(name=created.name)
75+
return v1_models.Workspace(name=request.name, is_active=False)
7376

7477

7578
@v1.delete(
7679
"/workspaces/{workspace_name}",
7780
tags=["Workspaces"],
7881
generate_unique_id_function=uniq_name,
79-
status_code=204,
8082
)
8183
async def delete_workspace(workspace_name: str):
8284
"""Delete a workspace by name."""
83-
raise NotImplementedError
85+
try:
86+
_ = await wscrud.soft_delete_workspace(workspace_name)
87+
except crud.WorkspaceDoesNotExistError:
88+
raise HTTPException(status_code=404, detail="Workspace does not exist")
89+
except Exception:
90+
raise HTTPException(status_code=500, detail="Internal server error")
91+
92+
return Response(status_code=204)

src/codegate/db/connection.py

Lines changed: 57 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,11 @@
3030
alert_queue = asyncio.Queue()
3131
fim_cache = FimCache()
3232

33+
3334
class AlreadyExistsError(Exception):
3435
pass
3536

37+
3638
class DbCodeGate:
3739
_instance = None
3840

@@ -246,16 +248,15 @@ async def record_context(self, context: Optional[PipelineContext]) -> None:
246248
except Exception as e:
247249
logger.error(f"Failed to record context: {context}.", error=str(e))
248250

249-
async def add_workspace(self, workspace_name: str) -> Optional[Workspace]:
251+
async def add_workspace(self, workspace_name: str) -> Workspace:
250252
"""Add a new workspace to the DB.
251253
252254
This handles validation and insertion of a new workspace.
253255
254256
It may raise a ValidationError if the workspace name is invalid.
255257
or a AlreadyExistsError if the workspace already exists.
256258
"""
257-
workspace = Workspace(id=str(uuid.uuid4()), name=workspace_name)
258-
259+
workspace = Workspace(id=str(uuid.uuid4()), name=workspace_name, system_prompt=None)
259260
sql = text(
260261
"""
261262
INSERT INTO workspaces (id, name)
@@ -266,12 +267,28 @@ async def add_workspace(self, workspace_name: str) -> Optional[Workspace]:
266267

267268
try:
268269
added_workspace = await self._execute_update_pydantic_model(
269-
workspace, sql, should_raise=True)
270+
workspace, sql, should_raise=True
271+
)
270272
except IntegrityError as e:
271273
logger.debug(f"Exception type: {type(e)}")
272274
raise AlreadyExistsError(f"Workspace {workspace_name} already exists.")
273275
return added_workspace
274276

277+
async def update_workspace(self, workspace: Workspace) -> Workspace:
278+
sql = text(
279+
"""
280+
UPDATE workspaces SET
281+
name = :name,
282+
system_prompt = :system_prompt
283+
WHERE id = :id
284+
RETURNING *
285+
"""
286+
)
287+
updated_workspace = await self._execute_update_pydantic_model(
288+
workspace, sql, should_raise=True
289+
)
290+
return updated_workspace
291+
275292
async def update_session(self, session: Session) -> Optional[Session]:
276293
sql = text(
277294
"""
@@ -284,9 +301,23 @@ async def update_session(self, session: Session) -> Optional[Session]:
284301
"""
285302
)
286303
# We only pass an object to respect the signature of the function
287-
active_session = await self._execute_update_pydantic_model(session, sql)
304+
active_session = await self._execute_update_pydantic_model(session, sql, should_raise=True)
288305
return active_session
289306

307+
async def soft_delete_workspace(self, workspace: Workspace) -> Optional[Workspace]:
308+
sql = text(
309+
"""
310+
UPDATE workspaces
311+
SET deleted_at = CURRENT_TIMESTAMP
312+
WHERE id = :id
313+
RETURNING *
314+
"""
315+
)
316+
deleted_workspace = await self._execute_update_pydantic_model(
317+
workspace, sql, should_raise=True
318+
)
319+
return deleted_workspace
320+
290321

291322
class DbReader(DbCodeGate):
292323

@@ -317,14 +348,21 @@ async def _execute_select_pydantic_model(
317348
return None
318349

319350
async def _exec_select_conditions_to_pydantic(
320-
self, model_type: Type[BaseModel], sql_command: TextClause, conditions: dict
351+
self,
352+
model_type: Type[BaseModel],
353+
sql_command: TextClause,
354+
conditions: dict,
355+
should_raise: bool = False,
321356
) -> Optional[List[BaseModel]]:
322357
async with self._async_db_engine.begin() as conn:
323358
try:
324359
result = await conn.execute(sql_command, conditions)
325360
return await self._dump_result_to_pydantic_model(model_type, result)
326361
except Exception as e:
327362
logger.error(f"Failed to select model with conditions: {model_type}.", error=str(e))
363+
# Exposes errors to the caller
364+
if should_raise:
365+
raise e
328366
return None
329367

330368
async def get_prompts_with_output(self) -> List[GetPromptWithOutputsRow]:
@@ -377,22 +415,25 @@ async def get_workspaces(self) -> List[WorkspaceActive]:
377415
w.id, w.name, s.active_workspace_id
378416
FROM workspaces w
379417
LEFT JOIN sessions s ON w.id = s.active_workspace_id
418+
WHERE w.deleted_at IS NULL
380419
"""
381420
)
382421
workspaces = await self._execute_select_pydantic_model(WorkspaceActive, sql)
383422
return workspaces
384423

385-
async def get_workspace_by_name(self, name: str) -> List[Workspace]:
424+
async def get_workspace_by_name(self, name: str) -> Optional[Workspace]:
386425
sql = text(
387426
"""
388427
SELECT
389-
id, name
428+
id, name, system_prompt
390429
FROM workspaces
391-
WHERE name = :name
430+
WHERE name = :name AND deleted_at IS NULL
392431
"""
393432
)
394433
conditions = {"name": name}
395-
workspaces = await self._exec_select_conditions_to_pydantic(Workspace, sql, conditions)
434+
workspaces = await self._exec_select_conditions_to_pydantic(
435+
Workspace, sql, conditions, should_raise=True
436+
)
396437
return workspaces[0] if workspaces else None
397438

398439
async def get_sessions(self) -> List[Session]:
@@ -410,7 +451,7 @@ async def get_active_workspace(self) -> Optional[ActiveWorkspace]:
410451
sql = text(
411452
"""
412453
SELECT
413-
w.id, w.name, s.id as session_id, s.last_update
454+
w.id, w.name, w.system_prompt, s.id as session_id, s.last_update
414455
FROM sessions s
415456
INNER JOIN workspaces w ON w.id = s.active_workspace_id
416457
"""
@@ -453,7 +494,11 @@ def init_session_if_not_exists(db_path: Optional[str] = None):
453494
last_update=datetime.datetime.now(datetime.timezone.utc),
454495
)
455496
db_recorder = DbRecorder(db_path)
456-
asyncio.run(db_recorder.update_session(session))
497+
try:
498+
asyncio.run(db_recorder.update_session(session))
499+
except Exception as e:
500+
logger.error(f"Failed to initialize session in DB: {e}")
501+
return
457502
logger.info("Session in DB initialized successfully.")
458503

459504

0 commit comments

Comments
 (0)