Fix home message tools calls
This commit is contained in:
@@ -16,6 +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.agent_session_buffer import session_buffer
|
||||
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
|
||||
@@ -24,6 +25,8 @@ from app.db import async_session
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
MAX_HISTORY_TURNS = 20
|
||||
|
||||
FloatingDomainType = Literal["task", "timeline", "project", "node"]
|
||||
FloatingDomainSection = Literal["task", "timeline", "note"]
|
||||
|
||||
@@ -176,6 +179,25 @@ def _relational_memory_injection(context: dict[str, Any]) -> str:
|
||||
return section
|
||||
|
||||
|
||||
_IDENTITY_KEYS = ("user_name", "job_role", "industry", "primary_use_case", "tone_preference")
|
||||
|
||||
|
||||
def _user_identity_injection(context: dict[str, Any]) -> str:
|
||||
"""Return a compact user-profile block from core memory onboarding fields.
|
||||
|
||||
Returns empty string when no onboarding keys are present.
|
||||
"""
|
||||
core = context.get("core_memory") or {}
|
||||
parts: list[str] = []
|
||||
for key in _IDENTITY_KEYS:
|
||||
val = (core.get(key) or "").strip()
|
||||
if val:
|
||||
parts.append(f"- {key}: {val}")
|
||||
if not parts:
|
||||
return ""
|
||||
return "\n\nUser profile:\n" + "\n".join(parts)
|
||||
|
||||
|
||||
def _request_context_block(context: dict[str, Any]) -> str:
|
||||
"""Return a small block with per-request scope and resolved project context."""
|
||||
parts: list[str] = []
|
||||
@@ -189,16 +211,39 @@ def _request_context_block(context: dict[str, Any]) -> str:
|
||||
|
||||
|
||||
_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.
|
||||
You are adiuvAI's home executive assistant.{user_identity}
|
||||
You are not a chatbot — you are a proactive partner who runs ahead of the user, anticipates what they need next, and closes every reply with a concrete next step or a clarifying question.
|
||||
|
||||
# How you work
|
||||
- Use tools before answering anything factual. Never guess counts, dates, or status.
|
||||
- Prefer parallel tool calls when the questions are independent (e.g. counts per status). Chain calls when one result feeds the next.
|
||||
- After delivering the answer, propose the next useful action: a follow-up task to draft, a deadline at risk, a project to triage, a person to remind. Use what you know about the user (job role, industry, primary use case) to make the suggestion relevant.
|
||||
- Match the user's tone preference. Default to warm-but-direct; stay concise.
|
||||
- When the user asks to remember, forget, or update something, use memory tools.
|
||||
|
||||
# Filter discipline
|
||||
- Never set the `assignee` filter on list_tasks/count_tasks unless the user explicitly names a person ("Marco's tasks") or refers to themselves ("my tasks", "assigned to me", "mine").
|
||||
- The user's own name in the User profile block is for context only — it is NOT a default filter.
|
||||
- When in doubt, omit `assignee` and return the global result.
|
||||
|
||||
# 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.
|
||||
Return markdown. Reference entities with these tags exactly — one id per tag, each tag on its own line, no prefix/suffix text on the same line:
|
||||
<project>id</project> <task>id</task> <note>id</note> <timeline>id</timeline>
|
||||
|
||||
When the answer contains a list of entities (any of the tags above), structure the reply as three blocks separated by blank lines:
|
||||
1. One short intro line stating what is coming (count + scope, e.g. "Ecco i tuoi 18 task ad alta priorità:"). Match the user's language.
|
||||
2. All entity tags, one per line, consecutive, no prose interleaved. Do NOT put titles, dates, priorities, or any descriptive text on the same line as a tag or between tags.
|
||||
3. One short closing recap (1–2 sentences) that points out a pattern, risk, or insight noticed in the list, and ends with a concrete next step or clarifying question.
|
||||
|
||||
For single-entity answers skip blocks 1 and 3 if they would be redundant; just emit the tag.
|
||||
|
||||
For analytical answers (status overviews, breakdowns by category/priority/project, comparisons, trends, "resoconto", "panoramica") consider returning a chart block when it communicates the answer faster than prose. The decision is yours — skip charts for trivial single-number answers. Schema:
|
||||
<chart>{{"chartType":"pie|bar|line|area|radar|radial","title":"...","data":[{{"name":"...","value":N}},...], "config":{{"value":{{"label":"...","color":"var(--chart-1)"}} }} }}</chart>
|
||||
- pie for share-of-total breakdowns; bar for category comparisons; line/area for time series; radar for multi-dimension.
|
||||
- data rows must include a "name" field; numeric series keys must match config keys.
|
||||
- Use var(--chart-1) through var(--chart-5) for colors, cycling 1-5 in series order. Do NOT wrap in hsl() or oklch() — these are complete CSS values already.
|
||||
|
||||
For upcoming-timeline questions ("prossimi eventi"), include only future items in the current month unless the user asks otherwise.
|
||||
|
||||
# Date filtering
|
||||
{date_context}
|
||||
@@ -221,11 +266,23 @@ For "today" / "tomorrow" queries, prefer list_tasks_due_today / list_timelines_t
|
||||
"""
|
||||
|
||||
_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.
|
||||
You are adiuvAI's floating executive assistant.{user_identity}
|
||||
You are pinned to a specific entity (task, timeline event, project, or note) and you stay strictly within that scope.
|
||||
Be a proactive partner: anticipate the next useful action and close with a concrete suggestion or a clarifying question — but stay terse, one short paragraph at most.
|
||||
|
||||
# How you work
|
||||
- Use tools before answering anything factual. Never guess.
|
||||
- Stay in the floating scope (see Request context). If the user asks something outside scope, answer briefly and suggest opening the home assistant.
|
||||
- Match the user's tone preference. Default to warm-but-direct.
|
||||
- When the user asks to remember, forget, or update something, use memory tools.
|
||||
|
||||
# Filter discipline
|
||||
- Never set the `assignee` filter on list_tasks/count_tasks unless the user explicitly names a person ("Marco's tasks") or refers to themselves ("my tasks", "assigned to me", "mine").
|
||||
- The user's own name in the User profile block is for context only — it is NOT a default filter.
|
||||
- When in doubt, omit `assignee` and return the global result.
|
||||
|
||||
# Output format
|
||||
Plain text only. Do NOT output XML/HTML-like tags such as <task>, <project>, <note>, <timeline>, or any bracketed-id wrappers, and do NOT output <chart> blocks — those are for the home assistant.
|
||||
|
||||
# Date filtering
|
||||
{date_context}
|
||||
@@ -361,6 +418,7 @@ def _build_system_prompt(name: str, fallback: str, context: dict[str, Any]) -> t
|
||||
template, prompt_obj,
|
||||
date_context=_datetime_context_injection(context).strip(),
|
||||
language_instruction=_language_instruction(context).strip(),
|
||||
user_identity=_user_identity_injection(context).strip(),
|
||||
relational_memory=_relational_memory_injection(context).strip(),
|
||||
proactive_hints=_proactive_hints_injection(context).strip(),
|
||||
request_context=_request_context_block(context),
|
||||
@@ -807,6 +865,23 @@ async def _infer_floating_domain(message: str, context: dict[str, Any]) -> dict[
|
||||
return _infer_floating_domain_rule_based(message, context)
|
||||
|
||||
|
||||
def _history_to_messages(history: list[dict[str, str]] | None) -> list[Any]:
|
||||
if not history:
|
||||
return []
|
||||
turns = history[-MAX_HISTORY_TURNS:]
|
||||
result: list[Any] = []
|
||||
for turn in turns:
|
||||
role = turn.get("role", "")
|
||||
content = turn.get("content", "")
|
||||
if not content:
|
||||
continue
|
||||
if role == "user":
|
||||
result.append(HumanMessage(content=content))
|
||||
elif role == "assistant":
|
||||
result.append(AIMessage(content=content))
|
||||
return result
|
||||
|
||||
|
||||
async def _run_single_agent(
|
||||
*,
|
||||
user_id: str,
|
||||
@@ -816,6 +891,7 @@ async def _run_single_agent(
|
||||
max_steps: int = 6,
|
||||
langfuse_prompt: Any = None,
|
||||
agent_name: str = "agent",
|
||||
conversation_history: list[dict[str, str]] | None = None,
|
||||
) -> str:
|
||||
trace_id = _trace_id_from_context(context)
|
||||
session_id = _session_id_from_context(context)
|
||||
@@ -824,8 +900,11 @@ async def _run_single_agent(
|
||||
tools = _all_tools_for_user(user_id, trace_id)
|
||||
logger.info("deep_agent: run_single_agent_start trace=%s user=%s", trace_id or "-", user_id)
|
||||
llm_with_tools = llm.bind_tools(tools)
|
||||
_buffered = session_buffer.get(user_id, session_id) if session_id else None
|
||||
history_messages = _buffered if _buffered is not None else _history_to_messages(conversation_history)
|
||||
messages: list[Any] = [
|
||||
SystemMessage(content=system_prompt),
|
||||
*history_messages,
|
||||
HumanMessage(content=message),
|
||||
]
|
||||
|
||||
@@ -838,7 +917,7 @@ async def _run_single_agent(
|
||||
|
||||
_span_ctx = (
|
||||
lf.start_as_current_observation(
|
||||
as_type="span",
|
||||
as_type="agent",
|
||||
name=agent_name,
|
||||
metadata={"user_id": user_id, "session_id": trace_id},
|
||||
input=message,
|
||||
@@ -846,6 +925,7 @@ async def _run_single_agent(
|
||||
if lf else None
|
||||
)
|
||||
_span = _span_ctx.__enter__() if _span_ctx else None
|
||||
_messages_to_save: list[Any] | None = None
|
||||
|
||||
try:
|
||||
for _ in range(max_steps):
|
||||
@@ -878,6 +958,7 @@ async def _run_single_agent(
|
||||
)
|
||||
if _span:
|
||||
_span.update(output=final_text)
|
||||
_messages_to_save = messages[1:] # strip SystemMessage; save full tool history
|
||||
return final_text
|
||||
|
||||
tool_map = {tool_def.name: tool_def for tool_def in tools}
|
||||
@@ -896,6 +977,14 @@ async def _run_single_agent(
|
||||
tool_fn = tool_map.get(call_name)
|
||||
if tool_fn is None:
|
||||
tool_output = f"Unknown tool: {call_name}"
|
||||
elif lf:
|
||||
with lf.start_as_current_observation(
|
||||
as_type="tool",
|
||||
name=call_name,
|
||||
input=call_args,
|
||||
) as tool_obs:
|
||||
tool_output = await tool_fn.ainvoke(call_args)
|
||||
tool_obs.update(output=str(tool_output)[:8000])
|
||||
else:
|
||||
tool_output = await tool_fn.ainvoke(call_args)
|
||||
|
||||
@@ -910,6 +999,7 @@ async def _run_single_agent(
|
||||
|
||||
final = await llm.ainvoke(messages)
|
||||
final_text = _as_text(final.content)
|
||||
messages.append(AIMessage(content=final_text))
|
||||
logger.info(
|
||||
"deep_agent: run_single_agent_end trace=%s user=%s tool_calls=%d response_chars=%d fallback=1",
|
||||
trace_id or "-",
|
||||
@@ -919,8 +1009,11 @@ async def _run_single_agent(
|
||||
)
|
||||
if _span:
|
||||
_span.update(output=final_text)
|
||||
_messages_to_save = messages[1:]
|
||||
return final_text
|
||||
finally:
|
||||
if session_id and _messages_to_save is not None:
|
||||
session_buffer.set(user_id, session_id, _messages_to_save)
|
||||
clear_tool_result_collector()
|
||||
if _span_ctx:
|
||||
_span_ctx.__exit__(None, None, None)
|
||||
@@ -939,6 +1032,7 @@ async def _run_single_agent_stream(
|
||||
langfuse_prompt: Any = None,
|
||||
agent_name: str = "agent",
|
||||
tools: list[Any] | None = None,
|
||||
conversation_history: list[dict[str, str]] | None = None,
|
||||
) -> AsyncGenerator[tuple[str, Any], None]:
|
||||
trace_id = _trace_id_from_context(context)
|
||||
session_id = _session_id_from_context(context)
|
||||
@@ -948,8 +1042,11 @@ async def _run_single_agent_stream(
|
||||
tools = _all_tools_for_user(user_id, trace_id)
|
||||
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)
|
||||
_buffered = session_buffer.get(user_id, session_id) if session_id else None
|
||||
history_messages = _buffered if _buffered is not None else _history_to_messages(conversation_history)
|
||||
messages: list[Any] = [
|
||||
SystemMessage(content=system_prompt),
|
||||
*history_messages,
|
||||
HumanMessage(content=message),
|
||||
]
|
||||
|
||||
@@ -963,7 +1060,7 @@ async def _run_single_agent_stream(
|
||||
|
||||
_span_ctx = (
|
||||
lf.start_as_current_observation(
|
||||
as_type="span",
|
||||
as_type="agent",
|
||||
name=f"{agent_name}-stream",
|
||||
metadata={"user_id": user_id, "session_id": trace_id},
|
||||
input=message,
|
||||
@@ -972,6 +1069,7 @@ async def _run_single_agent_stream(
|
||||
)
|
||||
_span = _span_ctx.__enter__() if _span_ctx else None
|
||||
streamed_text: list[str] = []
|
||||
_messages_to_save: list[Any] | None = None
|
||||
|
||||
try:
|
||||
for _ in range(max_steps):
|
||||
@@ -1009,6 +1107,8 @@ async def _run_single_agent_stream(
|
||||
)
|
||||
if _span:
|
||||
_span.update(output="".join(streamed_text))
|
||||
messages.append(response)
|
||||
_messages_to_save = messages[1:] # strip SystemMessage
|
||||
return
|
||||
|
||||
messages.append(response)
|
||||
@@ -1028,6 +1128,14 @@ async def _run_single_agent_stream(
|
||||
tool_fn = tool_map.get(call_name)
|
||||
if tool_fn is None:
|
||||
tool_output = f"Unknown tool: {call_name}"
|
||||
elif lf:
|
||||
with lf.start_as_current_observation(
|
||||
as_type="tool",
|
||||
name=call_name,
|
||||
input=call_args,
|
||||
) as tool_obs:
|
||||
tool_output = await tool_fn.ainvoke(call_args)
|
||||
tool_obs.update(output=str(tool_output)[:8000])
|
||||
else:
|
||||
tool_output = await tool_fn.ainvoke(call_args)
|
||||
|
||||
@@ -1040,12 +1148,16 @@ async def _run_single_agent_stream(
|
||||
|
||||
messages.append(ToolMessage(content=str(tool_output), tool_call_id=call["id"]))
|
||||
|
||||
fallback_chunks: list[str] = []
|
||||
async for chunk in llm.astream(messages):
|
||||
token = _as_text(getattr(chunk, "content", ""))
|
||||
if token:
|
||||
streamed_chars += len(token)
|
||||
streamed_text.append(token)
|
||||
fallback_chunks.append(token)
|
||||
yield "token", token
|
||||
messages.append(AIMessage(content="".join(fallback_chunks)))
|
||||
_messages_to_save = messages[1:]
|
||||
logger.info(
|
||||
"deep_agent: run_single_agent_stream_end trace=%s user=%s tool_calls=%d response_chars=%d fallback=1",
|
||||
trace_id or "-",
|
||||
@@ -1056,6 +1168,8 @@ async def _run_single_agent_stream(
|
||||
if _span:
|
||||
_span.update(output="".join(streamed_text))
|
||||
finally:
|
||||
if session_id and _messages_to_save is not None:
|
||||
session_buffer.set(user_id, session_id, _messages_to_save)
|
||||
clear_tool_result_collector()
|
||||
if _span_ctx:
|
||||
_span_ctx.__exit__(None, None, None)
|
||||
@@ -1074,6 +1188,7 @@ async def run_home(user_id: str, message: str, context: dict[str, Any]) -> str:
|
||||
context=prepared_context,
|
||||
langfuse_prompt=langfuse_prompt,
|
||||
agent_name="home-agent",
|
||||
conversation_history=context.get("conversation_history"),
|
||||
)
|
||||
return _normalize_tagged_list_lines(response, message)
|
||||
|
||||
@@ -1089,6 +1204,7 @@ async def run_floating(user_id: str, message: str, context: dict[str, Any]) -> t
|
||||
context=prepared_context,
|
||||
langfuse_prompt=langfuse_prompt,
|
||||
agent_name="floating-agent",
|
||||
conversation_history=context.get("conversation_history"),
|
||||
)
|
||||
sanitized = _strip_floating_markup(response)
|
||||
if not sanitized and response:
|
||||
@@ -1111,6 +1227,7 @@ async def run_home_stream(
|
||||
context=prepared_context,
|
||||
langfuse_prompt=langfuse_prompt,
|
||||
agent_name="home-agent",
|
||||
conversation_history=context.get("conversation_history"),
|
||||
):
|
||||
event_type, data = event
|
||||
if event_type != "token":
|
||||
@@ -1143,6 +1260,7 @@ async def run_floating_stream(
|
||||
context=prepared_context,
|
||||
langfuse_prompt=langfuse_prompt,
|
||||
agent_name="floating-agent",
|
||||
conversation_history=context.get("conversation_history"),
|
||||
):
|
||||
event_type, data = event
|
||||
if event_type != "token":
|
||||
|
||||
Reference in New Issue
Block a user