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

Commit 4d573ae

Browse files
Record the output of the LLM in the DB
We are going to store the complete output of the LLMs. - If the response was stream then a list of JSON objects will get stored. - If not stream then a single JSON object will get stored. The PR also changes the initial schema. The schema will now also store the complete input request to the LLM. Storing the complete input and output should help debug any problem with CodeGate as well as reproduce faithfully in the dashboard all the conversations.
1 parent 47c4330 commit 4d573ae

File tree

7 files changed

+168
-135
lines changed

7 files changed

+168
-135
lines changed

pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,9 @@ target-version = ["py310"]
5151
line-length = 100
5252
target-version = "py310"
5353
fix = true
54+
exclude = [
55+
"src/codegate/db/queries.py", # Ignore auto-generated file from sqlc
56+
]
5457

5558
[tool.ruff.lint]
5659
select = ["E", "F", "I", "N", "W"]

sql/queries/queries.sql

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,9 @@ INSERT INTO prompts (
33
id,
44
timestamp,
55
provider,
6-
system_prompt,
7-
user_prompt,
6+
request,
87
type
9-
) VALUES (?, ?, ?, ?, ?, ?) RETURNING *;
8+
) VALUES (?, ?, ?, ?, ?) RETURNING *;
109

1110
-- name: GetPrompt :one
1211
SELECT * FROM prompts WHERE id = ?;

sql/schema/schema.sql

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,7 @@ CREATE TABLE prompts (
55
id TEXT PRIMARY KEY, -- UUID stored as TEXT
66
timestamp DATETIME NOT NULL,
77
provider TEXT, -- VARCHAR(255)
8-
system_prompt TEXT,
9-
user_prompt TEXT NOT NULL,
8+
request TEXT NOT NULL, -- Record the full request that arrived to the server
109
type TEXT NOT NULL -- VARCHAR(50) (e.g. "fim", "chat")
1110
);
1211

@@ -15,7 +14,7 @@ CREATE TABLE outputs (
1514
id TEXT PRIMARY KEY, -- UUID stored as TEXT
1615
prompt_id TEXT NOT NULL,
1716
timestamp DATETIME NOT NULL,
18-
output TEXT NOT NULL,
17+
output TEXT NOT NULL, -- Record the full response. If it was stream will be a list of objects.
1918
FOREIGN KEY (prompt_id) REFERENCES prompts(id)
2019
);
2120

src/codegate/db/connection.py

Lines changed: 97 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
11
import asyncio
2+
import copy
23
import datetime
4+
import json
35
import uuid
46
from pathlib import Path
5-
from typing import Optional
7+
from typing import AsyncGenerator, AsyncIterator, Optional
68

79
import structlog
8-
from litellm import ChatCompletionRequest
10+
from litellm import ChatCompletionRequest, ModelResponse
11+
from pydantic import BaseModel
912
from sqlalchemy import create_engine, text
1013
from sqlalchemy.ext.asyncio import create_async_engine
1114

12-
from codegate.db.models import Prompt
15+
from codegate.db.models import Output, Prompt
1316

1417
logger = structlog.get_logger("codegate")
1518

@@ -68,64 +71,112 @@ async def init_db(self):
6871
finally:
6972
await self._async_db_engine.dispose()
7073

74+
async def _insert_pydantic_model(
75+
self, model: BaseModel, sql_insert: text
76+
) -> Optional[BaseModel]:
77+
# There are create method in queries.py automatically generated by sqlc
78+
# However, the methods are buggy for Pydancti and don't work as expected.
79+
# Manually writing the SQL query to insert Pydantic models.
80+
async with self._async_db_engine.begin() as conn:
81+
result = await conn.execute(sql_insert, model.model_dump())
82+
row = result.first()
83+
if row is None:
84+
return None
85+
86+
# Get the class of the Pydantic object to create a new object
87+
model_class = model.__class__
88+
return model_class(**row._asdict())
89+
7190
async def record_request(
7291
self, normalized_request: ChatCompletionRequest, is_fim_request: bool, provider_str: str
7392
) -> Optional[Prompt]:
74-
# Extract system prompt and user prompt from the messages
75-
messages = normalized_request.get("messages", [])
76-
system_prompt = []
77-
user_prompt = []
78-
79-
for msg in messages:
80-
if msg.get("role") == "system":
81-
system_prompt.append(msg.get("content"))
82-
elif msg.get("role") == "user":
83-
user_prompt.append(msg.get("content"))
84-
85-
# If no user prompt found in messages, try to get from the prompt field
86-
# (for non-chat completions)
87-
if not user_prompt:
88-
prompt = normalized_request.get("prompt")
89-
if prompt:
90-
user_prompt.append(prompt)
91-
92-
if not user_prompt:
93-
logger.warning("No user prompt found in request.")
94-
return None
93+
request_str = None
94+
if isinstance(normalized_request, BaseModel):
95+
request_str = normalized_request.model_dump_json(exclude_none=True, exclude_unset=True)
96+
else:
97+
try:
98+
request_str = json.dumps(normalized_request)
99+
except Exception as e:
100+
logger.error(f"Failed to serialize output: {normalized_request}", error=str(e))
101+
102+
if request_str is None:
103+
logger.warning("No request found to record.")
104+
return
95105

96106
# Create a new prompt record
97107
prompt_params = Prompt(
98108
id=str(uuid.uuid4()), # Generate a new UUID for the prompt
99109
timestamp=datetime.datetime.now(datetime.timezone.utc),
100110
provider=provider_str,
101111
type="fim" if is_fim_request else "chat",
102-
user_prompt="<|>".join(user_prompt),
103-
system_prompt="<|>".join(system_prompt),
112+
request=request_str,
104113
)
105-
# There is a `create_prompt` method in queries.py automatically generated by sqlc
106-
# However, the method is is buggy and doesn't work as expected.
107-
# Manually writing the SQL query to insert the prompt record.
108-
async with self._async_db_engine.begin() as conn:
109-
sql = text(
114+
sql = text(
115+
"""
116+
INSERT INTO prompts (id, timestamp, provider, request, type)
117+
VALUES (:id, :timestamp, :provider, :request, :type)
118+
RETURNING *
110119
"""
111-
INSERT INTO prompts (id, timestamp, provider, system_prompt, user_prompt, type)
112-
VALUES (:id, :timestamp, :provider, :system_prompt, :user_prompt, :type)
120+
)
121+
return await self._insert_pydantic_model(prompt_params, sql)
122+
123+
async def _record_output(self, prompt: Prompt, output_str: str) -> Optional[Output]:
124+
output_params = Output(
125+
id=str(uuid.uuid4()),
126+
prompt_id=prompt.id,
127+
timestamp=datetime.datetime.now(datetime.timezone.utc),
128+
output=output_str,
129+
)
130+
sql = text(
131+
"""
132+
INSERT INTO outputs (id, prompt_id, timestamp, output)
133+
VALUES (:id, :prompt_id, :timestamp, :output)
113134
RETURNING *
114135
"""
115-
)
116-
result = await conn.execute(sql, prompt_params.model_dump())
117-
row = result.first()
118-
if row is None:
119-
return None
136+
)
137+
return await self._insert_pydantic_model(output_params, sql)
138+
139+
async def record_output_stream(
140+
self, prompt: Prompt, model_response: AsyncIterator
141+
) -> AsyncGenerator:
142+
output_chunks = []
143+
async for chunk in model_response:
144+
if isinstance(chunk, BaseModel):
145+
chunk_to_record = chunk.model_dump(exclude_none=True, exclude_unset=True)
146+
output_chunks.append(chunk_to_record)
147+
elif isinstance(chunk, dict):
148+
output_chunks.append(copy.deepcopy(chunk))
149+
else:
150+
output_chunks.append({"chunk": str(chunk)})
151+
yield chunk
152+
153+
if output_chunks:
154+
# Record the output chunks
155+
output_str = json.dumps(output_chunks)
156+
logger.info(f"Recorded chunks: {output_chunks}. Str: {output_str}")
157+
await self._record_output(prompt, output_str)
158+
159+
async def record_output_non_stream(
160+
self, prompt: Optional[Prompt], model_response: ModelResponse
161+
) -> Optional[Output]:
162+
if prompt is None:
163+
logger.warning("No prompt found to record output.")
164+
return
165+
166+
output_str = None
167+
if isinstance(model_response, BaseModel):
168+
output_str = model_response.model_dump_json(exclude_none=True, exclude_unset=True)
169+
else:
170+
try:
171+
output_str = json.dumps(model_response)
172+
except Exception as e:
173+
logger.error(f"Failed to serialize output: {model_response}", error=str(e))
174+
175+
if output_str is None:
176+
logger.warning("No output found to record.")
177+
return
120178

121-
return Prompt(
122-
id=row.id,
123-
timestamp=row.timestamp,
124-
provider=row.provider,
125-
system_prompt=row.system_prompt,
126-
user_prompt=row.user_prompt,
127-
type=row.type,
128-
)
179+
return await self._record_output(prompt, output_str)
129180

130181

131182
def init_db_sync():

src/codegate/db/models.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,7 @@ class Prompt(pydantic.BaseModel):
2828
id: Any
2929
timestamp: Any
3030
provider: Optional[Any]
31-
system_prompt: Optional[Any]
32-
user_prompt: Any
31+
request: Any
3332
type: Any
3433

3534

0 commit comments

Comments
 (0)