feat: unify scoring and chat evidence pages
This commit is contained in:
@@ -1,8 +1,6 @@
|
||||
"""Persistent teacher conversations and SSE agent orchestration."""
|
||||
import json
|
||||
import re
|
||||
import time
|
||||
from collections import defaultdict
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from datetime import timedelta
|
||||
from threading import Lock
|
||||
@@ -20,7 +18,7 @@ from .agent_tools import TOOLS, execute, tool_specs
|
||||
from .config import get_settings
|
||||
from .db import get_db
|
||||
from .llm import ModelError
|
||||
from .models import AgentRun, ChatMessage, Conversation, KnowledgeChunk, PendingAction, Project, ScoreEvidence, ScoringBatch, now
|
||||
from .models import AgentRun, ChatMessage, Conversation, PendingAction, Project, ScoringBatch, now
|
||||
from .settings_store import client_for_chat
|
||||
|
||||
router = APIRouter(prefix="/api/conversations")
|
||||
@@ -90,48 +88,6 @@ def _validated_scope(db, data):
|
||||
return scope
|
||||
|
||||
|
||||
def _restored_citations(db, messages):
|
||||
"""Resolve old score image links only when saved evidence identifies one live text page."""
|
||||
refs = []
|
||||
originals = {msg.id: msg.citations or [] for msg in messages}
|
||||
for msg in messages:
|
||||
for index, item in enumerate(originals[msg.id]):
|
||||
if not isinstance(item, dict) or item.get("text_url") or re.fullmatch(
|
||||
r"/api/knowledge/documents/\d+/pages/\d+/?", str(item.get("url") or "")):
|
||||
continue
|
||||
match = re.fullmatch(r"/api/assets/(\d+)/(\d+)/?", str(item.get("image_url") or item.get("url") or ""))
|
||||
if match and isinstance(item.get("quote"), str) and item["quote"]:
|
||||
refs.append((msg.id, index, item, int(match[1]), int(match[2])))
|
||||
if not refs:
|
||||
return {}
|
||||
file_ids = {file_id for _, _, _, file_id, _ in refs}
|
||||
pages = {page for _, _, _, _, page in refs}
|
||||
evidence = defaultdict(list)
|
||||
for file_id, page, quote, document_id in db.execute(select(
|
||||
ScoreEvidence.submission_file_id, ScoreEvidence.page, ScoreEvidence.quote, ScoreEvidence.document_id).where(
|
||||
ScoreEvidence.submission_file_id.in_(file_ids), ScoreEvidence.page.in_(pages),
|
||||
ScoreEvidence.document_id.is_not(None))):
|
||||
evidence[file_id, page].append((quote, document_id))
|
||||
document_ids = {document_id for rows in evidence.values() for _, document_id in rows}
|
||||
if not document_ids:
|
||||
return {}
|
||||
chunks = defaultdict(list)
|
||||
for document_id, page, content in db.execute(select(
|
||||
KnowledgeChunk.document_id, KnowledgeChunk.page, KnowledgeChunk.text).where(
|
||||
KnowledgeChunk.document_id.in_(document_ids), KnowledgeChunk.page.in_(pages))):
|
||||
chunks[document_id, page].append(content)
|
||||
restored = {}
|
||||
for message_id, index, item, file_id, page in refs:
|
||||
quote = item["quote"]
|
||||
matches = {document_id for saved_quote, document_id in evidence[file_id, page]
|
||||
if saved_quote.startswith(quote) and any(quote in content for content in chunks[document_id, page])}
|
||||
if len(matches) == 1:
|
||||
citations = restored.setdefault(message_id, [dict(value) if isinstance(value, dict) else value
|
||||
for value in originals[message_id]])
|
||||
citations[index]["text_url"] = f"/api/knowledge/documents/{matches.pop()}/pages/{page}"
|
||||
return restored
|
||||
|
||||
|
||||
def _detail(db, conversation):
|
||||
stale = db.scalars(select(AgentRun).where(AgentRun.conversation_id == conversation.id,
|
||||
AgentRun.status == "running", AgentRun.created_at < now() - timedelta(minutes=15))).all()
|
||||
@@ -143,11 +99,10 @@ def _detail(db, conversation):
|
||||
db.commit()
|
||||
runs = db.scalars(select(AgentRun).where(AgentRun.conversation_id == conversation.id).order_by(AgentRun.id.desc()).limit(20)).all()
|
||||
messages = db.scalars(select(ChatMessage).where(ChatMessage.conversation_id == conversation.id).order_by(ChatMessage.id)).all()
|
||||
restored_citations = _restored_citations(db, messages)
|
||||
actions = db.scalars(select(PendingAction).where(PendingAction.conversation_id == conversation.id).order_by(PendingAction.id)).all()
|
||||
return {**_summary(conversation), "messages": [{**{key: getattr(msg, key) for key in
|
||||
("id", "role", "content", "tool_calls", "created_at")},
|
||||
"citations": restored_citations.get(msg.id, msg.citations)} for msg in messages],
|
||||
"citations": msg.citations} for msg in messages],
|
||||
"actions": [action_dict(action) for action in actions],
|
||||
"runs": [{key: getattr(run, key) for key in ("id", "status", "error", "tool_calls", "created_at", "finished_at")} for run in runs],
|
||||
"scope_locked": any(msg.role == "user" for msg in messages)}
|
||||
@@ -265,7 +220,7 @@ def _safe_citation(item):
|
||||
if not isinstance(item, dict):
|
||||
return None
|
||||
return {key: value[:limit] if isinstance(value, str) else value for key, limit in
|
||||
(("title", 200), ("quote", 500), ("url", 500), ("image_url", 500), ("text_url", 500))
|
||||
(("title", 200), ("quote", 500), ("url", 500), ("image_url", 500), ("score_url", 500))
|
||||
if isinstance((value := item.get(key)), str)} | {
|
||||
key: item[key] for key in ("chunk_id", "document_id", "page", "project_id")
|
||||
if isinstance(item.get(key), int)}
|
||||
@@ -280,7 +235,7 @@ def _answer_citations(items):
|
||||
if not safe:
|
||||
continue
|
||||
key = (safe.get("document_id"), safe.get("chunk_id"), safe.get("title"),
|
||||
safe.get("page"), safe.get("quote"), safe.get("url"), safe.get("text_url"))
|
||||
safe.get("page"), safe.get("quote"), safe.get("url"), safe.get("score_url"))
|
||||
if key not in seen:
|
||||
seen.add(key)
|
||||
citations.append(safe)
|
||||
|
||||
Reference in New Issue
Block a user