Add shared notes with tabbed pages editable by user and AI
Adds a note_pages table backing a tabbed notes panel where the user and the model can both read and write. Only page titles (plus a short preview) are injected into the system prompt on every turn; full page content is kept out of context and fetched on demand via a read_note tool, with write_note letting the model append to, replace, or create a page. Generalizes llm.resolve_web_search into resolve_tool_calls so the tool-calling loop handles web_search and the new note tools through one path. The chat stream emits a notes_updated SSE event when the model edits a note so the frontend panel can refresh; local edits are tracked as dirty and never clobbered by that refresh. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+32
-16
@@ -2,13 +2,17 @@ import json
|
||||
import logging
|
||||
import time
|
||||
import traceback
|
||||
from typing import AsyncIterator, AsyncGenerator
|
||||
from typing import TYPE_CHECKING, AsyncIterator, AsyncGenerator
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
import litellm
|
||||
from app.core.config import DEFAULT_MODEL, DEBUG
|
||||
from app.core.notes import NOTE_TOOLS, execute_note_tool
|
||||
from app.core.search.websearch import WEB_SEARCH_TOOL, search_web
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from sqlalchemy.orm import Session as DBSession
|
||||
|
||||
litellm.drop_params = True
|
||||
if DEBUG:
|
||||
litellm._turn_on_debug()
|
||||
@@ -103,32 +107,38 @@ async def embed(texts: list[str], model: str) -> list[list[float]]:
|
||||
return [item["embedding"] for item in response.data]
|
||||
|
||||
|
||||
async def resolve_web_search(
|
||||
async def resolve_tool_calls(
|
||||
messages: list[dict],
|
||||
model: str | None = None,
|
||||
session_id: str | None = None,
|
||||
db: "DBSession | None" = None,
|
||||
max_rounds: int = 2,
|
||||
) -> tuple[list[dict], list[dict]]:
|
||||
"""必要に応じてLLMにweb_searchツールを呼ばせ、検索結果を会話に組み込んだ上でメッセージ列を返す。
|
||||
) -> tuple[list[dict], list[dict], bool]:
|
||||
"""必要に応じてLLMにツールを呼ばせ、その結果を会話に組み込んだ上でメッセージ列を返す。
|
||||
|
||||
モデルがtool callingに対応していない場合は何もせず元のmessagesをそのまま返す。
|
||||
戻り値: (拡張後のmessages, 実際に検索したクエリと結果のリスト)
|
||||
扱うツールは web_search と共有ノート(read_note / write_note)。ノート系は db が
|
||||
渡されたときだけ提示する。モデルがtool callingに対応していない場合は何もせず
|
||||
元のmessagesをそのまま返す。
|
||||
戻り値: (拡張後のmessages, 実際に検索したクエリと結果のリスト, ノートを変更したか)
|
||||
"""
|
||||
used_model = model or DEFAULT_MODEL
|
||||
working_messages = list(messages)
|
||||
searches: list[dict] = []
|
||||
notes_changed = False
|
||||
|
||||
tools = [WEB_SEARCH_TOOL] + (NOTE_TOOLS if db is not None else [])
|
||||
|
||||
for _ in range(max_rounds):
|
||||
try:
|
||||
response = await litellm.acompletion(
|
||||
model=used_model,
|
||||
messages=working_messages,
|
||||
tools=[WEB_SEARCH_TOOL],
|
||||
tools=tools,
|
||||
tool_choice="auto",
|
||||
)
|
||||
except Exception:
|
||||
logger.info("Model %s does not support tool calling; skipping web search", used_model)
|
||||
return messages, searches
|
||||
logger.info("Model %s does not support tool calling; skipping tools", used_model)
|
||||
return messages, searches, notes_changed
|
||||
|
||||
choice_message = response.choices[0].message
|
||||
tool_calls = getattr(choice_message, "tool_calls", None)
|
||||
@@ -153,13 +163,19 @@ async def resolve_web_search(
|
||||
args = json.loads(tc.function.arguments or "{}")
|
||||
except json.JSONDecodeError:
|
||||
args = {}
|
||||
query = args.get("query", "")
|
||||
results = await search_web(query)
|
||||
if results:
|
||||
tool_text = "\n\n".join(f"[{r['title']}]({r['url']})\n{r['content']}" for r in results)
|
||||
searches.append({"query": query, "results": results})
|
||||
|
||||
name = tc.function.name
|
||||
if name in ("read_note", "write_note") and db is not None:
|
||||
tool_text, changed = execute_note_tool(db, name, args)
|
||||
notes_changed = notes_changed or changed
|
||||
else:
|
||||
tool_text = "検索結果が見つかりませんでした。"
|
||||
query = args.get("query", "")
|
||||
results = await search_web(query)
|
||||
if results:
|
||||
tool_text = "\n\n".join(f"[{r['title']}]({r['url']})\n{r['content']}" for r in results)
|
||||
searches.append({"query": query, "results": results})
|
||||
else:
|
||||
tool_text = "検索結果が見つかりませんでした。"
|
||||
|
||||
working_messages.append({
|
||||
"role": "tool",
|
||||
@@ -167,4 +183,4 @@ async def resolve_web_search(
|
||||
"content": tool_text,
|
||||
})
|
||||
|
||||
return working_messages, searches
|
||||
return working_messages, searches, notes_changed
|
||||
|
||||
Reference in New Issue
Block a user