/fix v2 chat stream api update to analyses_messages
#7
by rhbt6767 - opened
- src/agents/chat_handler.py +3 -3
- src/agents/gate.py +16 -15
- src/agents/state_store.py +14 -15
- src/api/v1/chat.py +29 -24
- src/api/v1/report.py +1 -1
- src/api/v2/chat.py +3 -3
- src/db/postgres/models.py +31 -16
src/agents/chat_handler.py
CHANGED
|
@@ -215,13 +215,13 @@ class ChatHandler:
|
|
| 215 |
from .gate import stub_analysis_state
|
| 216 |
|
| 217 |
if not analysis_id:
|
| 218 |
-
return stub_analysis_state(
|
| 219 |
try:
|
| 220 |
state = await self._get_state_store().get(analysis_id)
|
| 221 |
except Exception as e:
|
| 222 |
logger.warning("help state read failed — not-validated", error=str(e))
|
| 223 |
state = None
|
| 224 |
-
return state if state is not None else stub_analysis_state(
|
| 225 |
|
| 226 |
# ------------------------------------------------------------------
|
| 227 |
# Public entry
|
|
@@ -320,7 +320,7 @@ class ChatHandler:
|
|
| 320 |
# intent,
|
| 321 |
# analysis_state
|
| 322 |
# if analysis_state is not None
|
| 323 |
-
# else stub_analysis_state(
|
| 324 |
# )
|
| 325 |
|
| 326 |
# The `intent` event is consumed by the endpoint (it gates response caching
|
|
|
|
| 215 |
from .gate import stub_analysis_state
|
| 216 |
|
| 217 |
if not analysis_id:
|
| 218 |
+
return stub_analysis_state()
|
| 219 |
try:
|
| 220 |
state = await self._get_state_store().get(analysis_id)
|
| 221 |
except Exception as e:
|
| 222 |
logger.warning("help state read failed — not-validated", error=str(e))
|
| 223 |
state = None
|
| 224 |
+
return state if state is not None else stub_analysis_state()
|
| 225 |
|
| 226 |
# ------------------------------------------------------------------
|
| 227 |
# Public entry
|
|
|
|
| 320 |
# intent,
|
| 321 |
# analysis_state
|
| 322 |
# if analysis_state is not None
|
| 323 |
+
# else stub_analysis_state(),
|
| 324 |
# )
|
| 325 |
|
| 326 |
# The `intent` event is consumed by the endpoint (it gates response caching
|
src/agents/gate.py
CHANGED
|
@@ -20,7 +20,7 @@ from __future__ import annotations
|
|
| 20 |
|
| 21 |
from datetime import UTC, datetime
|
| 22 |
|
| 23 |
-
from pydantic import BaseModel
|
| 24 |
|
| 25 |
from src.agents.orchestration import Intent
|
| 26 |
from src.middlewares.logging import get_logger
|
|
@@ -29,17 +29,18 @@ logger = get_logger("gate")
|
|
| 29 |
|
| 30 |
|
| 31 |
class AnalysisState(BaseModel):
|
| 32 |
-
"""Per-analysis state the
|
| 33 |
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
|
|
|
| 37 |
"""
|
| 38 |
|
| 39 |
id: str
|
| 40 |
analysis_title: str
|
| 41 |
-
|
| 42 |
-
|
| 43 |
user_id: str
|
| 44 |
report_id: str | None = None
|
| 45 |
created_at: datetime
|
|
@@ -66,18 +67,18 @@ def gate(intent: Intent, state: AnalysisState) -> Intent:
|
|
| 66 |
return intent
|
| 67 |
|
| 68 |
|
| 69 |
-
def stub_analysis_state(
|
| 70 |
-
"""Hardcoded Analysis State for
|
| 71 |
|
| 72 |
-
Shared fixture so the gate, the Help skill, and tests all exercise the same
|
| 73 |
-
shape
|
| 74 |
"""
|
| 75 |
now = datetime.now(UTC)
|
| 76 |
return AnalysisState(
|
| 77 |
id="stub-analysis",
|
| 78 |
analysis_title="Stub analysis",
|
| 79 |
-
|
| 80 |
-
|
| 81 |
user_id="stub-user",
|
| 82 |
report_id=None,
|
| 83 |
created_at=now,
|
|
@@ -104,8 +105,8 @@ async def get_analysis_state(analysis_id: str) -> AnalysisState:
|
|
| 104 |
analysis_id=analysis_id,
|
| 105 |
error=str(exc),
|
| 106 |
)
|
| 107 |
-
return stub_analysis_state(
|
| 108 |
if state is None:
|
| 109 |
logger.debug("analysis_state missing — default not-validated", analysis_id=analysis_id)
|
| 110 |
-
return stub_analysis_state(
|
| 111 |
return state
|
|
|
|
| 20 |
|
| 21 |
from datetime import UTC, datetime
|
| 22 |
|
| 23 |
+
from pydantic import BaseModel, Field
|
| 24 |
|
| 25 |
from src.agents.orchestration import Intent
|
| 26 |
from src.middlewares.logging import get_logger
|
|
|
|
| 29 |
|
| 30 |
|
| 31 |
class AnalysisState(BaseModel):
|
| 32 |
+
"""Per-analysis state the Help skill + report layer read every turn.
|
| 33 |
|
| 34 |
+
Field names mirror the dedorch `analyses` table so the DB read swaps in without
|
| 35 |
+
touching readers. The goal is the user-entered `objective` + `business_questions`
|
| 36 |
+
(set at onboarding by Go); the old `problem_statement`/`problem_validated` gate fields
|
| 37 |
+
were dropped (dedorch #3 / KM-652). `report_id` is null until a report exists.
|
| 38 |
"""
|
| 39 |
|
| 40 |
id: str
|
| 41 |
analysis_title: str
|
| 42 |
+
objective: str = ""
|
| 43 |
+
business_questions: list[str] = Field(default_factory=list)
|
| 44 |
user_id: str
|
| 45 |
report_id: str | None = None
|
| 46 |
created_at: datetime
|
|
|
|
| 67 |
return intent
|
| 68 |
|
| 69 |
|
| 70 |
+
def stub_analysis_state() -> AnalysisState:
|
| 71 |
+
"""Hardcoded Analysis State for never-throw fallbacks / tests.
|
| 72 |
|
| 73 |
+
Shared fixture so the gate seam, the Help skill, and tests all exercise the same
|
| 74 |
+
shape when a real row is missing or a read fails.
|
| 75 |
"""
|
| 76 |
now = datetime.now(UTC)
|
| 77 |
return AnalysisState(
|
| 78 |
id="stub-analysis",
|
| 79 |
analysis_title="Stub analysis",
|
| 80 |
+
objective="",
|
| 81 |
+
business_questions=[],
|
| 82 |
user_id="stub-user",
|
| 83 |
report_id=None,
|
| 84 |
created_at=now,
|
|
|
|
| 105 |
analysis_id=analysis_id,
|
| 106 |
error=str(exc),
|
| 107 |
)
|
| 108 |
+
return stub_analysis_state()
|
| 109 |
if state is None:
|
| 110 |
logger.debug("analysis_state missing — default not-validated", analysis_id=analysis_id)
|
| 111 |
+
return stub_analysis_state()
|
| 112 |
return state
|
src/agents/state_store.py
CHANGED
|
@@ -25,8 +25,8 @@ def _row_to_state(row: AnalysisStateRow) -> AnalysisState:
|
|
| 25 |
return AnalysisState(
|
| 26 |
id=row.id,
|
| 27 |
analysis_title=row.analysis_title,
|
| 28 |
-
|
| 29 |
-
|
| 30 |
user_id=row.user_id,
|
| 31 |
report_id=row.report_id,
|
| 32 |
created_at=row.created_at,
|
|
@@ -58,14 +58,17 @@ class AnalysisStateStore:
|
|
| 58 |
no source bindings — binding scoping fail-opens to the whole catalog.
|
| 59 |
"""
|
| 60 |
async with AsyncSessionLocal() as session:
|
|
|
|
|
|
|
|
|
|
| 61 |
stmt = (
|
| 62 |
insert(AnalysisStateRow)
|
| 63 |
.values(
|
| 64 |
id=analysis_id,
|
| 65 |
user_id=user_id,
|
| 66 |
analysis_title=analysis_title,
|
| 67 |
-
|
| 68 |
-
|
| 69 |
)
|
| 70 |
.on_conflict_do_nothing(index_elements=[AnalysisStateRow.id])
|
| 71 |
)
|
|
@@ -80,7 +83,8 @@ class AnalysisStateStore:
|
|
| 80 |
analysis_id: str,
|
| 81 |
user_id: str,
|
| 82 |
analysis_title: str = "New analysis",
|
| 83 |
-
|
|
|
|
| 84 |
) -> AnalysisState:
|
| 85 |
"""Create the state row for a new analysis (id shared with its chat room)."""
|
| 86 |
async with AsyncSessionLocal() as session:
|
|
@@ -88,8 +92,8 @@ class AnalysisStateStore:
|
|
| 88 |
id=analysis_id,
|
| 89 |
user_id=user_id,
|
| 90 |
analysis_title=analysis_title,
|
| 91 |
-
|
| 92 |
-
|
| 93 |
)
|
| 94 |
session.add(row)
|
| 95 |
await session.commit()
|
|
@@ -100,14 +104,13 @@ class AnalysisStateStore:
|
|
| 100 |
self,
|
| 101 |
analysis_id: str,
|
| 102 |
*,
|
| 103 |
-
problem_statement: str | None = None,
|
| 104 |
-
problem_validated: bool | None = None,
|
| 105 |
report_id: str | None = None,
|
| 106 |
) -> AnalysisState | None:
|
| 107 |
"""Patch the given fields (only non-None args are written). Returns the row.
|
| 108 |
|
| 109 |
-
Used by the
|
| 110 |
-
|
|
|
|
| 111 |
"""
|
| 112 |
async with AsyncSessionLocal() as session:
|
| 113 |
row = await session.get(AnalysisStateRow, analysis_id)
|
|
@@ -117,10 +120,6 @@ class AnalysisStateStore:
|
|
| 117 |
analysis_id=analysis_id,
|
| 118 |
)
|
| 119 |
return None
|
| 120 |
-
if problem_statement is not None:
|
| 121 |
-
row.problem_statement = problem_statement
|
| 122 |
-
if problem_validated is not None:
|
| 123 |
-
row.problem_validated = problem_validated
|
| 124 |
if report_id is not None:
|
| 125 |
row.report_id = report_id
|
| 126 |
await session.commit()
|
|
|
|
| 25 |
return AnalysisState(
|
| 26 |
id=row.id,
|
| 27 |
analysis_title=row.analysis_title,
|
| 28 |
+
objective=row.objective or "",
|
| 29 |
+
business_questions=list(row.business_questions or []),
|
| 30 |
user_id=row.user_id,
|
| 31 |
report_id=row.report_id,
|
| 32 |
created_at=row.created_at,
|
|
|
|
| 58 |
no source bindings — binding scoping fail-opens to the whole catalog.
|
| 59 |
"""
|
| 60 |
async with AsyncSessionLocal() as session:
|
| 61 |
+
# Bare get-or-create row. objective/business_questions are always supplied
|
| 62 |
+
# (empty) so the INSERT satisfies dedorch whether or not they are NOT NULL;
|
| 63 |
+
# the real goal is set at onboarding by Go and preserved by ON CONFLICT DO NOTHING.
|
| 64 |
stmt = (
|
| 65 |
insert(AnalysisStateRow)
|
| 66 |
.values(
|
| 67 |
id=analysis_id,
|
| 68 |
user_id=user_id,
|
| 69 |
analysis_title=analysis_title,
|
| 70 |
+
objective="",
|
| 71 |
+
business_questions=[],
|
| 72 |
)
|
| 73 |
.on_conflict_do_nothing(index_elements=[AnalysisStateRow.id])
|
| 74 |
)
|
|
|
|
| 83 |
analysis_id: str,
|
| 84 |
user_id: str,
|
| 85 |
analysis_title: str = "New analysis",
|
| 86 |
+
objective: str = "",
|
| 87 |
+
business_questions: list[str] | None = None,
|
| 88 |
) -> AnalysisState:
|
| 89 |
"""Create the state row for a new analysis (id shared with its chat room)."""
|
| 90 |
async with AsyncSessionLocal() as session:
|
|
|
|
| 92 |
id=analysis_id,
|
| 93 |
user_id=user_id,
|
| 94 |
analysis_title=analysis_title,
|
| 95 |
+
objective=objective,
|
| 96 |
+
business_questions=business_questions or [],
|
| 97 |
)
|
| 98 |
session.add(row)
|
| 99 |
await session.commit()
|
|
|
|
| 104 |
self,
|
| 105 |
analysis_id: str,
|
| 106 |
*,
|
|
|
|
|
|
|
| 107 |
report_id: str | None = None,
|
| 108 |
) -> AnalysisState | None:
|
| 109 |
"""Patch the given fields (only non-None args are written). Returns the row.
|
| 110 |
|
| 111 |
+
Used by the report flow (`report_id` write-back). The analysis goal
|
| 112 |
+
(`objective`/`business_questions`) is set at onboarding by Go, not patched here.
|
| 113 |
+
Returns None if the analysis doesn't exist.
|
| 114 |
"""
|
| 115 |
async with AsyncSessionLocal() as session:
|
| 116 |
row = await session.get(AnalysisStateRow, analysis_id)
|
|
|
|
| 120 |
analysis_id=analysis_id,
|
| 121 |
)
|
| 122 |
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
| 123 |
if report_id is not None:
|
| 124 |
row.report_id = report_id
|
| 125 |
await session.commit()
|
src/api/v1/chat.py
CHANGED
|
@@ -14,7 +14,7 @@ from sse_starlette.sse import EventSourceResponse
|
|
| 14 |
from src.agents.chat_handler import ChatHandler
|
| 15 |
from src.config.settings import settings
|
| 16 |
from src.db.postgres.connection import get_db
|
| 17 |
-
from src.db.postgres.models import
|
| 18 |
from src.db.redis.connection import get_redis
|
| 19 |
from src.middlewares.logging import get_logger, log_execution
|
| 20 |
|
|
@@ -100,12 +100,16 @@ async def cache_response(redis, cache_key: str, response: str, sources: list):
|
|
| 100 |
)
|
| 101 |
|
| 102 |
|
| 103 |
-
async def load_history(db: AsyncSession,
|
| 104 |
-
"""Load recent
|
|
|
|
|
|
|
|
|
|
|
|
|
| 105 |
result = await db.execute(
|
| 106 |
-
select(
|
| 107 |
-
.where(
|
| 108 |
-
.order_by(
|
| 109 |
.limit(limit)
|
| 110 |
)
|
| 111 |
rows = result.scalars().all()
|
|
@@ -117,24 +121,25 @@ async def load_history(db: AsyncSession, room_id: str, limit: int = 10) -> list:
|
|
| 117 |
|
| 118 |
async def save_messages(
|
| 119 |
db: AsyncSession,
|
| 120 |
-
|
|
|
|
| 121 |
user_content: str,
|
| 122 |
assistant_content: str,
|
| 123 |
-
sources: Optional[List[Dict[str, Any]]] = None,
|
| 124 |
):
|
| 125 |
-
"""Persist user
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
|
|
|
| 138 |
await db.commit()
|
| 139 |
|
| 140 |
|
|
@@ -187,7 +192,7 @@ async def chat_stream(request: ChatRequest, db: AsyncSession = Depends(get_db)):
|
|
| 187 |
logger.info("Returning cached response")
|
| 188 |
cached_text = cached["response"]
|
| 189 |
cached_sources = cached["sources"]
|
| 190 |
-
await save_messages(db, request.room_id, request.
|
| 191 |
|
| 192 |
async def stream_cached():
|
| 193 |
yield {"event": "sources", "data": json.dumps(cached_sources)}
|
|
@@ -202,7 +207,7 @@ async def chat_stream(request: ChatRequest, db: AsyncSession = Depends(get_db)):
|
|
| 202 |
direct = _fast_intent(request.message)
|
| 203 |
if direct:
|
| 204 |
await cache_response(redis, cache_key, direct, sources=[])
|
| 205 |
-
await save_messages(db, request.room_id, request.
|
| 206 |
|
| 207 |
async def stream_direct():
|
| 208 |
yield {"event": "sources", "data": json.dumps([])}
|
|
@@ -244,7 +249,7 @@ async def chat_stream(request: ChatRequest, db: AsyncSession = Depends(get_db)):
|
|
| 244 |
await cache_response(redis, cache_key, full_response, sources=sources)
|
| 245 |
logger.info("saving messages", sources_count=len(sources), sources=sources)
|
| 246 |
try:
|
| 247 |
-
await save_messages(db, request.room_id, request.
|
| 248 |
except Exception as e:
|
| 249 |
logger.error("save_messages failed", room_id=request.room_id, error=str(e))
|
| 250 |
yield event
|
|
|
|
| 14 |
from src.agents.chat_handler import ChatHandler
|
| 15 |
from src.config.settings import settings
|
| 16 |
from src.db.postgres.connection import get_db
|
| 17 |
+
from src.db.postgres.models import AnalysesMessageRow
|
| 18 |
from src.db.redis.connection import get_redis
|
| 19 |
from src.middlewares.logging import get_logger, log_execution
|
| 20 |
|
|
|
|
| 100 |
)
|
| 101 |
|
| 102 |
|
| 103 |
+
async def load_history(db: AsyncSession, analysis_id: str, limit: int = 10) -> list:
|
| 104 |
+
"""Load recent conversation messages for an analysis as LangChain messages (oldest-first).
|
| 105 |
+
|
| 106 |
+
Reads the dedorch `analyses_messages` table (`role ∈ user|ai`), which replaced the
|
| 107 |
+
deprecated `rooms`/`chat_messages`.
|
| 108 |
+
"""
|
| 109 |
result = await db.execute(
|
| 110 |
+
select(AnalysesMessageRow)
|
| 111 |
+
.where(AnalysesMessageRow.analysis_id == analysis_id)
|
| 112 |
+
.order_by(AnalysesMessageRow.created_at.asc())
|
| 113 |
.limit(limit)
|
| 114 |
)
|
| 115 |
rows = result.scalars().all()
|
|
|
|
| 121 |
|
| 122 |
async def save_messages(
|
| 123 |
db: AsyncSession,
|
| 124 |
+
analysis_id: str,
|
| 125 |
+
user_id: str,
|
| 126 |
user_content: str,
|
| 127 |
assistant_content: str,
|
|
|
|
| 128 |
):
|
| 129 |
+
"""Persist the user turn + AI answer to dedorch `analyses_messages` (`role` user|ai).
|
| 130 |
+
|
| 131 |
+
Python writes this Go-owned table as a consumer (it does not create it). RAG source
|
| 132 |
+
citations are streamed to the client but not persisted — the old `message_sources`
|
| 133 |
+
table is deprecated along with `chat_messages`.
|
| 134 |
+
"""
|
| 135 |
+
db.add(AnalysesMessageRow(
|
| 136 |
+
id=str(uuid.uuid4()), analysis_id=analysis_id, user_id=user_id,
|
| 137 |
+
role="user", content=user_content,
|
| 138 |
+
))
|
| 139 |
+
db.add(AnalysesMessageRow(
|
| 140 |
+
id=str(uuid.uuid4()), analysis_id=analysis_id, user_id=user_id,
|
| 141 |
+
role="ai", content=assistant_content,
|
| 142 |
+
))
|
| 143 |
await db.commit()
|
| 144 |
|
| 145 |
|
|
|
|
| 192 |
logger.info("Returning cached response")
|
| 193 |
cached_text = cached["response"]
|
| 194 |
cached_sources = cached["sources"]
|
| 195 |
+
await save_messages(db, request.room_id, request.user_id, request.message, cached_text)
|
| 196 |
|
| 197 |
async def stream_cached():
|
| 198 |
yield {"event": "sources", "data": json.dumps(cached_sources)}
|
|
|
|
| 207 |
direct = _fast_intent(request.message)
|
| 208 |
if direct:
|
| 209 |
await cache_response(redis, cache_key, direct, sources=[])
|
| 210 |
+
await save_messages(db, request.room_id, request.user_id, request.message, direct)
|
| 211 |
|
| 212 |
async def stream_direct():
|
| 213 |
yield {"event": "sources", "data": json.dumps([])}
|
|
|
|
| 249 |
await cache_response(redis, cache_key, full_response, sources=sources)
|
| 250 |
logger.info("saving messages", sources_count=len(sources), sources=sources)
|
| 251 |
try:
|
| 252 |
+
await save_messages(db, request.room_id, request.user_id, request.message, full_response)
|
| 253 |
except Exception as e:
|
| 254 |
logger.error("save_messages failed", room_id=request.room_id, error=str(e))
|
| 255 |
yield event
|
src/api/v1/report.py
CHANGED
|
@@ -128,7 +128,7 @@ async def generate_report(
|
|
| 128 |
|
| 129 |
state = await _load_state(analysis_id)
|
| 130 |
floor_missing, _ = await report_floor(
|
| 131 |
-
analysis_id, state or stub_analysis_state(
|
| 132 |
)
|
| 133 |
if floor_missing:
|
| 134 |
raise HTTPException(
|
|
|
|
| 128 |
|
| 129 |
state = await _load_state(analysis_id)
|
| 130 |
floor_missing, _ = await report_floor(
|
| 131 |
+
analysis_id, state or stub_analysis_state()
|
| 132 |
)
|
| 133 |
if floor_missing:
|
| 134 |
raise HTTPException(
|
src/api/v2/chat.py
CHANGED
|
@@ -88,7 +88,7 @@ async def chat_stream(request: ChatRequest, db: AsyncSession = Depends(get_db)):
|
|
| 88 |
logger.info("Returning cached response")
|
| 89 |
cached_text = cached["response"]
|
| 90 |
cached_sources = cached["sources"]
|
| 91 |
-
await save_messages(db, analysis_id, request.
|
| 92 |
|
| 93 |
async def stream_cached():
|
| 94 |
yield {"event": "sources", "data": json.dumps(cached_sources)}
|
|
@@ -103,7 +103,7 @@ async def chat_stream(request: ChatRequest, db: AsyncSession = Depends(get_db)):
|
|
| 103 |
direct = _fast_intent(request.message)
|
| 104 |
if direct:
|
| 105 |
await cache_response(redis, cache_key, direct, sources=[])
|
| 106 |
-
await save_messages(db, analysis_id, request.
|
| 107 |
|
| 108 |
async def stream_direct():
|
| 109 |
yield {"event": "sources", "data": json.dumps([])}
|
|
@@ -144,7 +144,7 @@ async def chat_stream(request: ChatRequest, db: AsyncSession = Depends(get_db)):
|
|
| 144 |
await cache_response(redis, cache_key, full_response, sources=sources)
|
| 145 |
try:
|
| 146 |
await save_messages(
|
| 147 |
-
db, analysis_id, request.
|
| 148 |
)
|
| 149 |
except Exception as e:
|
| 150 |
logger.error("save_messages failed", analysis_id=analysis_id, error=str(e))
|
|
|
|
| 88 |
logger.info("Returning cached response")
|
| 89 |
cached_text = cached["response"]
|
| 90 |
cached_sources = cached["sources"]
|
| 91 |
+
await save_messages(db, analysis_id, request.user_id, request.message, cached_text)
|
| 92 |
|
| 93 |
async def stream_cached():
|
| 94 |
yield {"event": "sources", "data": json.dumps(cached_sources)}
|
|
|
|
| 103 |
direct = _fast_intent(request.message)
|
| 104 |
if direct:
|
| 105 |
await cache_response(redis, cache_key, direct, sources=[])
|
| 106 |
+
await save_messages(db, analysis_id, request.user_id, request.message, direct)
|
| 107 |
|
| 108 |
async def stream_direct():
|
| 109 |
yield {"event": "sources", "data": json.dumps([])}
|
|
|
|
| 144 |
await cache_response(redis, cache_key, full_response, sources=sources)
|
| 145 |
try:
|
| 146 |
await save_messages(
|
| 147 |
+
db, analysis_id, request.user_id, request.message, full_response
|
| 148 |
)
|
| 149 |
except Exception as e:
|
| 150 |
logger.error("save_messages failed", analysis_id=analysis_id, error=str(e))
|
src/db/postgres/models.py
CHANGED
|
@@ -3,7 +3,6 @@
|
|
| 3 |
from uuid import uuid4
|
| 4 |
|
| 5 |
from sqlalchemy import (
|
| 6 |
-
Boolean,
|
| 7 |
Column,
|
| 8 |
DateTime,
|
| 9 |
ForeignKey,
|
|
@@ -186,27 +185,23 @@ class AnalysisStateRow(Base):
|
|
| 186 |
One session = one analysis = one conversation; `id` is the shared session id
|
| 187 |
(canonical UUID). Verified against the dedorch DB 2026-06-25.
|
| 188 |
|
| 189 |
-
dedorch `analyses`
|
| 190 |
-
`
|
| 191 |
-
`
|
| 192 |
-
`
|
| 193 |
|
| 194 |
-
|
| 195 |
-
`
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
`AnalysisState` pydantic contract (no Python reader needs them yet).
|
| 200 |
-
|
| 201 |
-
`analysis` (singular) is the deprecated DUPLICATE table Harry will drop — never use it.
|
| 202 |
-
Class name kept as `AnalysisStateRow`.
|
| 203 |
"""
|
| 204 |
__tablename__ = "analyses"
|
| 205 |
|
| 206 |
id = Column(UUID(as_uuid=False), primary_key=True) # shared session id (uuid)
|
| 207 |
analysis_title = Column(String, nullable=False, default="New analysis")
|
| 208 |
-
|
| 209 |
-
|
| 210 |
user_id = Column(String, nullable=False, index=True) # was owner_id (dedorch uses user_id)
|
| 211 |
report_id = Column(UUID(as_uuid=False), nullable=True)
|
| 212 |
# dedorch `analyses` columns (FE/Go concerns; carried so create_all matches dedorch).
|
|
@@ -240,3 +235,23 @@ class AnalysisDataSourceRow(Base):
|
|
| 240 |
bound_at = Column(DateTime(timezone=True), nullable=False, server_default=func.now())
|
| 241 |
source_metadata = Column("metadata", JSONB, nullable=True)
|
| 242 |
created_at = Column(DateTime(timezone=True), nullable=False, server_default=func.now())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
from uuid import uuid4
|
| 4 |
|
| 5 |
from sqlalchemy import (
|
|
|
|
| 6 |
Column,
|
| 7 |
DateTime,
|
| 8 |
ForeignKey,
|
|
|
|
| 185 |
One session = one analysis = one conversation; `id` is the shared session id
|
| 186 |
(canonical UUID). Verified against the dedorch DB 2026-06-25.
|
| 187 |
|
| 188 |
+
dedorch `analyses` columns (reconciled 2026-07-01 — Harry's #3 landed): `id` (uuid),
|
| 189 |
+
`analysis_title`, `objective` (text), `business_questions` (jsonb), `user_id` (text),
|
| 190 |
+
`report_id` (uuid), `status` (text 'active'|'inactive' — soft-delete), `data_bind` (jsonb),
|
| 191 |
+
`data_bind_version` (int), `report_collection` (jsonb), `created_at`, `updated_at`.
|
| 192 |
|
| 193 |
+
`problem_statement`/`problem_validated` were DROPPED in dedorch (#3) and removed here;
|
| 194 |
+
`objective` + `business_questions` (the user-entered goal, set at onboarding by Go) replace
|
| 195 |
+
them. The FE/Go columns (`status`/`data_bind*`/`report_collection`) are carried to match
|
| 196 |
+
dedorch but are NOT surfaced in the `AnalysisState` pydantic contract. `analysis` (singular)
|
| 197 |
+
is the deprecated DUPLICATE table Harry will drop — never use it. Class name kept.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 198 |
"""
|
| 199 |
__tablename__ = "analyses"
|
| 200 |
|
| 201 |
id = Column(UUID(as_uuid=False), primary_key=True) # shared session id (uuid)
|
| 202 |
analysis_title = Column(String, nullable=False, default="New analysis")
|
| 203 |
+
objective = Column(Text, nullable=False, default="")
|
| 204 |
+
business_questions = Column(JSONB, nullable=False, default=list)
|
| 205 |
user_id = Column(String, nullable=False, index=True) # was owner_id (dedorch uses user_id)
|
| 206 |
report_id = Column(UUID(as_uuid=False), nullable=True)
|
| 207 |
# dedorch `analyses` columns (FE/Go concerns; carried so create_all matches dedorch).
|
|
|
|
| 235 |
bound_at = Column(DateTime(timezone=True), nullable=False, server_default=func.now())
|
| 236 |
source_metadata = Column("metadata", JSONB, nullable=True)
|
| 237 |
created_at = Column(DateTime(timezone=True), nullable=False, server_default=func.now())
|
| 238 |
+
|
| 239 |
+
|
| 240 |
+
class AnalysesMessageRow(Base):
|
| 241 |
+
"""One conversation message — dedorch `analyses_messages` (Go-owned table).
|
| 242 |
+
|
| 243 |
+
The analysis chat room (user question + AI answer), replacing the deprecated
|
| 244 |
+
`rooms`/`chat_messages`. Python is a **consumer/writer** here: it INSERTs and reads
|
| 245 |
+
rows but does NOT own the table (Go's migration creates it). Shape mirrors the Go
|
| 246 |
+
contract (`API_CONTRACT_BE_GOLANG.md` §Analysis Messages): `role ∈ user|ai`. RAG source
|
| 247 |
+
citations are NOT persisted here — the old `message_sources` table is deprecated along
|
| 248 |
+
with `chat_messages`.
|
| 249 |
+
"""
|
| 250 |
+
__tablename__ = "analyses_messages"
|
| 251 |
+
|
| 252 |
+
id = Column(UUID(as_uuid=False), primary_key=True)
|
| 253 |
+
analysis_id = Column(UUID(as_uuid=False), nullable=False, index=True)
|
| 254 |
+
user_id = Column(String, nullable=False, index=True)
|
| 255 |
+
role = Column(String, nullable=False) # user | ai
|
| 256 |
+
content = Column(Text, nullable=False)
|
| 257 |
+
created_at = Column(DateTime(timezone=True), nullable=False, server_default=func.now())
|