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

Commit d738353

Browse files
Merge pull request #8 from justgithubaccount/codex/migrate-projectmemory-to-sqlmodel/postgresql
Move project state to SQLModel
2 parents d8d81a2 + c67cfa0 commit d738353

File tree

9 files changed

+117
-12
lines changed

9 files changed

+117
-12
lines changed

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,18 @@ make test # pytest
108108
make run # запускает FastAPI
109109
```
110110

111+
### Работа с БД и миграции
112+
113+
Используется SQLModel и PostgreSQL. URL подключения задаётся переменной `DATABASE_URL` (по умолчанию SQLite `sqlite:///db.sqlite3`).
114+
115+
Инициализация и миграции через Alembic:
116+
117+
```bash
118+
alembic init alembic # однократная инициализация
119+
alembic revision --autogenerate -m "init"
120+
alembic upgrade head
121+
```
122+
111123
### Makefile (доступные команды):
112124

113125
```bash

apps/chat/app/api.py

Lines changed: 56 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,13 @@
44
CreateProjectRequest, ProjectInfo,
55
)
66
from .models import ChatHistory, StoredChatMessage
7+
from .models import Project, ChatHistoryDB
78
from .integrations.behavior_manager import BehaviorManager
89
from .services.chat_service import ChatService
910
from .core.project_memory import ProjectMemory
11+
from .core.db import get_session
12+
from sqlmodel import Session, select
13+
import json
1014
from app.logger import enrich_context
1115
from opentelemetry import metrics
1216
from opentelemetry.trace import get_current_span
@@ -17,6 +21,7 @@
1721
tags=["chat"]
1822
)
1923

24+
# In-memory fallback (not used when database is configured)
2025
memory = ProjectMemory()
2126
meter = metrics.get_meter(__name__)
2227
chat_counter = meter.create_counter("chat_requests_total")
@@ -55,25 +60,33 @@ async def chat_endpoint(
5560
raise HTTPException(status_code=500, detail="AI сервис недоступен")
5661

5762
@api_router.post("/projects", response_model=ProjectInfo)
58-
def create_project(req: CreateProjectRequest):
59-
project = memory.create_project(req.name)
63+
def create_project(
64+
req: CreateProjectRequest,
65+
session: Session = Depends(get_session),
66+
):
67+
project = Project(name=req.name)
68+
session.add(project)
69+
session.commit()
70+
session.refresh(project)
6071
enrich_context(
6172
event="project_created",
6273
project_id=project.id,
6374
project_name=project.name
6475
).info("New project created")
65-
return project
76+
return ProjectInfo(id=project.id, name=project.name)
6677

6778
@api_router.get("/projects", response_model=list[ProjectInfo])
68-
def list_projects():
79+
def list_projects(session: Session = Depends(get_session)):
6980
enrich_context(event="project_list_requested").info("Project list requested")
70-
return memory.list_projects()
81+
projects = session.exec(select(Project)).all()
82+
return [ProjectInfo(id=p.id, name=p.name) for p in projects]
7183

7284
@api_router.post("/projects/{project_id}/chat", response_model=ChatResponse)
7385
async def chat_in_project(
7486
project_id: str,
7587
req: ChatRequest,
76-
chat_service: ChatService = Depends(get_chat_service)
88+
chat_service: ChatService = Depends(get_chat_service),
89+
session: Session = Depends(get_session)
7790
):
7891
log = enrich_context(
7992
event="project_chat_called",
@@ -94,11 +107,23 @@ async def chat_in_project(
94107
span_id = format(ctx.span_id, "016x")
95108

96109
try:
110+
# Ensure project exists
111+
project = session.get(Project, project_id)
112+
if not project:
113+
raise ValueError(f"Проект {project_id} не найден")
114+
97115
chat_messages = [
98116
StoredChatMessage(role=m.role, content=m.content)
99117
for m in req.messages
100118
]
101-
memory.add_chat(project_id, chat_messages, trace_id=trace_id, span_id=span_id)
119+
history_db = ChatHistoryDB(
120+
project_id=project_id,
121+
messages=json.dumps([m.model_dump() for m in chat_messages]),
122+
trace_id=trace_id,
123+
span_id=span_id,
124+
)
125+
session.add(history_db)
126+
session.commit()
102127

103128
reply = await chat_service.get_ai_reply(
104129
req.messages, req.user_api_key, project_id=project_id, trace_id=trace_id
@@ -115,7 +140,10 @@ async def chat_in_project(
115140
raise HTTPException(status_code=500, detail="Ошибка при обращении к AI")
116141

117142
@api_router.get("/projects/{project_id}/history", response_model=list[ChatHistory])
118-
def get_project_history(project_id: str):
143+
def get_project_history(
144+
project_id: str,
145+
session: Session = Depends(get_session)
146+
):
119147
log = enrich_context(
120148
event="project_history_requested",
121149
project_id=project_id
@@ -124,7 +152,26 @@ def get_project_history(project_id: str):
124152
log.info("Project history requested")
125153

126154
try:
127-
return memory.get_project_history(project_id)
155+
project = session.get(Project, project_id)
156+
if not project:
157+
raise ValueError(f"Проект {project_id} не найден")
158+
histories = session.exec(
159+
select(ChatHistoryDB).where(ChatHistoryDB.project_id == project_id)
160+
).all()
161+
result = []
162+
for h in histories:
163+
messages = [StoredChatMessage(**m) for m in json.loads(h.messages)]
164+
result.append(
165+
ChatHistory(
166+
id=h.id,
167+
project_id=h.project_id,
168+
messages=messages,
169+
trace_id=h.trace_id,
170+
span_id=h.span_id,
171+
timestamp=h.timestamp,
172+
)
173+
)
174+
return result
128175
except ValueError as e:
129176
log.bind(event="project_history_not_found").warning("Project not found when fetching history")
130177
raise HTTPException(status_code=404, detail=str(e))

apps/chat/app/core/config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ class Settings(BaseSettings):
1515
project_name: str = "ChatMicroservice"
1616
notion_token: str = os.getenv("NOTION_TOKEN", "")
1717
notion_page_id: str = os.getenv("NOTION_PAGE_ID", "")
18+
database_url: str = os.getenv("DATABASE_URL", "sqlite:///db.sqlite3")
1819

1920
class Config:
2021
env_file = ".env"

apps/chat/app/core/db.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
from sqlmodel import SQLModel, create_engine, Session
2+
from app.core.config import get_settings
3+
4+
DATABASE_URL = get_settings().database_url
5+
engine = create_engine(DATABASE_URL, echo=True)
6+
7+
8+
def init_db() -> None:
9+
SQLModel.metadata.create_all(engine)
10+
11+
12+
def get_session():
13+
with Session(engine) as session:
14+
yield session

apps/chat/app/main.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from .observability.tracing import setup_tracing
77
from .logger import enrich_context
88
from .core.config import get_settings
9+
from .core.db import init_db
910
from .integrations.notion_client import NotionClient
1011
from .integrations.behavior_manager import BehaviorManager
1112

@@ -20,6 +21,10 @@ def create_app() -> FastAPI:
2021

2122
settings = get_settings()
2223

24+
@app.on_event("startup")
25+
def _init_db() -> None:
26+
init_db()
27+
2328
if settings.notion_token and settings.notion_page_id:
2429
notion_client = NotionClient(settings.notion_token)
2530
behavior_manager = BehaviorManager(notion_client, settings.notion_page_id)

apps/chat/app/models/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
from .chat import StoredChatMessage, ChatHistory
2+
from .db_models import Project, ChatHistoryDB
23

34
__all__ = [
45
"StoredChatMessage",
56
"ChatHistory",
7+
"Project",
8+
"ChatHistoryDB",
69
]

apps/chat/app/models/db_models.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
from sqlmodel import SQLModel, Field
2+
from typing import Optional
3+
from datetime import datetime
4+
import uuid
5+
6+
class Project(SQLModel, table=True):
7+
id: str = Field(default_factory=lambda: str(uuid.uuid4()), primary_key=True)
8+
name: str
9+
10+
class ChatHistoryDB(SQLModel, table=True):
11+
id: str = Field(default_factory=lambda: str(uuid.uuid4()), primary_key=True)
12+
project_id: str = Field(foreign_key="project.id")
13+
messages: str
14+
trace_id: Optional[str] = None
15+
span_id: Optional[str] = None
16+
timestamp: datetime = Field(default_factory=datetime.utcnow)
17+

apps/chat/requirements.txt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,7 @@ structlog
88
opentelemetry-api
99
opentelemetry-sdk
1010
opentelemetry-instrumentation-fastapi
11-
crewai
11+
crewai
12+
sqlmodel
13+
psycopg2-binary
14+
alembic

pyproject.toml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,17 @@ dependencies = [
2323
"opentelemetry-sdk",
2424
"opentelemetry-instrumentation-fastapi",
2525
"PyYAML",
26-
"notion-client"
26+
"notion-client",
27+
"sqlmodel",
28+
"psycopg2-binary"
2729
]
2830

2931
[project.optional-dependencies]
3032
dev = [
3133
"pytest",
3234
"httpx",
33-
"pytest-asyncio"
35+
"pytest-asyncio",
36+
"alembic"
3437
]
3538

3639
[project.urls]

0 commit comments

Comments
 (0)