fix tools calls
This commit is contained in:
@@ -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 (Mon–Sun)
|
||||
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')} (Mon–Sun)\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] = []
|
||||
|
||||
Reference in New Issue
Block a user