fix tools calls

This commit is contained in:
Roberto
2026-04-27 09:15:08 +02:00
parent cb8f56d909
commit 6787e690ba
4 changed files with 659 additions and 120 deletions

View File

@@ -16,7 +16,7 @@ from app.agents.note_agent import NOTE_TOOLS
from app.agents.project_agent import PROJECT_TOOLS
from app.agents.task_agent import TASK_TOOLS
from app.agents.timeline_agent import TIMELINE_TOOLS
from app.core.langfuse_client import extract_usage, get_langfuse, get_prompt_or_fallback, langfuse_context
from app.core.langfuse_client import compile_prompt, extract_usage, get_langfuse, get_prompt_or_fallback, langfuse_context
from app.core.llm import get_agent_llm, model_for_agent
from app.core.memory_middleware import MemoryMiddleware
from app.core.ws_context import clear_tool_result_collector, execute_on_client, set_tool_result_collector
@@ -56,36 +56,89 @@ def _language_instruction(context: dict[str, Any]) -> str:
)
def _datetime_context_injection(context: dict[str, Any]) -> str:
"""Build a system-prompt paragraph with current timestamp, user timezone, and format prefs."""
"""Build a comprehensive DATE CONTEXT block with pre-computed ms-epoch boundaries for common ranges."""
fp = context.get("format_prefs")
if not fp or not isinstance(fp, dict):
return ""
try:
from zoneinfo import ZoneInfo
from datetime import datetime as _dt, timezone as _utc
from datetime import datetime as _dt, timezone as _utc, timedelta as _td
tz_name: str = str(fp.get("timezone") or "UTC")
now_iso: str = str(fp.get("now_iso") or "")
date_fmt: str = str(fp.get("date_format") or "dd/MM/yyyy")
time_fmt: str = str(fp.get("time_format") or "24h")
tz = ZoneInfo(tz_name)
if now_iso:
now_utc = _dt.fromisoformat(now_iso.replace("Z", "+00:00"))
else:
now_utc = _dt.now(_utc.utc)
tz = ZoneInfo(tz_name)
now_ms = int(now_utc.timestamp() * 1000)
now_local = now_utc.astimezone(tz)
today_local = now_local.strftime("%Y-%m-%d")
weekday_local = now_local.strftime("%A")
now_local_str = now_local.strftime("%Y-%m-%d %H:%M")
weekday_str = now_local.strftime("%A")
y, m, d = now_local.year, now_local.month, now_local.day
def _day(year: int, month: int, day: int) -> tuple[int, int]:
s = _dt(year, month, day, tzinfo=tz)
e = s + _td(days=1)
return int(s.timestamp() * 1000), int(e.timestamp() * 1000) - 1
def _between(start: "_dt", end_excl: "_dt") -> tuple[int, int]:
return int(start.timestamp() * 1000), int(end_excl.timestamp() * 1000) - 1
today_s, today_e = _day(y, m, d)
yd = now_local - _td(days=1)
yesterday_s, yesterday_e = _day(yd.year, yd.month, yd.day)
tm = now_local + _td(days=1)
tomorrow_s, tomorrow_e = _day(tm.year, tm.month, tm.day)
# ISO week (MonSun)
monday = _dt(y, m, d, tzinfo=tz) - _td(days=now_local.weekday())
last_monday = monday - _td(weeks=1)
next_monday = monday + _td(weeks=1)
this_week_s, this_week_e = _between(monday, next_monday)
last_week_s, last_week_e = _between(last_monday, monday)
next_week_s, next_week_e = _between(next_monday, next_monday + _td(weeks=1))
# Calendar months
this_m_start = _dt(y, m, 1, tzinfo=tz)
next_m_start = _dt(y + (m // 12), m % 12 + 1, 1, tzinfo=tz)
last_m_start = _dt(y - (1 if m == 1 else 0), 12 if m == 1 else m - 1, 1, tzinfo=tz)
next2_m = next_m_start.month % 12 + 1
next2_y = next_m_start.year + (1 if next_m_start.month == 12 else 0)
next2_m_start = _dt(next2_y, next2_m, 1, tzinfo=tz)
this_month_s, this_month_e = _between(this_m_start, next_m_start)
last_month_s, last_month_e = _between(last_m_start, this_m_start)
next_month_s, next_month_e = _between(next_m_start, next2_m_start)
# Calendar years
this_yr_s, this_yr_e = _between(_dt(y, 1, 1, tzinfo=tz), _dt(y + 1, 1, 1, tzinfo=tz))
last_yr_s, last_yr_e = _between(_dt(y - 1, 1, 1, tzinfo=tz), _dt(y, 1, 1, tzinfo=tz))
sunday = monday + _td(days=6)
last_sunday = last_monday + _td(days=6)
next_sunday = next_monday + _td(days=6)
return (
f"\n\nCurrent instant: {now_utc.isoformat()}. "
f"User local date: {today_local} ({weekday_local}). "
f"Timezone: {tz_name}. "
f"Display preference: dateFormat={date_fmt}, timeFormat={time_fmt}. "
f"When calling tools with date fields, always pass integer Unix milliseconds (ms since epoch, UTC). "
f"When calling list_tasks_due_today or list_timelines_today, always pass user_timezone=\"{tz_name}\". "
f"When presenting dates to the user in chat, format using the display preference above."
f"\n\nDATE CONTEXT (timezone: {tz_name}, dateFormat: {date_fmt}, timeFormat: {time_fmt})\n"
f"now_local: {now_local_str} ({weekday_str})\n"
f"now_ms: {now_ms}\n\n"
f"today [{today_s}, {today_e}] {y:04d}-{m:02d}-{d:02d}\n"
f"tomorrow [{tomorrow_s}, {tomorrow_e}] {tm.strftime('%Y-%m-%d')}\n"
f"yesterday [{yesterday_s}, {yesterday_e}] {yd.strftime('%Y-%m-%d')}\n"
f"this_week [{this_week_s}, {this_week_e}] {monday.strftime('%Y-%m-%d')}{sunday.strftime('%Y-%m-%d')} (MonSun)\n"
f"last_week [{last_week_s}, {last_week_e}] {last_monday.strftime('%Y-%m-%d')}{last_sunday.strftime('%Y-%m-%d')}\n"
f"next_week [{next_week_s}, {next_week_e}] {next_monday.strftime('%Y-%m-%d')}{next_sunday.strftime('%Y-%m-%d')}\n"
f"this_month [{this_month_s}, {this_month_e}] {y:04d}-{m:02d}\n"
f"last_month [{last_month_s}, {last_month_e}] {last_m_start.strftime('%Y-%m')}\n"
f"next_month [{next_month_s}, {next_month_e}] {next_m_start.strftime('%Y-%m')}\n"
f"this_year [{this_yr_s}, {this_yr_e}] {y:04d}\n"
f"last_year [{last_yr_s}, {last_yr_e}] {y - 1:04d}\n\n"
f"When calling list_tasks_due_today or list_timelines_today, always pass user_timezone=\"{tz_name}\".\n"
f"When presenting dates, format using dateFormat={date_fmt} and timeFormat={time_fmt}."
)
except Exception:
return ""
@@ -123,27 +176,75 @@ def _relational_memory_injection(context: dict[str, Any]) -> str:
return section
_HOME_SYSTEM_PROMPT = (
"You are the home assistant with direct access to all tools: tasks, projects, notes, timelines, and memory tools. "
"Always use tools for factual data retrieval before answering. "
"When the user asks to remember, forget, or update what you know about them, use memory tools. "
"If context.context.resolved_project_id exists, use it as project_id for scoped list calls. "
"Return markdown and use tags when relevant: <project>[ids]</project>, <task>[ids]</task>, "
"<note>[ids]</note>, <timeline>[ids]</timeline>, <chart>{json}</chart>. "
"When listing tasks or timelines, each id tag must be on its own line with no prefix/suffix text. "
"Never put titles, priorities, or dates on the same line as <task> or <timeline> tags. "
"For questions about upcoming timelines (e.g. 'prossimi eventi'), include only future items in the current month unless the user asks a different range. "
"For upcoming tasks, after tag lines add a short recommendation based on due date and priority."
)
def _request_context_block(context: dict[str, Any]) -> str:
"""Return a small block with per-request scope and resolved project context."""
parts: list[str] = []
scope = context.get("scope")
if scope and isinstance(scope, dict):
parts.append(f"scope: {json.dumps(scope, ensure_ascii=True)}")
resolved = context.get("resolved_project_id")
if resolved and isinstance(resolved, str):
parts.append(f"resolved_project_id: {resolved}")
return "\n".join(parts)
_FLOATING_SYSTEM_PROMPT = (
"You are the floating assistant with direct access to all tools: tasks, projects, notes, timelines, and memory tools. "
"Stay focused on the floating scope in context.scope and answer concisely. "
"Return plain text only. Do not output XML/HTML-like tags such as <task>, <project>, <note>, <timeline>, or any bracketed id tag wrappers. "
"Always use tools for factual data retrieval before answering. "
"When the user asks to remember, forget, or update what you know about them, use memory tools. "
"If context.context.resolved_project_id exists, use it as project_id for scoped list calls. "
)
_HOME_SYSTEM_PROMPT = """\
You are the home assistant for adiuvAI with direct access to all tools: tasks, projects, notes, timelines, and memory tools.
Always use tools for factual data retrieval before answering.
When the user asks to remember, forget, or update what you know about them, use memory tools.
# Output format
Return markdown and use tags when relevant: <project>[ids]</project>, <task>[ids]</task>, <note>[ids]</note>, <timeline>[ids]</timeline>, <chart>{{json}}</chart>.
When listing tasks or timelines, each id tag must be on its own line with no prefix/suffix text.
Never put titles, priorities, or dates on the same line as <task> or <timeline> tags.
For questions about upcoming timelines (e.g. 'prossimi eventi'), include only future items in the current month unless the user asks a different range.
For upcoming tasks, after tag lines add a short recommendation based on due date and priority.
# Date filtering
{date_context}
When filtering tasks/timelines/notes by date, take dueDateFrom / dueDateTo (ms epoch UTC) verbatim from the DATE CONTEXT boundary table above. Do NOT compute boundaries from now_ms yourself.
For specific dates not listed, compute local-midnight in the user timezone and convert to UTC ms.
For "today" / "tomorrow" queries, prefer list_tasks_due_today / list_timelines_today with user_timezone from DATE CONTEXT.
# Language
{language_instruction}
# Known people & projects
{relational_memory}
# Behavioral hints
{proactive_hints}
# Request context
{request_context}\
"""
_FLOATING_SYSTEM_PROMPT = """\
You are the floating assistant for adiuvAI with direct access to all tools: tasks, projects, notes, timelines, and memory tools.
Stay focused on the floating scope and answer concisely.
Return plain text only. Do not output XML/HTML-like tags such as <task>, <project>, <note>, <timeline>, or any bracketed id tag wrappers.
Always use tools for factual data retrieval before answering.
When the user asks to remember, forget, or update what you know about them, use memory tools.
# Date filtering
{date_context}
When filtering by date, take dueDateFrom / dueDateTo (ms epoch UTC) verbatim from the DATE CONTEXT boundary table above. Do NOT compute boundaries from now_ms yourself.
For specific dates not listed, compute local-midnight in the user timezone and convert to UTC ms.
# Language
{language_instruction}
# Known people & projects
{relational_memory}
# Behavioral hints
{proactive_hints}
# Request context
{request_context}\
"""
_FLOATING_DOMAIN_CLASSIFIER_PROMPT = (
"You are a strict domain classifier for websocket floating requests. "
@@ -253,10 +354,18 @@ def _session_id_from_context(context: dict[str, Any]) -> str | None:
return None
def _context_for_model(context: dict[str, Any]) -> dict[str, Any]:
sanitized = dict(context)
sanitized.pop("_debug", None)
return sanitized
def _build_system_prompt(name: str, fallback: str, context: dict[str, Any]) -> tuple[str, Any]:
"""Fetch Langfuse template and compile all per-request slots into one system prompt."""
template, prompt_obj = get_prompt_or_fallback(name, fallback)
text = compile_prompt(
template, prompt_obj,
date_context=_datetime_context_injection(context).strip(),
language_instruction=_language_instruction(context).strip(),
relational_memory=_relational_memory_injection(context).strip(),
proactive_hints=_proactive_hints_injection(context).strip(),
request_context=_request_context_block(context),
)
return text, prompt_obj
_TAG_LINE_RE = re.compile(r"<(task|timeline)>\[[^\]]+\]</\1>")
@@ -713,17 +822,11 @@ async def _run_single_agent(
lf = get_langfuse()
llm = get_agent_llm(agent_name)
tools = _all_tools_for_user(user_id, trace_id)
model_context = _context_for_model(context)
logger.info("deep_agent: run_single_agent_start trace=%s user=%s", trace_id or "-", user_id)
llm_with_tools = llm.bind_tools(tools)
messages: list[Any] = [
SystemMessage(content=system_prompt),
HumanMessage(
content=(
f"User message:\n{message}\n\n"
f"Context:\n{json.dumps({'context': model_context}, ensure_ascii=True)[:3500]}"
)
),
HumanMessage(content=message),
]
tool_calls_count = 0
@@ -843,17 +946,11 @@ async def _run_single_agent_stream(
llm = get_agent_llm(agent_name)
if tools is None:
tools = _all_tools_for_user(user_id, trace_id)
model_context = _context_for_model(context)
logger.info("deep_agent: run_single_agent_stream_start trace=%s user=%s", trace_id or "-", user_id)
llm_with_tools = llm.bind_tools(tools)
messages: list[Any] = [
SystemMessage(content=system_prompt),
HumanMessage(
content=(
f"User message:\n{message}\n\n"
f"Context:\n{json.dumps({'context': model_context}, ensure_ascii=True)[:3500]}"
)
),
HumanMessage(content=message),
]
tool_calls_count = 0
@@ -969,13 +1066,7 @@ async def _run_single_agent_stream(
async def run_home(user_id: str, message: str, context: dict[str, Any]) -> str:
prepared_context = await _prepare_context(message, context)
system_prompt, langfuse_prompt = get_prompt_or_fallback(
"home_system", _HOME_SYSTEM_PROMPT
)
system_prompt += _relational_memory_injection(context)
system_prompt += _proactive_hints_injection(context)
system_prompt += _datetime_context_injection(context)
system_prompt += _language_instruction(context)
system_prompt, langfuse_prompt = _build_system_prompt("home_system", _HOME_SYSTEM_PROMPT, prepared_context)
response = await _run_single_agent(
user_id=user_id,
system_prompt=system_prompt,
@@ -990,13 +1081,7 @@ async def run_home(user_id: str, message: str, context: dict[str, Any]) -> str:
async def run_floating(user_id: str, message: str, context: dict[str, Any]) -> tuple[str, dict[str, str | None]]:
prepared_context = await _prepare_context(message, context)
domain = await _infer_floating_domain(message, prepared_context)
system_prompt, langfuse_prompt = get_prompt_or_fallback(
"floating_system", _FLOATING_SYSTEM_PROMPT
)
system_prompt += _relational_memory_injection(context)
system_prompt += _proactive_hints_injection(context)
system_prompt += _datetime_context_injection(context)
system_prompt += _language_instruction(context)
system_prompt, langfuse_prompt = _build_system_prompt("floating_system", _FLOATING_SYSTEM_PROMPT, prepared_context)
response = await _run_single_agent(
user_id=user_id,
system_prompt=system_prompt,
@@ -1017,13 +1102,7 @@ async def run_home_stream(
context: dict[str, Any],
) -> AsyncGenerator[tuple[str, Any], None]:
prepared_context = await _prepare_context(message, context)
system_prompt, langfuse_prompt = get_prompt_or_fallback(
"home_system", _HOME_SYSTEM_PROMPT
)
system_prompt += _relational_memory_injection(context)
system_prompt += _proactive_hints_injection(context)
system_prompt += _datetime_context_injection(context)
system_prompt += _language_instruction(context)
system_prompt, langfuse_prompt = _build_system_prompt("home_system", _HOME_SYSTEM_PROMPT, prepared_context)
text_chunks: list[str] = []
async for event in _run_single_agent_stream(
user_id=user_id,
@@ -1053,13 +1132,7 @@ async def run_floating_stream(
domain = await _infer_floating_domain(message, prepared_context)
yield "floating_domain", domain
system_prompt, langfuse_prompt = get_prompt_or_fallback(
"floating_system", _FLOATING_SYSTEM_PROMPT
)
system_prompt += _relational_memory_injection(context)
system_prompt += _proactive_hints_injection(context)
system_prompt += _datetime_context_injection(context)
system_prompt += _language_instruction(context)
system_prompt, langfuse_prompt = _build_system_prompt("floating_system", _FLOATING_SYSTEM_PROMPT, prepared_context)
sanitizer = _FloatingStreamSanitizer()
emitted_sanitized = False
raw_chunks: list[str] = []