Add web search knowledge base, message tagging, and word-memory tip topics
Introduces websearch.py (Tavily) + knowledge.py to ground chat responses in search results, tagging.py to tag user and assistant messages via LLM and pick related topics for the Discord useless-tips job, and word_memory.py to track recent conversation nouns (surfaced in the new WordPool.vue sidebar) used to pick tip topics close to a related tag.
This commit is contained in:
+45
-3
@@ -1,3 +1,4 @@
|
||||
import asyncio
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
@@ -9,7 +10,11 @@ from app.db.database import get_db
|
||||
from app.db.models import Session, Message
|
||||
from app.core import memory as mem
|
||||
from app.core import llm
|
||||
from app.core import knowledge
|
||||
from app.core.websearch import WEB_SEARCH_TOOL
|
||||
from app.core.config import DEFAULT_MODEL
|
||||
from app.core.tagging import tag_message_by_id
|
||||
from app.core.word_memory import record_message
|
||||
|
||||
router = APIRouter(prefix="/chat", tags=["chat"])
|
||||
|
||||
@@ -35,6 +40,7 @@ class MessageResponse(BaseModel):
|
||||
role: str
|
||||
content: str
|
||||
created_at: datetime
|
||||
tags: list[str] = []
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
@@ -50,7 +56,16 @@ def get_messages(session_id: str, db: DBSession = Depends(get_db)):
|
||||
session = db.get(Session, session_id)
|
||||
if not session:
|
||||
raise HTTPException(status_code=404, detail="Session not found")
|
||||
return session.messages
|
||||
return [
|
||||
MessageResponse(
|
||||
id=m.id,
|
||||
role=m.role,
|
||||
content=m.content,
|
||||
created_at=m.created_at,
|
||||
tags=[t.tag for t in m.tags],
|
||||
)
|
||||
for m in session.messages
|
||||
]
|
||||
|
||||
|
||||
@router.delete("/sessions/{session_id}", status_code=204)
|
||||
@@ -82,6 +97,10 @@ async def stream_chat(req: ChatRequest, db: DBSession = Depends(get_db)):
|
||||
user_msg = Message(session_id=session.id, role="user", content=req.message)
|
||||
db.add(user_msg)
|
||||
db.commit()
|
||||
db.refresh(user_msg)
|
||||
|
||||
record_message(req.message)
|
||||
asyncio.create_task(tag_message_by_id(user_msg.id))
|
||||
|
||||
context = mem.get_context_messages(db, session.id)
|
||||
|
||||
@@ -94,17 +113,40 @@ async def stream_chat(req: ChatRequest, db: DBSession = Depends(get_db)):
|
||||
# セッションIDをまず返す(新規セッション時にフロントへ通知)
|
||||
yield sse({"type": "session_id", "value": session.id})
|
||||
|
||||
used_model = req.model or session.model
|
||||
full_response = []
|
||||
try:
|
||||
async for chunk in llm.stream_chat(context, model=req.model or session.model, session_id=session.id):
|
||||
resolved_context, searches = await llm.resolve_web_search(
|
||||
context, model=used_model, session_id=session.id,
|
||||
)
|
||||
if searches:
|
||||
sources = [
|
||||
{"title": r["title"], "url": r["url"]}
|
||||
for s in searches for r in s["results"]
|
||||
]
|
||||
yield sse({"type": "sources", "value": sources})
|
||||
for s in searches:
|
||||
summary = "\n".join(r["content"][:300] for r in s["results"] if r["content"])
|
||||
if summary:
|
||||
knowledge.save_knowledge(db, s["query"], summary, s["results"])
|
||||
|
||||
search_tools = [WEB_SEARCH_TOOL] if len(resolved_context) > len(context) else None
|
||||
async for chunk in llm.stream_chat(
|
||||
resolved_context, model=used_model, session_id=session.id, tools=search_tools,
|
||||
):
|
||||
full_response.append(chunk)
|
||||
yield sse({"type": "chunk", "value": chunk})
|
||||
|
||||
# アシスタントメッセージを保存
|
||||
assistant_content = "".join(full_response)
|
||||
db.add(Message(session_id=session.id, role="assistant", content=assistant_content))
|
||||
assistant_msg = Message(session_id=session.id, role="assistant", content=assistant_content)
|
||||
db.add(assistant_msg)
|
||||
session.updated_at = datetime.now(timezone.utc)
|
||||
db.commit()
|
||||
db.refresh(assistant_msg)
|
||||
|
||||
record_message(assistant_content)
|
||||
asyncio.create_task(tag_message_by_id(assistant_msg.id))
|
||||
|
||||
# メモリ要約チェック
|
||||
await mem.maybe_summarize(db, session)
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
from fastapi import APIRouter
|
||||
from fastapi import APIRouter, Depends
|
||||
import psutil
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy.orm import Session as DBSession
|
||||
|
||||
from app.core.scheduler import send_useless_tip, send_weather_laundry_notification
|
||||
from app.core.tagging import backfill_tags
|
||||
from app.core.weather_cache import get_latest as get_latest_weather
|
||||
from app.core.word_memory import get_word_pool_with_counts
|
||||
from app.db.database import get_db
|
||||
|
||||
router = APIRouter(prefix="/api/system", tags=["system"])
|
||||
|
||||
@@ -68,3 +72,16 @@ async def test_useless_tip():
|
||||
async def get_latest_weather_data():
|
||||
"""直近で取得した明日の天気データを返す(未取得ならnull)"""
|
||||
return get_latest_weather()
|
||||
|
||||
|
||||
@router.get("/word-pool")
|
||||
async def get_word_pool():
|
||||
"""どうでもいいtips生成に使われているワードプールを出現回数順で返す"""
|
||||
return get_word_pool_with_counts()
|
||||
|
||||
|
||||
@router.post("/tags/backfill")
|
||||
async def run_tags_backfill(limit: int | None = None, db: DBSession = Depends(get_db)):
|
||||
"""過去のユーザーメッセージのうち、タグが未生成のものにまとめてタグを付ける"""
|
||||
count = await backfill_tags(db, limit=limit)
|
||||
return {"tagged_count": count}
|
||||
|
||||
Reference in New Issue
Block a user