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

Commit 4f111e3

Browse files
committed
Merge branch 'main' of github.com:stacklok/codegate into workspaces-to-open-api-spec
2 parents 09e0d28 + c6d8a30 commit 4f111e3

File tree

11 files changed

+84
-55
lines changed

11 files changed

+84
-55
lines changed

src/codegate/api/v1.py

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
from fastapi import APIRouter, Response
22
from fastapi.exceptions import HTTPException
33
from fastapi.routing import APIRoute
4+
from pydantic import ValidationError
45

56
from codegate.api import v1_models
7+
from codegate.db.connection import AlreadyExistsError
68
from codegate.workspaces.crud import WorkspaceCrud
79

810
v1 = APIRouter()
@@ -52,13 +54,19 @@ async def activate_workspace(request: v1_models.ActivateWorkspaceRequest, status
5254
async def create_workspace(request: v1_models.CreateWorkspaceRequest):
5355
"""Create a new workspace."""
5456
# Input validation is done in the model
55-
created = await wscrud.add_workspace(request.name)
56-
57-
# TODO: refactor to use a more specific exception
58-
if not created:
59-
raise HTTPException(status_code=400, detail="Failed to create workspace")
60-
61-
return v1_models.Workspace(name=request.name)
57+
try:
58+
created = await wscrud.add_workspace(request.name)
59+
except AlreadyExistsError:
60+
raise HTTPException(status_code=409, detail="Workspace already exists")
61+
except ValidationError:
62+
raise HTTPException(status_code=400,
63+
detail=("Invalid workspace name. "
64+
"Please use only alphanumeric characters and dashes"))
65+
except Exception:
66+
raise HTTPException(status_code=500, detail="Internal server error")
67+
68+
if created:
69+
return v1_models.Workspace(name=created.name)
6270

6371

6472
@v1.delete(

src/codegate/dashboard/dashboard.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
import structlog
66
from fastapi import APIRouter, Depends, FastAPI
77
from fastapi.responses import StreamingResponse
8-
from codegate import __version__
98

9+
from codegate import __version__
1010
from codegate.dashboard.post_processing import (
1111
parse_get_alert_conversation,
1212
parse_messages_in_conversations,
@@ -81,7 +81,7 @@ def version_check():
8181
latest_version_stripped = latest_version.lstrip('v')
8282

8383
is_latest: bool = latest_version_stripped == current_version
84-
84+
8585
return {
8686
"current_version": current_version,
8787
"latest_version": latest_version_stripped,

src/codegate/db/connection.py

Lines changed: 26 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@
77
import structlog
88
from alembic import command as alembic_command
99
from alembic.config import Config as AlembicConfig
10-
from pydantic import BaseModel, ValidationError
10+
from pydantic import BaseModel
1111
from sqlalchemy import CursorResult, TextClause, text
12-
from sqlalchemy.exc import OperationalError
12+
from sqlalchemy.exc import IntegrityError, OperationalError
1313
from sqlalchemy.ext.asyncio import create_async_engine
1414

1515
from codegate.db.fim_cache import FimCache
@@ -30,6 +30,8 @@
3030
alert_queue = asyncio.Queue()
3131
fim_cache = FimCache()
3232

33+
class AlreadyExistsError(Exception):
34+
pass
3335

3436
class DbCodeGate:
3537
_instance = None
@@ -70,11 +72,11 @@ def __init__(self, sqlite_path: Optional[str] = None):
7072
super().__init__(sqlite_path)
7173

7274
async def _execute_update_pydantic_model(
73-
self, model: BaseModel, sql_command: TextClause
75+
self, model: BaseModel, sql_command: TextClause, should_raise: bool = False
7476
) -> Optional[BaseModel]:
7577
"""Execute an update or insert command for a Pydantic model."""
76-
async with self._async_db_engine.begin() as conn:
77-
try:
78+
try:
79+
async with self._async_db_engine.begin() as conn:
7880
result = await conn.execute(sql_command, model.model_dump())
7981
row = result.first()
8082
if row is None:
@@ -83,9 +85,11 @@ async def _execute_update_pydantic_model(
8385
# Get the class of the Pydantic object to create a new object
8486
model_class = model.__class__
8587
return model_class(**row._asdict())
86-
except Exception as e:
87-
logger.error(f"Failed to update model: {model}.", error=str(e))
88-
return None
88+
except Exception as e:
89+
logger.error(f"Failed to update model: {model}.", error=str(e))
90+
if should_raise:
91+
raise e
92+
return None
8993

9094
async def record_request(self, prompt_params: Optional[Prompt] = None) -> Optional[Prompt]:
9195
if prompt_params is None:
@@ -243,11 +247,14 @@ async def record_context(self, context: Optional[PipelineContext]) -> None:
243247
logger.error(f"Failed to record context: {context}.", error=str(e))
244248

245249
async def add_workspace(self, workspace_name: str) -> Optional[Workspace]:
246-
try:
247-
workspace = Workspace(id=str(uuid.uuid4()), name=workspace_name)
248-
except ValidationError as e:
249-
logger.error(f"Failed to create workspace with name: {workspace_name}: {str(e)}")
250-
return None
250+
"""Add a new workspace to the DB.
251+
252+
This handles validation and insertion of a new workspace.
253+
254+
It may raise a ValidationError if the workspace name is invalid.
255+
or a AlreadyExistsError if the workspace already exists.
256+
"""
257+
workspace = Workspace(id=str(uuid.uuid4()), name=workspace_name)
251258

252259
sql = text(
253260
"""
@@ -256,12 +263,13 @@ async def add_workspace(self, workspace_name: str) -> Optional[Workspace]:
256263
RETURNING *
257264
"""
258265
)
259-
try:
260-
added_workspace = await self._execute_update_pydantic_model(workspace, sql)
261-
except Exception as e:
262-
logger.error(f"Failed to add workspace: {workspace_name}.", error=str(e))
263-
return None
264266

267+
try:
268+
added_workspace = await self._execute_update_pydantic_model(
269+
workspace, sql, should_raise=True)
270+
except IntegrityError as e:
271+
logger.debug(f"Exception type: {type(e)}")
272+
raise AlreadyExistsError(f"Workspace {workspace_name} already exists.")
265273
return added_workspace
266274

267275
async def update_session(self, session: Session) -> Optional[Session]:

src/codegate/pipeline/cli/cli.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,11 @@ async def codegate_cli(command):
3232
available_commands = {
3333
"version": Version().exec,
3434
"workspace": Workspace().exec,
35-
"-h": lambda _: HELP_TEXT,
3635
}
3736
out_func = available_commands.get(command[0])
3837
if out_func is None:
38+
if command[0] == "-h":
39+
return HELP_TEXT
3940
return NOT_FOUND_TEXT
4041

4142
return await out_func(command[1:])

src/codegate/pipeline/cli/commands.py

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
from abc import ABC, abstractmethod
22
from typing import List
33

4+
from pydantic import ValidationError
5+
46
from codegate import __version__
7+
from codegate.db.connection import AlreadyExistsError
58
from codegate.workspaces.crud import WorkspaceCrud
69

710

@@ -63,37 +66,39 @@ async def _add_workspace(self, args: List[str]) -> str:
6366
Add a workspace
6467
"""
6568
if args is None or len(args) == 0:
66-
return "Please provide a name. Use `codegate-workspace add your_workspace_name`"
69+
return "Please provide a name. Use `codegate workspace add your_workspace_name`"
6770

6871
new_workspace_name = args[0]
6972
if not new_workspace_name:
70-
return "Please provide a name. Use `codegate-workspace add your_workspace_name`"
73+
return "Please provide a name. Use `codegate workspace add your_workspace_name`"
74+
75+
try:
76+
_ = await self.workspace_crud.add_workspace(new_workspace_name)
77+
except ValidationError:
78+
return "Invalid workspace name: It should be alphanumeric and dashes"
79+
except AlreadyExistsError:
80+
return f"Workspace **{new_workspace_name}** already exists"
81+
except Exception:
82+
return "An error occurred while adding the workspace"
7183

72-
workspace_created = await self.workspace_crud.add_workspace(new_workspace_name)
73-
if not workspace_created:
74-
return (
75-
"Something went wrong. Workspace could not be added.\n"
76-
"1. Check if the name is alphanumeric and only contains dashes, and underscores.\n"
77-
"2. Check if the workspace already exists."
78-
)
7984
return f"Workspace **{new_workspace_name}** has been added"
8085

8186
async def _activate_workspace(self, args: List[str]) -> str:
8287
"""
8388
Activate a workspace
8489
"""
8590
if args is None or len(args) == 0:
86-
return "Please provide a name. Use `codegate-workspace activate workspace_name`"
91+
return "Please provide a name. Use `codegate workspace activate workspace_name`"
8792

8893
workspace_name = args[0]
8994
if not workspace_name:
90-
return "Please provide a name. Use `codegate-workspace activate workspace_name`"
95+
return "Please provide a name. Use `codegate workspace activate workspace_name`"
9196

9297
was_activated = await self.workspace_crud.activate_workspace(workspace_name)
9398
if not was_activated:
9499
return (
95100
f"Workspace **{workspace_name}** does not exist or was already active. "
96-
f"Use `codegate-workspace add {workspace_name}` to add it"
101+
f"Use `codegate workspace add {workspace_name}` to add it"
97102
)
98103
return f"Workspace **{workspace_name}** has been activated"
99104

src/codegate/providers/ollama/adapter.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,8 @@ def normalize(self, data: Dict) -> ChatCompletionRequest:
2525
{"content": normalized_data.pop("prompt"), "role": "user"}
2626
]
2727

28-
# In Ollama force the stream to be True. Continue is not setting this parameter and
29-
# most of our functionality is for streaming completions.
30-
normalized_data["stream"] = True
31-
28+
# if we have the stream flag in data we set it, otherwise defaults to true
29+
normalized_data["stream"] = data.get("stream", True)
3230
return ChatCompletionRequest(**normalized_data)
3331

3432
def denormalize(self, data: ChatCompletionRequest) -> Dict:

src/codegate/providers/ollama/completion_handler.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ def _create_streaming_response(self, stream: AsyncIterator[ChatResponse]) -> Str
6060
"""
6161
return StreamingResponse(
6262
ollama_stream_generator(stream),
63-
media_type="application/x-ndjson",
63+
media_type="application/x-ndjson; charset=utf-8",
6464
headers={
6565
"Cache-Control": "no-cache",
6666
"Connection": "keep-alive",
@@ -70,4 +70,4 @@ def _create_streaming_response(self, stream: AsyncIterator[ChatResponse]) -> Str
7070
def _create_json_response(
7171
self, response: Union[GenerateResponse, ChatResponse]
7272
) -> JSONResponse:
73-
return JSONResponse(content=response.model_dump_json(), status_code=200)
73+
return JSONResponse(status_code=200, content=response.model_dump())

src/codegate/providers/ollama/provider.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ async def show_model(request: Request):
6565
response = await client.post(
6666
f"{self.base_url}/api/show",
6767
content=body,
68-
headers={"Content-Type": "application/json"},
68+
headers={"Content-Type": "application/json; charset=utf-8"},
6969
)
7070
return response.json()
7171

src/codegate/providers/registry.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ def add_provider(self, name: str, provider: BaseProvider):
1616
to the FastAPI app.
1717
"""
1818
self.providers[name] = provider
19-
self.app.include_router(provider.get_routes())
19+
self.app.include_router(provider.get_routes(), include_in_schema=False)
2020

2121
def get_provider(self, name: str) -> Optional[BaseProvider]:
2222
"""

src/codegate/workspaces/crud.py

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
11
import datetime
2-
from typing import Optional, Tuple
2+
from typing import List, Optional, Tuple
33

44
from codegate.db.connection import DbReader, DbRecorder
5-
from codegate.db.models import Session, Workspace
5+
from codegate.db.models import ActiveWorkspace, Session, Workspace, WorkspaceActive
66

77

8+
class WorkspaceCrudError(Exception):
9+
pass
10+
811
class WorkspaceCrud:
912

1013
def __init__(self):
1114
self._db_reader = DbReader()
1215

13-
async def add_workspace(self, new_workspace_name: str) -> bool:
16+
async def add_workspace(self, new_workspace_name: str) -> Workspace:
1417
"""
1518
Add a workspace
1619
@@ -19,14 +22,20 @@ async def add_workspace(self, new_workspace_name: str) -> bool:
1922
"""
2023
db_recorder = DbRecorder()
2124
workspace_created = await db_recorder.add_workspace(new_workspace_name)
22-
return bool(workspace_created)
25+
return workspace_created
2326

24-
async def get_workspaces(self):
27+
async def get_workspaces(self)-> List[WorkspaceActive]:
2528
"""
2629
Get all workspaces
2730
"""
2831
return await self._db_reader.get_workspaces()
2932

33+
async def get_active_workspace(self) -> Optional[ActiveWorkspace]:
34+
"""
35+
Get the active workspace
36+
"""
37+
return await self._db_reader.get_active_workspace()
38+
3039
async def _is_workspace_active_or_not_exist(
3140
self, workspace_name: str
3241
) -> Tuple[bool, Optional[Session], Optional[Workspace]]:

0 commit comments

Comments
 (0)