feat: integrate tracing throughout request pipeline
Instrument the full request flow with trace spans for debugging: - Wrap expert delegations (librarian/biographer/housekeeper) in spans - Add orchestrate and synthesize spans to TatlockAgent - Trace Steward analysis in preprocessing - Start/end traces in response service with context management - Simplify router by moving context handling to service layer - Include tracing router in debug mode - Remove benchmark recording from tool_tracking and steward service 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
+145
-84
@@ -13,6 +13,7 @@ from enum import Enum
|
|||||||
from typing import AsyncGenerator, Callable, Optional, Any
|
from typing import AsyncGenerator, Callable, Optional, Any
|
||||||
|
|
||||||
from src.core.logging_config import get_logger
|
from src.core.logging_config import get_logger
|
||||||
|
from src.core.tracing import trace_span, SpanType
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
@@ -239,38 +240,58 @@ async def delegate_to_librarian(
|
|||||||
has_context=bool(context),
|
has_context=bool(context),
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
async with trace_span(
|
||||||
# Use run() not run_stream() - avoids Ollama bug
|
"delegate_to_librarian",
|
||||||
output = await run_librarian(task=task, context=context)
|
SpanType.EXPERT,
|
||||||
|
metadata={
|
||||||
|
"expert": "librarian",
|
||||||
|
"task_preview": task[:100],
|
||||||
|
"has_context": bool(context),
|
||||||
|
},
|
||||||
|
) as span:
|
||||||
|
try:
|
||||||
|
# Use run() not run_stream() - avoids Ollama bug
|
||||||
|
output = await run_librarian(task=task, context=context)
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
"delegation_to_librarian_completed",
|
"delegation_to_librarian_completed",
|
||||||
task=task[:50],
|
task=task[:50],
|
||||||
output_length=len(output),
|
output_length=len(output),
|
||||||
)
|
)
|
||||||
|
|
||||||
return DelegationResult(
|
if span:
|
||||||
expert_name="librarian",
|
span.metadata["success"] = True
|
||||||
task=task,
|
span.metadata["output_length"] = len(output)
|
||||||
success=True,
|
span.details["task"] = task
|
||||||
output=output,
|
span.details["context"] = context[:500] if context else None
|
||||||
)
|
span.details["result_preview"] = output[:1000]
|
||||||
|
|
||||||
except Exception as e:
|
return DelegationResult(
|
||||||
logger.error(
|
expert_name="librarian",
|
||||||
"delegation_to_librarian_error",
|
task=task,
|
||||||
task=task[:50],
|
success=True,
|
||||||
error=str(e),
|
output=output,
|
||||||
exc_info=True,
|
)
|
||||||
)
|
|
||||||
|
|
||||||
return DelegationResult(
|
except Exception as e:
|
||||||
expert_name="librarian",
|
logger.error(
|
||||||
task=task,
|
"delegation_to_librarian_error",
|
||||||
success=False,
|
task=task[:50],
|
||||||
output="",
|
error=str(e),
|
||||||
error=str(e),
|
exc_info=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if span:
|
||||||
|
span.metadata["success"] = False
|
||||||
|
span.details["error"] = str(e)
|
||||||
|
|
||||||
|
return DelegationResult(
|
||||||
|
expert_name="librarian",
|
||||||
|
task=task,
|
||||||
|
success=False,
|
||||||
|
output="",
|
||||||
|
error=str(e),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def delegate_to_biographer(
|
async def delegate_to_biographer(
|
||||||
@@ -317,38 +338,58 @@ async def delegate_to_biographer(
|
|||||||
has_context=bool(context),
|
has_context=bool(context),
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
async with trace_span(
|
||||||
# Use run() not run_stream() - avoids Ollama bug
|
"delegate_to_biographer",
|
||||||
output = await run_biographer(task=task, context=context)
|
SpanType.EXPERT,
|
||||||
|
metadata={
|
||||||
|
"expert": "biographer",
|
||||||
|
"task_preview": task[:100],
|
||||||
|
"has_context": bool(context),
|
||||||
|
},
|
||||||
|
) as span:
|
||||||
|
try:
|
||||||
|
# Use run() not run_stream() - avoids Ollama bug
|
||||||
|
output = await run_biographer(task=task, context=context)
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
"delegation_to_biographer_completed",
|
"delegation_to_biographer_completed",
|
||||||
task=task[:50],
|
task=task[:50],
|
||||||
output_length=len(output),
|
output_length=len(output),
|
||||||
)
|
)
|
||||||
|
|
||||||
return DelegationResult(
|
if span:
|
||||||
expert_name="biographer",
|
span.metadata["success"] = True
|
||||||
task=task,
|
span.metadata["output_length"] = len(output)
|
||||||
success=True,
|
span.details["task"] = task
|
||||||
output=output,
|
span.details["context"] = context[:500] if context else None
|
||||||
)
|
span.details["result_preview"] = output[:1000]
|
||||||
|
|
||||||
except Exception as e:
|
return DelegationResult(
|
||||||
logger.error(
|
expert_name="biographer",
|
||||||
"delegation_to_biographer_error",
|
task=task,
|
||||||
task=task[:50],
|
success=True,
|
||||||
error=str(e),
|
output=output,
|
||||||
exc_info=True,
|
)
|
||||||
)
|
|
||||||
|
|
||||||
return DelegationResult(
|
except Exception as e:
|
||||||
expert_name="biographer",
|
logger.error(
|
||||||
task=task,
|
"delegation_to_biographer_error",
|
||||||
success=False,
|
task=task[:50],
|
||||||
output="",
|
error=str(e),
|
||||||
error=str(e),
|
exc_info=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if span:
|
||||||
|
span.metadata["success"] = False
|
||||||
|
span.details["error"] = str(e)
|
||||||
|
|
||||||
|
return DelegationResult(
|
||||||
|
expert_name="biographer",
|
||||||
|
task=task,
|
||||||
|
success=False,
|
||||||
|
output="",
|
||||||
|
error=str(e),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def delegate_to_housekeeper(
|
async def delegate_to_housekeeper(
|
||||||
@@ -394,38 +435,58 @@ async def delegate_to_housekeeper(
|
|||||||
has_context=bool(context),
|
has_context=bool(context),
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
async with trace_span(
|
||||||
# Use run() not run_stream() - avoids Ollama bug
|
"delegate_to_housekeeper",
|
||||||
output = await run_housekeeper(task=task, context=context)
|
SpanType.EXPERT,
|
||||||
|
metadata={
|
||||||
|
"expert": "housekeeper",
|
||||||
|
"task_preview": task[:100],
|
||||||
|
"has_context": bool(context),
|
||||||
|
},
|
||||||
|
) as span:
|
||||||
|
try:
|
||||||
|
# Use run() not run_stream() - avoids Ollama bug
|
||||||
|
output = await run_housekeeper(task=task, context=context)
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
"delegation_to_housekeeper_completed",
|
"delegation_to_housekeeper_completed",
|
||||||
task=task[:50],
|
task=task[:50],
|
||||||
output_length=len(output),
|
output_length=len(output),
|
||||||
)
|
)
|
||||||
|
|
||||||
return DelegationResult(
|
if span:
|
||||||
expert_name="housekeeper",
|
span.metadata["success"] = True
|
||||||
task=task,
|
span.metadata["output_length"] = len(output)
|
||||||
success=True,
|
span.details["task"] = task
|
||||||
output=output,
|
span.details["context"] = context[:500] if context else None
|
||||||
)
|
span.details["result_preview"] = output[:1000]
|
||||||
|
|
||||||
except Exception as e:
|
return DelegationResult(
|
||||||
logger.error(
|
expert_name="housekeeper",
|
||||||
"delegation_to_housekeeper_error",
|
task=task,
|
||||||
task=task[:50],
|
success=True,
|
||||||
error=str(e),
|
output=output,
|
||||||
exc_info=True,
|
)
|
||||||
)
|
|
||||||
|
|
||||||
return DelegationResult(
|
except Exception as e:
|
||||||
expert_name="housekeeper",
|
logger.error(
|
||||||
task=task,
|
"delegation_to_housekeeper_error",
|
||||||
success=False,
|
task=task[:50],
|
||||||
output="",
|
error=str(e),
|
||||||
error=str(e),
|
exc_info=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if span:
|
||||||
|
span.metadata["success"] = False
|
||||||
|
span.details["error"] = str(e)
|
||||||
|
|
||||||
|
return DelegationResult(
|
||||||
|
expert_name="housekeeper",
|
||||||
|
task=task,
|
||||||
|
success=False,
|
||||||
|
output="",
|
||||||
|
error=str(e),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
"""
|
"""
|
||||||
Steward service layer.
|
Steward service layer.
|
||||||
|
|
||||||
Provides high-level interface for request analysis with logging,
|
Provides high-level interface for request analysis with logging
|
||||||
benchmarking, and error handling.
|
and error handling.
|
||||||
|
|
||||||
Parses plain text recommendations into structured data.
|
Parses plain text recommendations into structured data.
|
||||||
Includes memory pre-fetch for user context injection.
|
Includes memory pre-fetch for user context injection.
|
||||||
@@ -10,7 +10,6 @@ Includes memory pre-fetch for user context injection.
|
|||||||
import re
|
import re
|
||||||
from typing import Any, Optional
|
from typing import Any, Optional
|
||||||
|
|
||||||
from src.core.benchmarks import PerformanceBenchmark, get_benchmark_store
|
|
||||||
from src.core.household_registry import get_household_registry
|
from src.core.household_registry import get_household_registry
|
||||||
from src.core.logging_config import get_logger, log_operation
|
from src.core.logging_config import get_logger, log_operation
|
||||||
from src.core.memory_service import memory_service
|
from src.core.memory_service import memory_service
|
||||||
@@ -285,8 +284,7 @@ async def analyze_request(
|
|||||||
This is the main entry point for Steward analysis. It:
|
This is the main entry point for Steward analysis. It:
|
||||||
1. Calls the Steward agent with full conversation history
|
1. Calls the Steward agent with full conversation history
|
||||||
2. Logs the operation with timing
|
2. Logs the operation with timing
|
||||||
3. Records performance benchmarks to Redis
|
3. Returns structured recommendations
|
||||||
4. Returns structured recommendations
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
user_request: The current user message to analyze
|
user_request: The current user message to analyze
|
||||||
@@ -365,23 +363,6 @@ async def analyze_request(
|
|||||||
reasoning=analysis_text[:200], # First 200 chars
|
reasoning=analysis_text[:200], # First 200 chars
|
||||||
)
|
)
|
||||||
|
|
||||||
# Record performance benchmark
|
|
||||||
if log_ctx.get("duration_seconds"):
|
|
||||||
benchmark = PerformanceBenchmark(
|
|
||||||
operation="steward_analysis",
|
|
||||||
duration_seconds=log_ctx["duration_seconds"],
|
|
||||||
success=True,
|
|
||||||
recommendation_count=len(recommendation.recommended_capabilities),
|
|
||||||
confidence=None, # Could add confidence scoring in future
|
|
||||||
conversation_id=conversation_id,
|
|
||||||
metadata={
|
|
||||||
"complexity": recommendation.estimated_complexity,
|
|
||||||
"has_context": recommendation.conversation_context.has_previous_context,
|
|
||||||
"missing_capabilities": recommendation.missing_capabilities is not None,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
await get_benchmark_store().record(benchmark)
|
|
||||||
|
|
||||||
return recommendation
|
return recommendation
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
+56
-2
@@ -20,6 +20,11 @@ from src.agents.tatlock_core.tools import (
|
|||||||
)
|
)
|
||||||
from src.core.config import config
|
from src.core.config import config
|
||||||
from src.core.logging_config import get_logger
|
from src.core.logging_config import get_logger
|
||||||
|
from src.core.tracing import (
|
||||||
|
start_span, end_span, get_current_span,
|
||||||
|
add_tool_spans_from_messages,
|
||||||
|
SpanType, SpanStatus,
|
||||||
|
)
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
@@ -433,7 +438,7 @@ class TatlockAgent(AgentInterface):
|
|||||||
steward_note: Note from Steward (prepended to request, invisible to user)
|
steward_note: Note from Steward (prepended to request, invisible to user)
|
||||||
scoped_tools: List of tool definitions from household registry
|
scoped_tools: List of tool definitions from household registry
|
||||||
message_history: Conversation history in PydanticAI format
|
message_history: Conversation history in PydanticAI format
|
||||||
tool_tracker: Optional tool call tracker for benchmarking
|
tool_tracker: Optional tool call tracker for analysis
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
str: Tatlock's response text
|
str: Tatlock's response text
|
||||||
@@ -627,7 +632,7 @@ class TatlockAgent(AgentInterface):
|
|||||||
steward_note: Note from Steward (invisible to user)
|
steward_note: Note from Steward (invisible to user)
|
||||||
scoped_tools: List of tool definitions from household registry
|
scoped_tools: List of tool definitions from household registry
|
||||||
message_history: Conversation history
|
message_history: Conversation history
|
||||||
tool_tracker: Optional tool call tracker for benchmarking
|
tool_tracker: Optional tool call tracker for analysis
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
dict with:
|
dict with:
|
||||||
@@ -655,6 +660,16 @@ class TatlockAgent(AgentInterface):
|
|||||||
history_length=len(message_history),
|
history_length=len(message_history),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Start tracing span for orchestration phase
|
||||||
|
orchestrate_span = start_span(
|
||||||
|
"tatlock_orchestrate",
|
||||||
|
SpanType.TATLOCK,
|
||||||
|
metadata={
|
||||||
|
"scoped_tool_count": len(scoped_tools),
|
||||||
|
"tool_names": [getattr(t, '__name__', str(t)) for t in scoped_tools[:5]],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
# Create a fresh agent instance with scoped tools only
|
# Create a fresh agent instance with scoped tools only
|
||||||
clean_host = self.ollama_host.rstrip('/')
|
clean_host = self.ollama_host.rstrip('/')
|
||||||
base_url = f"{clean_host}/v1"
|
base_url = f"{clean_host}/v1"
|
||||||
@@ -731,6 +746,23 @@ class TatlockAgent(AgentInterface):
|
|||||||
tool_output_count=len(tool_outputs),
|
tool_output_count=len(tool_outputs),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Add tool-level spans from result messages
|
||||||
|
if orchestrate_span:
|
||||||
|
add_tool_spans_from_messages(result.new_messages(), orchestrate_span)
|
||||||
|
|
||||||
|
# End orchestration span with results
|
||||||
|
end_span(
|
||||||
|
orchestrate_span,
|
||||||
|
metadata_update={
|
||||||
|
"tools_called": tools_called,
|
||||||
|
"expert_count": len(expert_results),
|
||||||
|
"tool_output_count": len(tool_outputs),
|
||||||
|
},
|
||||||
|
details_update={
|
||||||
|
"steward_note_preview": steward_note[:500] if steward_note else None,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"tools_called": tools_called,
|
"tools_called": tools_called,
|
||||||
"expert_results": expert_results,
|
"expert_results": expert_results,
|
||||||
@@ -769,6 +801,16 @@ class TatlockAgent(AgentInterface):
|
|||||||
tool_count=len(orchestration_results.get("tool_outputs", {})),
|
tool_count=len(orchestration_results.get("tool_outputs", {})),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Start tracing span for synthesis phase
|
||||||
|
synthesize_span = start_span(
|
||||||
|
"tatlock_synthesize",
|
||||||
|
SpanType.TATLOCK,
|
||||||
|
metadata={
|
||||||
|
"expert_count": len(orchestration_results.get("expert_results", {})),
|
||||||
|
"tool_output_count": len(orchestration_results.get("tool_outputs", {})),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
# Build synthesis prompt with all available information
|
# Build synthesis prompt with all available information
|
||||||
synthesis_parts = []
|
synthesis_parts = []
|
||||||
synthesis_parts.append(f"The user asked: {user_message}")
|
synthesis_parts.append(f"The user asked: {user_message}")
|
||||||
@@ -841,6 +883,18 @@ class TatlockAgent(AgentInterface):
|
|||||||
response_preview=result.output[:100],
|
response_preview=result.output[:100],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# End synthesis span with result
|
||||||
|
end_span(
|
||||||
|
synthesize_span,
|
||||||
|
metadata_update={
|
||||||
|
"response_length": len(result.output),
|
||||||
|
},
|
||||||
|
details_update={
|
||||||
|
"synthesis_prompt": synthesis_prompt[:1000],
|
||||||
|
"response_preview": result.output[:500],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
return result.output
|
return result.output
|
||||||
|
|
||||||
async def get_capabilities(self) -> dict:
|
async def get_capabilities(self) -> dict:
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ from src.agents.steward import analyze_request, format_steward_note
|
|||||||
from src.agents.steward.schemas import StewardRecommendation
|
from src.agents.steward.schemas import StewardRecommendation
|
||||||
from src.core.household_registry import get_household_registry
|
from src.core.household_registry import get_household_registry
|
||||||
from src.core.logging_config import get_logger
|
from src.core.logging_config import get_logger
|
||||||
|
from src.core.tracing import trace_span, SpanType
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
@@ -93,12 +94,32 @@ async def preprocess_request(
|
|||||||
conversation_id=conversation_id,
|
conversation_id=conversation_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Call Steward with full conversation history
|
# Call Steward with full conversation history (traced)
|
||||||
recommendation = await analyze_request(
|
async with trace_span(
|
||||||
enriched_request,
|
"steward_analysis",
|
||||||
conversation_history=conversation_history,
|
SpanType.STEWARD,
|
||||||
conversation_id=conversation_id,
|
metadata={
|
||||||
)
|
"request_preview": user_request[:100],
|
||||||
|
"history_length": len(conversation_history),
|
||||||
|
},
|
||||||
|
) as span:
|
||||||
|
recommendation = await analyze_request(
|
||||||
|
enriched_request,
|
||||||
|
conversation_history=conversation_history,
|
||||||
|
conversation_id=conversation_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Update span with results
|
||||||
|
if span:
|
||||||
|
span.metadata.update({
|
||||||
|
"recommended_capabilities": recommendation.recommended_capabilities,
|
||||||
|
"complexity": recommendation.estimated_complexity,
|
||||||
|
"has_memory_context": bool(recommendation.memory_context),
|
||||||
|
"has_conversation_context": recommendation.conversation_context.has_previous_context,
|
||||||
|
})
|
||||||
|
span.details["reasoning"] = recommendation.reasoning
|
||||||
|
if recommendation.enriched_query:
|
||||||
|
span.details["enriched_query"] = recommendation.enriched_query
|
||||||
|
|
||||||
# Format note for Tatlock (includes conversation context)
|
# Format note for Tatlock (includes conversation context)
|
||||||
steward_note = await format_steward_note(recommendation)
|
steward_note = await format_steward_note(recommendation)
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
"""
|
"""
|
||||||
Tool call tracking and benchmarking.
|
Tool call tracking.
|
||||||
|
|
||||||
Tracks which tools are recommended by the Steward versus which tools
|
Tracks which tools are recommended by the Steward versus which tools
|
||||||
are actually used by Tatlock, recording benchmarks for analysis.
|
are actually used by Tatlock for debugging and analysis.
|
||||||
"""
|
"""
|
||||||
from datetime import datetime, timezone
|
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from src.core.benchmarks import PerformanceBenchmark, get_benchmark_store
|
|
||||||
from src.core.logging_config import get_logger
|
from src.core.logging_config import get_logger
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
@@ -15,7 +13,7 @@ logger = get_logger(__name__)
|
|||||||
|
|
||||||
class ToolCallTracker:
|
class ToolCallTracker:
|
||||||
"""
|
"""
|
||||||
Tracks tool calls for benchmarking and accuracy analysis.
|
Tracks tool calls for accuracy analysis.
|
||||||
|
|
||||||
Compares Steward's recommendations with Tatlock's actual tool usage
|
Compares Steward's recommendations with Tatlock's actual tool usage
|
||||||
to measure recommendation accuracy.
|
to measure recommendation accuracy.
|
||||||
@@ -53,6 +51,10 @@ class ToolCallTracker:
|
|||||||
return tool_name.replace("delegate_to_", "")
|
return tool_name.replace("delegate_to_", "")
|
||||||
return tool_name
|
return tool_name
|
||||||
|
|
||||||
|
def log_call(self, message: str):
|
||||||
|
"""Log a tool call message (for UI display)."""
|
||||||
|
logger.debug("tool_call_message", message=message)
|
||||||
|
|
||||||
async def track_call(self, tool_name: str, duration: float):
|
async def track_call(self, tool_name: str, duration: float):
|
||||||
"""
|
"""
|
||||||
Record a tool call with timing.
|
Record a tool call with timing.
|
||||||
@@ -78,23 +80,6 @@ class ToolCallTracker:
|
|||||||
recommended=list(self.recommended_capabilities),
|
recommended=list(self.recommended_capabilities),
|
||||||
)
|
)
|
||||||
|
|
||||||
# Record benchmark to Redis
|
|
||||||
benchmark = PerformanceBenchmark(
|
|
||||||
timestamp=datetime.now(timezone.utc),
|
|
||||||
operation="tool_call",
|
|
||||||
duration_seconds=duration,
|
|
||||||
success=True, # If we got here, the call succeeded
|
|
||||||
tool_name=tool_name,
|
|
||||||
was_recommended=was_recommended,
|
|
||||||
was_actually_used=True,
|
|
||||||
conversation_id=self.conversation_id,
|
|
||||||
metadata={
|
|
||||||
"recommended_capabilities": list(self.recommended_capabilities),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
await get_benchmark_store().record(benchmark)
|
|
||||||
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"tool_call_tracked",
|
"tool_call_tracked",
|
||||||
tool_name=tool_name,
|
tool_name=tool_name,
|
||||||
@@ -124,24 +109,6 @@ class ToolCallTracker:
|
|||||||
conversation_id=self.conversation_id,
|
conversation_id=self.conversation_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Record benchmarks for unused recommendations
|
|
||||||
for tool_name in unused_tools:
|
|
||||||
benchmark = PerformanceBenchmark(
|
|
||||||
timestamp=datetime.now(timezone.utc),
|
|
||||||
operation="tool_call",
|
|
||||||
duration_seconds=0.0, # Not used
|
|
||||||
success=True,
|
|
||||||
tool_name=tool_name,
|
|
||||||
was_recommended=True,
|
|
||||||
was_actually_used=False,
|
|
||||||
conversation_id=self.conversation_id,
|
|
||||||
metadata={
|
|
||||||
"recommended_capabilities": list(self.recommended_capabilities),
|
|
||||||
"reason": "recommended_but_unused",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
await get_benchmark_store().record(benchmark)
|
|
||||||
|
|
||||||
# Log summary
|
# Log summary
|
||||||
total_calls = sum(len(durations) for durations in self.actual_calls.values())
|
total_calls = sum(len(durations) for durations in self.actual_calls.values())
|
||||||
logger.info(
|
logger.info(
|
||||||
|
|||||||
+8
-2
@@ -23,6 +23,7 @@ from src.core.exceptions import AppException
|
|||||||
from src.core.logging_config import get_logger
|
from src.core.logging_config import get_logger
|
||||||
from src.core.router import router as core_router
|
from src.core.router import router as core_router
|
||||||
from src.core.startup import initialize_application
|
from src.core.startup import initialize_application
|
||||||
|
from src.core.tracing_router import router as tracing_router
|
||||||
from src.models.router import router as models_router
|
from src.models.router import router as models_router
|
||||||
from src.responses.router import router as responses_router
|
from src.responses.router import router as responses_router
|
||||||
|
|
||||||
@@ -45,7 +46,7 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
|
|||||||
environment=config.ENVIRONMENT.value,
|
environment=config.ENVIRONMENT.value,
|
||||||
ollama_host=str(config.OLLAMA_HOST),
|
ollama_host=str(config.OLLAMA_HOST),
|
||||||
ollama_model=config.OLLAMA_DEFAULT_MODEL,
|
ollama_model=config.OLLAMA_DEFAULT_MODEL,
|
||||||
redis_url=config.redis_url,
|
redis_url=config.redis_memory_url,
|
||||||
log_format=config.log_format,
|
log_format=config.log_format,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -90,7 +91,12 @@ def create_application() -> FastAPI:
|
|||||||
application.include_router(chat_router, prefix=config.API_PREFIX)
|
application.include_router(chat_router, prefix=config.API_PREFIX)
|
||||||
application.include_router(models_router, prefix=config.API_PREFIX)
|
application.include_router(models_router, prefix=config.API_PREFIX)
|
||||||
application.include_router(responses_router, prefix=config.API_PREFIX) # Responses API
|
application.include_router(responses_router, prefix=config.API_PREFIX) # Responses API
|
||||||
|
|
||||||
|
# Conditionally include tracing router (only in debug mode)
|
||||||
|
if config.DEBUG:
|
||||||
|
application.include_router(tracing_router)
|
||||||
|
logger.info("tracing_router_enabled")
|
||||||
|
|
||||||
return application
|
return application
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+5
-73
@@ -10,7 +10,6 @@ from sse_starlette.sse import EventSourceResponse
|
|||||||
from src.responses import service
|
from src.responses import service
|
||||||
from src.responses.schemas import ResponseRequest, Response
|
from src.responses.schemas import ResponseRequest, Response
|
||||||
from src.core.exceptions import ModelNotFoundError, AppException
|
from src.core.exceptions import ModelNotFoundError, AppException
|
||||||
from src.core.context import current_user, current_conversation, get_default_user
|
|
||||||
from src.core.logging_config import get_logger
|
from src.core.logging_config import get_logger
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
@@ -37,77 +36,16 @@ async def create_response(
|
|||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Response object or SSE stream
|
Response object or SSE stream
|
||||||
|
|
||||||
Example non-streaming request:
|
|
||||||
POST /v1/responses
|
|
||||||
{
|
|
||||||
"model": "lorem-tester",
|
|
||||||
"input": [{"role": "user", "content": "Hello"}],
|
|
||||||
"reasoning": {"effort": "medium", "summary": "auto"},
|
|
||||||
"stream": false
|
|
||||||
}
|
|
||||||
|
|
||||||
Example streaming request:
|
|
||||||
POST /v1/responses
|
|
||||||
{
|
|
||||||
"model": "lorem-tester",
|
|
||||||
"input": [{"role": "user", "content": "Hello"}],
|
|
||||||
"stream": true
|
|
||||||
}
|
|
||||||
|
|
||||||
Response format (non-streaming):
|
|
||||||
{
|
|
||||||
"id": "resp_...",
|
|
||||||
"object": "response",
|
|
||||||
"created_at": 1733529600,
|
|
||||||
"model": "lorem-tester",
|
|
||||||
"status": "completed",
|
|
||||||
"output": [
|
|
||||||
{
|
|
||||||
"type": "reasoning",
|
|
||||||
"id": "rs_...",
|
|
||||||
"summary": ["Analyzing...", "Considering..."]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "message",
|
|
||||||
"id": "msg_...",
|
|
||||||
"role": "assistant",
|
|
||||||
"content": [{"type": "output_text", "text": "Lorem ipsum..."}]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"usage": {
|
|
||||||
"input_tokens": 10,
|
|
||||||
"output_tokens": 50,
|
|
||||||
"reasoning_tokens": 20,
|
|
||||||
"total_tokens": 80
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Streaming format (SSE):
|
|
||||||
event: response.reasoning_summary_text.delta
|
|
||||||
data: {"delta": "Analyzing..."}
|
|
||||||
|
|
||||||
event: response.output_text.delta
|
|
||||||
data: {"delta": "Lorem"}
|
|
||||||
|
|
||||||
event: response.done
|
|
||||||
data: {"response": {...}}
|
|
||||||
"""
|
"""
|
||||||
# Set request context (propagates through all async calls)
|
|
||||||
effective_user = request.user or get_default_user()
|
|
||||||
user_token = current_user.set(effective_user)
|
|
||||||
conv_id = request.metadata.get("conversation_id") if request.metadata else None
|
|
||||||
conv_token = current_conversation.set(conv_id)
|
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
"response_request_received",
|
"response_request_received",
|
||||||
model=request.model,
|
model=request.model,
|
||||||
user=effective_user,
|
user=request.user,
|
||||||
conversation_id=conv_id,
|
streaming=request.stream,
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Check if this is a Tatlock request - use Steward preprocessing (Phase 2)
|
# Check if this is a Tatlock request - use Steward preprocessing
|
||||||
model_id = request.model
|
model_id = request.model
|
||||||
if "." in model_id:
|
if "." in model_id:
|
||||||
model_id = model_id.split(".", 1)[1]
|
model_id = model_id.split(".", 1)[1]
|
||||||
@@ -116,21 +54,20 @@ async def create_response(
|
|||||||
|
|
||||||
if request.stream:
|
if request.stream:
|
||||||
logger.info("Streaming response requested")
|
logger.info("Streaming response requested")
|
||||||
|
|
||||||
if use_steward:
|
if use_steward:
|
||||||
logger.info("Streaming with Steward preprocessing for Tatlock request")
|
logger.info("Streaming with Steward preprocessing for Tatlock request")
|
||||||
# Use Steward + Tatlock streaming (Milestone 3.5)
|
|
||||||
from src.responses.streaming import StreamingCoordinator
|
from src.responses.streaming import StreamingCoordinator
|
||||||
coordinator = StreamingCoordinator()
|
coordinator = StreamingCoordinator()
|
||||||
return EventSourceResponse(
|
return EventSourceResponse(
|
||||||
coordinator.stream_response_with_steward(request)
|
coordinator.stream_response_with_steward(request)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# Regular streaming for non-Tatlock models
|
|
||||||
return EventSourceResponse(
|
return EventSourceResponse(
|
||||||
service.create_response_stream(request)
|
service.create_response_stream(request)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Use appropriate service method
|
# Non-streaming response
|
||||||
if use_steward:
|
if use_steward:
|
||||||
logger.info("Using Steward preprocessing for Tatlock request")
|
logger.info("Using Steward preprocessing for Tatlock request")
|
||||||
return await service.create_response_with_steward(request)
|
return await service.create_response_with_steward(request)
|
||||||
@@ -148,8 +85,3 @@ async def create_response(
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Unexpected error: {e}", exc_info=True)
|
logger.error(f"Unexpected error: {e}", exc_info=True)
|
||||||
raise HTTPException(status_code=500, detail="Internal server error")
|
raise HTTPException(status_code=500, detail="Internal server error")
|
||||||
|
|
||||||
finally:
|
|
||||||
# Reset context (important for connection reuse)
|
|
||||||
current_user.reset(user_token)
|
|
||||||
current_conversation.reset(conv_token)
|
|
||||||
|
|||||||
+251
-140
@@ -26,6 +26,8 @@ from src.responses.context import ContextWindow
|
|||||||
from src.core.preprocessing import preprocess_request
|
from src.core.preprocessing import preprocess_request
|
||||||
from src.core.tool_tracking import ToolCallTracker
|
from src.core.tool_tracking import ToolCallTracker
|
||||||
from src.core.logging_config import get_logger
|
from src.core.logging_config import get_logger
|
||||||
|
from src.core.tracing import start_trace, end_trace, start_span, SpanType
|
||||||
|
from src.core.context import current_user, current_conversation, get_default_user
|
||||||
from src.agents.steward.schemas import StewardRecommendation
|
from src.agents.steward.schemas import StewardRecommendation
|
||||||
|
|
||||||
import re
|
import re
|
||||||
@@ -34,6 +36,29 @@ import asyncio
|
|||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_user_input(input_data) -> str:
|
||||||
|
"""Extract user input text from request input for tracing."""
|
||||||
|
if isinstance(input_data, str):
|
||||||
|
return input_data
|
||||||
|
elif isinstance(input_data, list) and input_data:
|
||||||
|
last_msg = input_data[-1]
|
||||||
|
if isinstance(last_msg, dict):
|
||||||
|
return last_msg.get("content", str(last_msg))
|
||||||
|
return str(last_msg)
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_response_preview(response: Response) -> str:
|
||||||
|
"""Extract response preview text for tracing."""
|
||||||
|
if response.output:
|
||||||
|
for item in response.output:
|
||||||
|
if hasattr(item, 'content'):
|
||||||
|
for content in item.content:
|
||||||
|
if hasattr(content, 'text'):
|
||||||
|
return content.text[:200]
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
async def _execute_single_delegation(
|
async def _execute_single_delegation(
|
||||||
agent_name: str,
|
agent_name: str,
|
||||||
task: str,
|
task: str,
|
||||||
@@ -405,45 +430,88 @@ async def create_response(request: ResponseRequest) -> Response:
|
|||||||
# Get or generate conversation ID
|
# Get or generate conversation ID
|
||||||
conversation_id = await _conversation_history.get_conversation_id(request)
|
conversation_id = await _conversation_history.get_conversation_id(request)
|
||||||
|
|
||||||
# Strip pipeline prefix if present (e.g., "pipeline.model" -> "model")
|
# Set context for tracing
|
||||||
model_id = request.model
|
effective_user = request.user or get_default_user()
|
||||||
if "." in model_id:
|
current_user.set(effective_user)
|
||||||
model_id = model_id.split(".", 1)[1]
|
current_conversation.set(conversation_id)
|
||||||
|
|
||||||
# Get agent for model
|
# Extract user input for tracing
|
||||||
agent = ModelRegistry.get_agent(model_id)
|
user_input = _extract_user_input(request.input)
|
||||||
|
|
||||||
# Collect all output items from agent
|
# Start trace
|
||||||
output_items = []
|
trace = start_trace(
|
||||||
async for item in agent.generate_response(
|
conversation_id=conversation_id,
|
||||||
messages=request.input,
|
user=effective_user,
|
||||||
reasoning=request.reasoning,
|
request={
|
||||||
tools=request.tools,
|
"model": request.model,
|
||||||
temperature=request.temperature,
|
"input_preview": user_input[:200] if user_input else "",
|
||||||
max_tokens=request.max_output_tokens,
|
"full_input": request.input,
|
||||||
stop=request.stop,
|
"streaming": False,
|
||||||
):
|
},
|
||||||
output_items.append(item)
|
|
||||||
|
|
||||||
# Convert agent OutputItems to schema OutputItems
|
|
||||||
converted_items = _convert_output_items(output_items)
|
|
||||||
|
|
||||||
# Calculate token usage
|
|
||||||
usage = _calculate_usage(request.input, output_items)
|
|
||||||
|
|
||||||
response = Response(
|
|
||||||
id=f"resp_{generate_id()}",
|
|
||||||
created_at=int(time.time()),
|
|
||||||
model=request.model,
|
|
||||||
status="completed",
|
|
||||||
output=converted_items,
|
|
||||||
usage=usage
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Track conversation history (for analytics and future vector memory)
|
# Start service span
|
||||||
await _conversation_history.add_response(conversation_id, response)
|
service_span = start_span(
|
||||||
|
"create_response",
|
||||||
|
SpanType.ROUTER,
|
||||||
|
metadata={"model": request.model, "user": effective_user},
|
||||||
|
)
|
||||||
|
|
||||||
return response
|
try:
|
||||||
|
# Strip pipeline prefix if present (e.g., "pipeline.model" -> "model")
|
||||||
|
model_id = request.model
|
||||||
|
if "." in model_id:
|
||||||
|
model_id = model_id.split(".", 1)[1]
|
||||||
|
|
||||||
|
# Get agent for model
|
||||||
|
agent = ModelRegistry.get_agent(model_id)
|
||||||
|
|
||||||
|
# Collect all output items from agent
|
||||||
|
output_items = []
|
||||||
|
async for item in agent.generate_response(
|
||||||
|
messages=request.input,
|
||||||
|
reasoning=request.reasoning,
|
||||||
|
tools=request.tools,
|
||||||
|
temperature=request.temperature,
|
||||||
|
max_tokens=request.max_output_tokens,
|
||||||
|
stop=request.stop,
|
||||||
|
):
|
||||||
|
output_items.append(item)
|
||||||
|
|
||||||
|
# Convert agent OutputItems to schema OutputItems
|
||||||
|
converted_items = _convert_output_items(output_items)
|
||||||
|
|
||||||
|
# Calculate token usage
|
||||||
|
usage = _calculate_usage(request.input, output_items)
|
||||||
|
|
||||||
|
response = Response(
|
||||||
|
id=f"resp_{generate_id()}",
|
||||||
|
created_at=int(time.time()),
|
||||||
|
model=request.model,
|
||||||
|
status="completed",
|
||||||
|
output=converted_items,
|
||||||
|
usage=usage
|
||||||
|
)
|
||||||
|
|
||||||
|
# Track conversation history (for analytics and future vector memory)
|
||||||
|
await _conversation_history.add_response(conversation_id, response)
|
||||||
|
|
||||||
|
# End trace with response info
|
||||||
|
response_preview = _extract_response_preview(response)
|
||||||
|
end_trace(
|
||||||
|
response={
|
||||||
|
"output_preview": response_preview,
|
||||||
|
"output_count": len(response.output) if response.output else 0,
|
||||||
|
"status": response.status,
|
||||||
|
},
|
||||||
|
status="completed",
|
||||||
|
)
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
end_trace(status="error")
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
async def create_response_with_steward(request: ResponseRequest) -> Response:
|
async def create_response_with_steward(request: ResponseRequest) -> Response:
|
||||||
@@ -454,7 +522,7 @@ async def create_response_with_steward(request: ResponseRequest) -> Response:
|
|||||||
1. Steward analyzes the request and recommends capabilities
|
1. Steward analyzes the request and recommends capabilities
|
||||||
2. Phase 1: Tatlock orchestrates tool calls and expert delegations
|
2. Phase 1: Tatlock orchestrates tool calls and expert delegations
|
||||||
3. Phase 2: Tatlock synthesizes butler-toned response from results
|
3. Phase 2: Tatlock synthesizes butler-toned response from results
|
||||||
4. Tool usage is tracked for benchmarking
|
4. Tool usage is tracked for analysis
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
request: Response request
|
request: Response request
|
||||||
@@ -473,133 +541,176 @@ async def create_response_with_steward(request: ResponseRequest) -> Response:
|
|||||||
# Get or generate conversation ID
|
# Get or generate conversation ID
|
||||||
conversation_id = await _conversation_history.get_conversation_id(request)
|
conversation_id = await _conversation_history.get_conversation_id(request)
|
||||||
|
|
||||||
# Extract user message and conversation history
|
# Set context for tracing
|
||||||
user_message = ""
|
effective_user = request.user or get_default_user()
|
||||||
for msg in reversed(request.input):
|
current_user.set(effective_user)
|
||||||
if msg.get("role") == "user":
|
current_conversation.set(conversation_id)
|
||||||
user_message = msg.get("content", "")
|
|
||||||
break
|
|
||||||
|
|
||||||
# Conversation history is all messages except the current one
|
# Extract user input for tracing
|
||||||
conversation_history = request.input[:-1] if len(request.input) > 1 else []
|
user_input = _extract_user_input(request.input)
|
||||||
|
|
||||||
logger.info(
|
# Start trace
|
||||||
"creating_response_with_steward",
|
trace = start_trace(
|
||||||
user_message_preview=user_message[:100],
|
|
||||||
history_length=len(conversation_history),
|
|
||||||
conversation_id=conversation_id,
|
conversation_id=conversation_id,
|
||||||
|
user=effective_user,
|
||||||
|
request={
|
||||||
|
"model": request.model,
|
||||||
|
"input_preview": user_input[:200] if user_input else "",
|
||||||
|
"full_input": request.input,
|
||||||
|
"streaming": False,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
# Steward preprocessing
|
# Start service span
|
||||||
enriched = await preprocess_request(
|
service_span = start_span(
|
||||||
user_message,
|
"create_response_with_steward",
|
||||||
conversation_history=conversation_history,
|
SpanType.ROUTER,
|
||||||
conversation_id=conversation_id,
|
metadata={"model": request.model, "user": effective_user},
|
||||||
)
|
)
|
||||||
|
|
||||||
# Initialize tool tracker
|
try:
|
||||||
tracker = ToolCallTracker(
|
# Extract user message and conversation history
|
||||||
recommended_capabilities=enriched.recommendation.recommended_capabilities,
|
user_message = ""
|
||||||
conversation_id=conversation_id,
|
for msg in reversed(request.input):
|
||||||
)
|
if msg.get("role") == "user":
|
||||||
|
user_message = msg.get("content", "")
|
||||||
|
break
|
||||||
|
|
||||||
# Check if direct delegation is recommended
|
# Conversation history is all messages except the current one
|
||||||
# If Steward recommends ONLY delegation agents (biographer/librarian/housekeeper),
|
conversation_history = request.input[:-1] if len(request.input) > 1 else []
|
||||||
# we still use two-phase but delegate directly in Phase 1
|
|
||||||
delegation_agents = {"biographer", "librarian", "housekeeper"}
|
|
||||||
delegation_only = all(
|
|
||||||
cap in delegation_agents
|
|
||||||
for cap in enriched.recommendation.recommended_capabilities
|
|
||||||
) and enriched.recommendation.recommended_capabilities
|
|
||||||
|
|
||||||
from src.agents.tatlock import TatlockAgent
|
logger.info(
|
||||||
tatlock = TatlockAgent()
|
"creating_response_with_steward",
|
||||||
|
user_message_preview=user_message[:100],
|
||||||
# Use enriched query (with location/timezone context) if available
|
history_length=len(conversation_history),
|
||||||
effective_query = enriched.recommendation.enriched_query or user_message
|
conversation_id=conversation_id,
|
||||||
|
|
||||||
if delegation_only:
|
|
||||||
# Direct delegation path - collect results then synthesize
|
|
||||||
orchestration_results = await _direct_delegation_with_results(
|
|
||||||
effective_query, enriched.recommendation, tracker, conversation_id
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
# Phase 1: Orchestrate tool calls
|
|
||||||
orchestration_results = await tatlock.orchestrate_tool_calls(
|
|
||||||
user_message=effective_query,
|
|
||||||
steward_note=enriched.steward_note,
|
|
||||||
scoped_tools=enriched.scoped_tools,
|
|
||||||
message_history=conversation_history,
|
|
||||||
tool_tracker=tracker,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Handle text-based delegation fallback if present
|
# Steward preprocessing
|
||||||
if "[DELEGATE:" in orchestration_results.get("raw_output", ""):
|
enriched = await preprocess_request(
|
||||||
text_delegation_results = await _handle_text_delegation(
|
user_message,
|
||||||
orchestration_results["raw_output"], tracker, conversation_id
|
conversation_history=conversation_history,
|
||||||
|
conversation_id=conversation_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Initialize tool tracker
|
||||||
|
tracker = ToolCallTracker(
|
||||||
|
recommended_capabilities=enriched.recommendation.recommended_capabilities,
|
||||||
|
conversation_id=conversation_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check if direct delegation is recommended
|
||||||
|
# If Steward recommends ONLY delegation agents (biographer/librarian/housekeeper),
|
||||||
|
# we still use two-phase but delegate directly in Phase 1
|
||||||
|
delegation_agents = {"biographer", "librarian", "housekeeper"}
|
||||||
|
delegation_only = all(
|
||||||
|
cap in delegation_agents
|
||||||
|
for cap in enriched.recommendation.recommended_capabilities
|
||||||
|
) and enriched.recommendation.recommended_capabilities
|
||||||
|
|
||||||
|
from src.agents.tatlock import TatlockAgent
|
||||||
|
tatlock = TatlockAgent()
|
||||||
|
|
||||||
|
# Use enriched query (with location/timezone context) if available
|
||||||
|
effective_query = enriched.recommendation.enriched_query or user_message
|
||||||
|
|
||||||
|
if delegation_only:
|
||||||
|
# Direct delegation path - collect results then synthesize
|
||||||
|
orchestration_results = await _direct_delegation_with_results(
|
||||||
|
effective_query, enriched.recommendation, tracker, conversation_id
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Phase 1: Orchestrate tool calls
|
||||||
|
orchestration_results = await tatlock.orchestrate_tool_calls(
|
||||||
|
user_message=effective_query,
|
||||||
|
steward_note=enriched.steward_note,
|
||||||
|
scoped_tools=enriched.scoped_tools,
|
||||||
|
message_history=conversation_history,
|
||||||
|
tool_tracker=tracker,
|
||||||
)
|
)
|
||||||
# Add text delegation results to expert_results
|
|
||||||
if text_delegation_results != orchestration_results["raw_output"]:
|
|
||||||
orchestration_results["expert_results"]["text_delegation"] = text_delegation_results
|
|
||||||
|
|
||||||
# Phase 2: Synthesize butler-toned response from all results
|
# Handle text-based delegation fallback if present
|
||||||
tatlock_response = await tatlock.synthesize_from_results(
|
if "[DELEGATE:" in orchestration_results.get("raw_output", ""):
|
||||||
user_message=user_message,
|
text_delegation_results = await _handle_text_delegation(
|
||||||
orchestration_results=orchestration_results,
|
orchestration_results["raw_output"], tracker, conversation_id
|
||||||
message_history=conversation_history,
|
)
|
||||||
)
|
# Add text delegation results to expert_results
|
||||||
|
if text_delegation_results != orchestration_results["raw_output"]:
|
||||||
|
orchestration_results["expert_results"]["text_delegation"] = text_delegation_results
|
||||||
|
|
||||||
# Finalize tool tracking
|
# Phase 2: Synthesize butler-toned response from all results
|
||||||
await tracker.finalize()
|
tatlock_response = await tatlock.synthesize_from_results(
|
||||||
|
user_message=user_message,
|
||||||
|
orchestration_results=orchestration_results,
|
||||||
|
message_history=conversation_history,
|
||||||
|
)
|
||||||
|
|
||||||
# Build response output items
|
# Finalize tool tracking
|
||||||
output_items = []
|
await tracker.finalize()
|
||||||
|
|
||||||
# Add Steward reasoning as a reasoning output item
|
# Build response output items
|
||||||
output_items.append(ReasoningOutputItem(
|
output_items = []
|
||||||
id=f"reasoning_{generate_id()}",
|
|
||||||
summary=[
|
|
||||||
"🎩 Steward's Analysis:",
|
|
||||||
enriched.steward_reasoning,
|
|
||||||
],
|
|
||||||
status="completed"
|
|
||||||
))
|
|
||||||
|
|
||||||
# Add Tatlock's message
|
# Add Steward reasoning as a reasoning output item
|
||||||
output_items.append(MessageOutputItem(
|
output_items.append(ReasoningOutputItem(
|
||||||
id=f"msg_{generate_id()}",
|
id=f"reasoning_{generate_id()}",
|
||||||
role="assistant",
|
summary=[
|
||||||
content=[OutputTextContent(
|
"🎩 Steward's Analysis:",
|
||||||
type="output_text",
|
enriched.steward_reasoning,
|
||||||
text=tatlock_response,
|
],
|
||||||
annotations=[]
|
status="completed"
|
||||||
)],
|
))
|
||||||
status="completed"
|
|
||||||
))
|
|
||||||
|
|
||||||
# Calculate usage (approximate)
|
# Add Tatlock's message
|
||||||
usage = _calculate_usage(request.input, output_items)
|
output_items.append(MessageOutputItem(
|
||||||
|
id=f"msg_{generate_id()}",
|
||||||
|
role="assistant",
|
||||||
|
content=[OutputTextContent(
|
||||||
|
type="output_text",
|
||||||
|
text=tatlock_response,
|
||||||
|
annotations=[]
|
||||||
|
)],
|
||||||
|
status="completed"
|
||||||
|
))
|
||||||
|
|
||||||
response = Response(
|
# Calculate usage (approximate)
|
||||||
id=f"resp_{generate_id()}",
|
usage = _calculate_usage(request.input, output_items)
|
||||||
created_at=int(time.time()),
|
|
||||||
model=request.model,
|
|
||||||
status="completed",
|
|
||||||
output=output_items,
|
|
||||||
usage=usage
|
|
||||||
)
|
|
||||||
|
|
||||||
# Track conversation history
|
response = Response(
|
||||||
await _conversation_history.add_response(conversation_id, response)
|
id=f"resp_{generate_id()}",
|
||||||
|
created_at=int(time.time()),
|
||||||
|
model=request.model,
|
||||||
|
status="completed",
|
||||||
|
output=output_items,
|
||||||
|
usage=usage
|
||||||
|
)
|
||||||
|
|
||||||
logger.info(
|
# Track conversation history
|
||||||
"response_with_steward_complete",
|
await _conversation_history.add_response(conversation_id, response)
|
||||||
response_id=response.id,
|
|
||||||
recommended_capabilities=enriched.recommendation.recommended_capabilities,
|
|
||||||
tool_summary=tracker.get_summary(),
|
|
||||||
)
|
|
||||||
|
|
||||||
return response
|
logger.info(
|
||||||
|
"response_with_steward_complete",
|
||||||
|
response_id=response.id,
|
||||||
|
recommended_capabilities=enriched.recommendation.recommended_capabilities,
|
||||||
|
tool_summary=tracker.get_summary(),
|
||||||
|
)
|
||||||
|
|
||||||
|
# End trace with response info
|
||||||
|
response_preview = _extract_response_preview(response)
|
||||||
|
end_trace(
|
||||||
|
response={
|
||||||
|
"output_preview": response_preview,
|
||||||
|
"output_count": len(response.output) if response.output else 0,
|
||||||
|
"status": response.status,
|
||||||
|
},
|
||||||
|
status="completed",
|
||||||
|
)
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
end_trace(status="error")
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
async def create_response_stream(
|
async def create_response_stream(
|
||||||
|
|||||||
Reference in New Issue
Block a user