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

Commit 0c50847

Browse files
Moved DB objects to Pydantic and added a class
1 parent 7507703 commit 0c50847

File tree

10 files changed

+268
-368
lines changed

10 files changed

+268
-368
lines changed

sql/queries/queries.sql

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,8 @@ INSERT INTO prompts (
55
provider,
66
system_prompt,
77
user_prompt,
8-
type,
9-
status
10-
) VALUES (?, ?, ?, ?, ?, ?, ?) RETURNING *;
8+
type
9+
) VALUES (?, ?, ?, ?, ?, ?) RETURNING *;
1110

1211
-- name: GetPrompt :one
1312
SELECT * FROM prompts WHERE id = ?;
@@ -22,9 +21,8 @@ INSERT INTO outputs (
2221
id,
2322
prompt_id,
2423
timestamp,
25-
output,
26-
status
27-
) VALUES (?, ?, ?, ?, ?) RETURNING *;
24+
output
25+
) VALUES (?, ?, ?, ?) RETURNING *;
2826

2927
-- name: GetOutput :one
3028
SELECT * FROM outputs WHERE id = ?;
@@ -79,7 +77,6 @@ SELECT
7977
p.*,
8078
o.id as output_id,
8179
o.output,
82-
o.status as output_status,
8380
a.id as alert_id,
8481
a.code_snippet,
8582
a.trigger_string,

sql/schema/schema.sql

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,7 @@ CREATE TABLE prompts (
77
provider TEXT, -- VARCHAR(255)
88
system_prompt TEXT,
99
user_prompt TEXT NOT NULL,
10-
type TEXT NOT NULL, -- VARCHAR(50) (e.g. "fim", "chat")
11-
status TEXT NOT NULL -- VARCHAR(50)
10+
type TEXT NOT NULL -- VARCHAR(50) (e.g. "fim", "chat")
1211
);
1312

1413
-- Outputs table
@@ -17,7 +16,6 @@ CREATE TABLE outputs (
1716
prompt_id TEXT NOT NULL,
1817
timestamp DATETIME NOT NULL,
1918
output TEXT NOT NULL,
20-
status TEXT NOT NULL, -- VARCHAR(50)
2119
FOREIGN KEY (prompt_id) REFERENCES prompts(id)
2220
);
2321

sqlc.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,4 @@ sql:
1717
emit_sync_querier: true
1818
emit_async_querier: true
1919
query_parameter_limit: 5
20+
emit_pydantic_models: true

src/codegate/db/connection.py

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
import asyncio
2+
import datetime
3+
import uuid
4+
from pathlib import Path
5+
from typing import Optional
6+
7+
import structlog
8+
from litellm import ChatCompletionRequest
9+
from sqlalchemy import create_engine, text
10+
from sqlalchemy.ext.asyncio import create_async_engine
11+
12+
from codegate.db.models import Prompt
13+
14+
logger = structlog.get_logger("codegate")
15+
16+
17+
class DbRecorder:
18+
19+
def __init__(self, sqlite_path: Optional[str] = None):
20+
# Initialize SQLite database engine with proper async URL
21+
if not sqlite_path:
22+
current_dir = Path(__file__).parent
23+
self._db_path = (current_dir.parent.parent.parent / "codegate.db").absolute()
24+
else:
25+
self._db_path = Path(sqlite_path).absolute()
26+
27+
logger.debug(f"Initializing DB from path: {self._db_path}")
28+
engine_dict = {
29+
"url": f"sqlite+aiosqlite:///{self._db_path}",
30+
"echo": True, # Set to False in production
31+
"isolation_level": "AUTOCOMMIT", # Required for SQLite
32+
}
33+
self._async_db_engine = create_async_engine(**engine_dict)
34+
self._db_engine = create_engine(**engine_dict)
35+
36+
if not self.does_db_exist():
37+
logger.info(f"Database does not exist at {self._db_path}. Creating..")
38+
asyncio.run(self.init_db())
39+
40+
def does_db_exist(self):
41+
return self._db_path.is_file()
42+
43+
async def init_db(self):
44+
"""Initialize the database with the schema."""
45+
if self.does_db_exist():
46+
logger.info("Database already exists. Skipping initialization.")
47+
return
48+
49+
# Get the absolute path to the schema file
50+
current_dir = Path(__file__).parent
51+
schema_path = current_dir.parent.parent.parent / "sql" / "schema" / "schema.sql"
52+
53+
if not schema_path.exists():
54+
raise FileNotFoundError(f"Schema file not found at {schema_path}")
55+
56+
# Read the schema
57+
with open(schema_path, "r") as f:
58+
schema = f.read()
59+
60+
try:
61+
# Execute the schema
62+
async with self._async_db_engine.begin() as conn:
63+
# Split the schema into individual statements and execute each one
64+
statements = [stmt.strip() for stmt in schema.split(";") if stmt.strip()]
65+
for statement in statements:
66+
# Use SQLAlchemy text() to create executable SQL statements
67+
await conn.execute(text(statement))
68+
finally:
69+
await self._async_db_engine.dispose()
70+
71+
async def record_request(
72+
self,
73+
normalized_request: ChatCompletionRequest,
74+
is_fim_request: bool,
75+
provider_str: str
76+
) -> Optional[Prompt]:
77+
# Extract system prompt and user prompt from the messages
78+
messages = normalized_request.get("messages", [])
79+
system_prompt = []
80+
user_prompt = []
81+
82+
for msg in messages:
83+
if msg.get("role") == "system":
84+
system_prompt.append(msg.get("content"))
85+
elif msg.get("role") == "user":
86+
user_prompt.append(msg.get("content"))
87+
88+
# If no user prompt found in messages, try to get from the prompt field
89+
# (for non-chat completions)
90+
if not user_prompt:
91+
prompt = normalized_request.get("prompt")
92+
if prompt:
93+
user_prompt.append(prompt)
94+
95+
if not user_prompt:
96+
logger.warning("No user prompt found in request.")
97+
return None
98+
99+
# Create a new prompt record
100+
prompt_params = Prompt(
101+
id=str(uuid.uuid4()), # Generate a new UUID for the prompt
102+
timestamp=datetime.datetime.now(datetime.timezone.utc),
103+
provider=provider_str,
104+
type="fim" if is_fim_request else "chat",
105+
user_prompt="<|>".join(user_prompt),
106+
system_prompt="<|>".join(system_prompt),
107+
)
108+
# There is a `create_prompt` method in queries.py automatically generated by sqlc
109+
# However, the method is is buggy and doesn't work as expected.
110+
# Manually writing the SQL query to insert the prompt record.
111+
async with self._async_db_engine.begin() as conn:
112+
sql = text(
113+
"""
114+
INSERT INTO prompts (id, timestamp, provider, system_prompt, user_prompt, type)
115+
VALUES (:id, :timestamp, :provider, :system_prompt, :user_prompt, :type)
116+
RETURNING *
117+
"""
118+
)
119+
result = await conn.execute(sql, prompt_params.model_dump())
120+
row = result.first()
121+
if row is None:
122+
return None
123+
124+
return Prompt(
125+
id=row.id,
126+
timestamp=row.timestamp,
127+
provider=row.provider,
128+
system_prompt=row.system_prompt,
129+
user_prompt=row.user_prompt,
130+
type=row.type
131+
)
132+
133+
134+
def init_db_sync():
135+
"""DB will be initialized in the constructor in case it doesn't exist."""
136+
DbRecorder()
137+
138+
139+
if __name__ == "__main__":
140+
init_db_sync()

src/codegate/db/init_db.py

Lines changed: 0 additions & 47 deletions
This file was deleted.

src/codegate/db/models.py

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
# Code generated by sqlc. DO NOT EDIT.
22
# versions:
33
# sqlc v1.27.0
4-
import dataclasses
4+
import pydantic
55
from typing import Any, Optional
66

77

8-
@dataclasses.dataclass()
9-
class Alert:
8+
class Alert(pydantic.BaseModel):
109
id: Any
1110
prompt_id: Any
1211
output_id: Any
@@ -17,28 +16,23 @@ class Alert:
1716
timestamp: Any
1817

1918

20-
@dataclasses.dataclass()
21-
class Output:
19+
class Output(pydantic.BaseModel):
2220
id: Any
2321
prompt_id: Any
2422
timestamp: Any
2523
output: Any
26-
status: Any
2724

2825

29-
@dataclasses.dataclass()
30-
class Prompt:
26+
class Prompt(pydantic.BaseModel):
3127
id: Any
3228
timestamp: Any
3329
provider: Optional[Any]
3430
system_prompt: Optional[Any]
3531
user_prompt: Any
3632
type: Any
37-
status: Any
3833

3934

40-
@dataclasses.dataclass()
41-
class Setting:
35+
class Setting(pydantic.BaseModel):
4236
id: Any
4337
ip: Optional[Any]
4438
port: Optional[Any]

0 commit comments

Comments
 (0)