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:
2026-06-20 18:06:06 +09:00
parent 4e837d1419
commit 234ebba103
23 changed files with 946 additions and 39 deletions
+45 -3
View File
@@ -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)
+18 -1
View File
@@ -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}