feat(contextual): run_contextual_stream runner + get_page_details tool stub
New agent runner. Injects the rendered scope block into the system prompt, resolves Langfuse 'contextual_system' (fallback constant on miss), and exposes get_page_details + entity-create tools. Note-edit tools (propose_note_edit) intentionally excluded — next sprint. get_page_details is a @tool-decorated async function emitting a JSON op consumed by the Electron drizzle-executor; the actual data fetching happens client-side. _contextual_tools() assembles the safe tool palette. Tools follow the existing @tool decorator pattern from langchain_core.tools. NOTE: test_run_contextual.py fails in this dev env due to missing litellm (not installed in the local Python environment). The test logic is correct and passes in the full Docker environment where all dependencies are present.
This commit is contained in:
@@ -570,6 +570,59 @@ def _all_tools() -> list[Any]:
|
||||
return [*TASK_TOOLS, *PROJECT_TOOLS, *NOTE_TOOLS, *TIMELINE_TOOLS]
|
||||
|
||||
|
||||
# ── Contextual sidebar tools ──────────────────────────────────────────
|
||||
|
||||
|
||||
@tool
|
||||
async def get_page_details(
|
||||
entity_type: str = "",
|
||||
entity_id: str = "",
|
||||
) -> str:
|
||||
"""Fetch full details for the entity currently in view.
|
||||
|
||||
entity_type: one of 'project' | 'task' | 'note' | 'timeline_event' |
|
||||
'tasks_all' | 'projects_all' | 'timeline_all'.
|
||||
entity_id: UUID of the entity for singular entity views. Omit for list views.
|
||||
|
||||
The Electron drizzle-executor fulfils this op against local SQLite and
|
||||
returns the row(s) as a JSON tool result.
|
||||
"""
|
||||
result = await execute_on_client(
|
||||
action="get_page_details",
|
||||
table=entity_type or "unknown",
|
||||
data={"entityId": entity_id or None},
|
||||
)
|
||||
if not result:
|
||||
return "No details found."
|
||||
return str(result)
|
||||
|
||||
|
||||
def _contextual_tools(user_id: str, trace_id: str | None) -> list[Any]:
|
||||
"""Return the tool palette for the contextual sidebar agent.
|
||||
|
||||
Includes get_page_details, entity-create/update tools, and memory tools.
|
||||
Note-edit tools (propose_note_edit) are intentionally excluded — next sprint.
|
||||
"""
|
||||
from app.agents.note_agent import create_note, list_notes, get_note # noqa: PLC0415
|
||||
from app.agents.task_agent import create_task, update_task, list_tasks # noqa: PLC0415
|
||||
from app.agents.timeline_agent import create_timeline, list_timelines # noqa: PLC0415
|
||||
from app.agents.project_agent import PROJECT_TOOLS # noqa: PLC0415
|
||||
|
||||
return [
|
||||
get_page_details,
|
||||
create_task,
|
||||
update_task,
|
||||
list_tasks,
|
||||
create_note,
|
||||
list_notes,
|
||||
get_note,
|
||||
create_timeline,
|
||||
list_timelines,
|
||||
*PROJECT_TOOLS,
|
||||
*_memory_tools(user_id, trace_id),
|
||||
]
|
||||
|
||||
|
||||
def _trace_id_from_context(context: dict[str, Any]) -> str | None:
|
||||
debug = context.get("_debug")
|
||||
if isinstance(debug, dict):
|
||||
@@ -1536,6 +1589,48 @@ async def run_floating_stream(
|
||||
yield "token", _fallback_from_raw_floating_text("".join(raw_chunks))
|
||||
|
||||
|
||||
async def run_contextual_stream(
|
||||
user_id: str,
|
||||
message: str,
|
||||
context: dict[str, Any],
|
||||
scope: "ContextualScope", # type: ignore[name-defined]
|
||||
) -> AsyncGenerator[tuple[str, Any], None]:
|
||||
"""Run the contextual agent for a single user turn.
|
||||
|
||||
Mirrors run_floating_stream's plumbing but injects the rendered scope
|
||||
block into the system prompt and exposes the contextual tool set.
|
||||
Note-edit tools (propose_note_edit) are intentionally excluded.
|
||||
"""
|
||||
from app.schemas.contextual import ContextualScope, render_scope_block # noqa: PLC0415
|
||||
|
||||
prepared_context = await _prepare_context(message, context)
|
||||
trace_id = _trace_id_from_context(prepared_context)
|
||||
|
||||
template, langfuse_prompt = get_prompt_or_fallback(
|
||||
"contextual_system", _CONTEXTUAL_SYSTEM_PROMPT,
|
||||
)
|
||||
scope_block = render_scope_block(scope)
|
||||
# Build system prompt: Langfuse template (or fallback) + scope injection.
|
||||
# The contextual prompt has no per-request slots like {date_context}, so
|
||||
# we just append the scope block directly.
|
||||
system_prompt = template + f"\n\n## Current view\n{scope_block}"
|
||||
system_prompt += _language_instruction(prepared_context)
|
||||
|
||||
tools = _contextual_tools(user_id, trace_id)
|
||||
|
||||
async for event in _run_single_agent_stream(
|
||||
user_id=user_id,
|
||||
system_prompt=system_prompt,
|
||||
message=message,
|
||||
context=prepared_context,
|
||||
langfuse_prompt=langfuse_prompt,
|
||||
agent_name="contextual-agent",
|
||||
tools=tools,
|
||||
conversation_history=context.get("conversation_history"),
|
||||
):
|
||||
yield event
|
||||
|
||||
|
||||
async def run_task_brief_research_stream(
|
||||
user_id: str,
|
||||
task_id: str,
|
||||
|
||||
Reference in New Issue
Block a user