feat: add remember_fact and forget_fact chat tools
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V57jSQPqwkGG8BuAXg59X5
This commit is contained in:
parent
9c4ad034e5
commit
72d2bc379c
|
|
@ -497,6 +497,21 @@ async def send_email(to: str, subject: str, body: str) -> dict:
|
|||
return await asyncio.to_thread(_send_email_sync, to, subject, body)
|
||||
|
||||
|
||||
async def remember_fact(fact: str) -> dict:
|
||||
fact_id = await insert_memory_fact(DEFAULT_USER_ID, fact)
|
||||
return {"id": fact_id, "content": fact}
|
||||
|
||||
|
||||
async def forget_fact(query: str) -> dict:
|
||||
matches = await search_memory_facts(query)
|
||||
if not matches:
|
||||
return {"deleted": False, "matches": [], "message": "Kein passender Fakt gefunden."}
|
||||
if len(matches) > 1:
|
||||
return {"deleted": False, "matches": [{"id": m["id"], "content": m["content"]} for m in matches]}
|
||||
await delete_memory_fact(matches[0]["id"])
|
||||
return {"deleted": True, "content": matches[0]["content"]}
|
||||
|
||||
|
||||
CALENDAR_TOOLS = [
|
||||
{
|
||||
"name": "list_calendar_events",
|
||||
|
|
@ -590,6 +605,35 @@ CALENDAR_TOOLS = [
|
|||
},
|
||||
]
|
||||
|
||||
MEMORY_TOOLS = [
|
||||
{
|
||||
"name": "remember_fact",
|
||||
"description": "Store a fact the user explicitly asked to remember, so it is available in future chats.",
|
||||
"input_schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"fact": {"type": "string", "description": "The fact to remember, phrased as a standalone statement"},
|
||||
},
|
||||
"required": ["fact"],
|
||||
},
|
||||
},
|
||||
{
|
||||
"name": "forget_fact",
|
||||
"description": (
|
||||
"Search remembered facts matching a query and delete it if exactly "
|
||||
"one matches. Only call after the user has explicitly confirmed "
|
||||
"which fact to forget in the conversation."
|
||||
),
|
||||
"input_schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {"type": "string", "description": "Text to search for among remembered facts"},
|
||||
},
|
||||
"required": ["query"],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
CALENDAR_ASSISTANT_INSTRUCTIONS = (
|
||||
"Du hast Zugriff auf den Kalender 'FFW-Onza-Alle' ueber die Tools "
|
||||
"list_calendar_events, create_calendar_event, update_calendar_event und "
|
||||
|
|
@ -610,6 +654,18 @@ CALENDAR_ASSISTANT_INSTRUCTIONS = (
|
|||
"versendete Mail laesst sich nicht zurueckholen."
|
||||
)
|
||||
|
||||
MEMORY_ASSISTANT_INSTRUCTIONS = (
|
||||
"Du hast ausserdem Zugriff auf ein chatuebergreifendes Gedaechtnis ueber "
|
||||
"die Tools remember_fact und forget_fact. Wenn der Nutzer dich explizit "
|
||||
"bittet, dir etwas zu merken (z.B. 'merke dir, dass...'), rufe "
|
||||
"remember_fact direkt auf - keine Rueckfrage noetig. Wenn der Nutzer "
|
||||
"dich bittet, einen gemerkten Fakt zu vergessen, frage zuerst explizit "
|
||||
"im Klartext nach Bestaetigung, welcher Fakt gemeint ist, und rufe "
|
||||
"forget_fact erst auf, nachdem der Nutzer zugestimmt hat. Gibt "
|
||||
"forget_fact mehrere moegliche Treffer zurueck, liste sie im Chat auf "
|
||||
"und frage nach, welcher gemeint ist, statt den falschen zu loeschen."
|
||||
)
|
||||
|
||||
|
||||
async def execute_tool(name: str, tool_input: dict) -> str:
|
||||
if name == "list_calendar_events":
|
||||
|
|
@ -641,6 +697,12 @@ async def execute_tool(name: str, tool_input: dict) -> str:
|
|||
if name == "send_email":
|
||||
result = await send_email(tool_input["to"], tool_input["subject"], tool_input["body"])
|
||||
return json.dumps(result)
|
||||
if name == "remember_fact":
|
||||
result = await remember_fact(tool_input["fact"])
|
||||
return json.dumps(result)
|
||||
if name == "forget_fact":
|
||||
result = await forget_fact(tool_input["query"])
|
||||
return json.dumps(result)
|
||||
raise ValueError(f"Unknown tool: {name}")
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -82,3 +82,62 @@ async def test_search_similar_conversation_summaries_excludes_current_conversati
|
|||
args, kwargs = mock_query.call_args
|
||||
assert "conversation_id != %s" in args[0]
|
||||
assert args[1] == ("[0.1,0.2]", 9, 3)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_remember_fact_inserts_and_returns_content():
|
||||
main.DEFAULT_USER_ID = 1
|
||||
with patch.object(main, "insert_memory_fact", new=AsyncMock(return_value=99)):
|
||||
result = await main.remember_fact("Hund heisst Bruno")
|
||||
|
||||
assert result == {"id": 99, "content": "Hund heisst Bruno"}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_forget_fact_no_match_returns_message():
|
||||
with patch.object(main, "search_memory_facts", new=AsyncMock(return_value=[])):
|
||||
result = await main.forget_fact("Katze")
|
||||
|
||||
assert result["deleted"] is False
|
||||
assert result["matches"] == []
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_forget_fact_single_match_deletes():
|
||||
matches = [{"id": 5, "content": "Hund heisst Bruno"}]
|
||||
with patch.object(main, "search_memory_facts", new=AsyncMock(return_value=matches)), patch.object(
|
||||
main, "delete_memory_fact", new=AsyncMock()
|
||||
) as mock_delete:
|
||||
result = await main.forget_fact("Bruno")
|
||||
|
||||
mock_delete.assert_called_once_with(5)
|
||||
assert result == {"deleted": True, "content": "Hund heisst Bruno"}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_forget_fact_multiple_matches_does_not_delete():
|
||||
matches = [
|
||||
{"id": 5, "content": "Hund heisst Bruno"},
|
||||
{"id": 6, "content": "Bruno ist der Nachbar"},
|
||||
]
|
||||
with patch.object(main, "search_memory_facts", new=AsyncMock(return_value=matches)), patch.object(
|
||||
main, "delete_memory_fact", new=AsyncMock()
|
||||
) as mock_delete:
|
||||
result = await main.forget_fact("Bruno")
|
||||
|
||||
mock_delete.assert_not_called()
|
||||
assert result["deleted"] is False
|
||||
assert len(result["matches"]) == 2
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_tool_dispatches_remember_and_forget_fact():
|
||||
import json
|
||||
|
||||
with patch.object(main, "remember_fact", new=AsyncMock(return_value={"id": 1, "content": "X"})):
|
||||
result = await main.execute_tool("remember_fact", {"fact": "X"})
|
||||
assert json.loads(result) == {"id": 1, "content": "X"}
|
||||
|
||||
with patch.object(main, "forget_fact", new=AsyncMock(return_value={"deleted": True, "content": "X"})):
|
||||
result = await main.execute_tool("forget_fact", {"query": "X"})
|
||||
assert json.loads(result) == {"deleted": True, "content": "X"}
|
||||
|
|
|
|||
Loading…
Reference in New Issue