OffGridSchedula / server /tools.py
ParetoOptimal's picture
Initial Commit
0366d65
Raw
History Blame Contribute Delete
3.13 kB
"""Hermes tool-calling: let the model write its own long-term memory.
Hermes is a tool-calling fine-tune. When `HERMES_TOOLS=1`, the remote inference
path (server/model.py) advertises these tools so the model can call `remember`
mid-run to save durable facts ("Dana is the soccer coach", "you decline Mondays")
— the active half of "grows with you". Kept separate + small so the round-trip
logic is unit-testable without a live server.
"""
from __future__ import annotations
import json
from . import memory
# OpenAI-compatible tool specs (llama-server understands these with --jinja).
TOOL_SPECS = [
{
"type": "function",
"function": {
"name": "remember",
"description": (
"Save a durable fact or preference about the user to long-term memory "
"so future scheduling is more personal. Use for stable facts only "
"(roles, recurring preferences, default locations), not one-off details."
),
"parameters": {
"type": "object",
"properties": {
"text": {
"type": "string",
"description": "the fact, e.g. 'Dana is the soccer coach'",
},
"kind": {
"type": "string",
"enum": ["contact", "preference", "location", "note"],
},
},
"required": ["text"],
},
},
}
]
def dispatch(name: str, arguments) -> str:
"""Execute one tool call; returns a short result string for the tool message."""
if name != "remember":
return f"unknown tool: {name}"
try:
args = json.loads(arguments) if isinstance(arguments, str) else (arguments or {})
except (ValueError, TypeError):
args = {}
text = (args.get("text") or "").strip()
if not text:
return "no text provided"
memory.remember(text, args.get("kind", "note"))
return f"remembered: {text}"
def run_with_tools(messages: list[dict], post_fn, max_rounds: int = 3):
"""Drive a tool-calling loop. ``post_fn(messages) -> openai_response_dict`` does
the actual HTTP POST (tools already configured by the caller); injectable so the
loop is testable. Returns (final_content, last_response)."""
msgs = list(messages)
resp = {}
for _ in range(max_rounds):
resp = post_fn(msgs)
msg = resp["choices"][0]["message"]
tool_calls = msg.get("tool_calls") or []
if not tool_calls:
return msg.get("content", ""), resp
msgs.append(msg) # assistant turn carrying the tool_calls
for tc in tool_calls:
fn = tc.get("function", {})
result = dispatch(fn.get("name", ""), fn.get("arguments", "{}"))
msgs.append(
{"role": "tool", "tool_call_id": tc.get("id", ""), "content": result}
)
# ran out of rounds — one final call to get content
resp = post_fn(msgs)
return resp["choices"][0]["message"].get("content", ""), resp