style: apply ruff's automatic fixes and formatter
Mechanical only, and separated from the judgment calls that follow so the reviewable changes are not buried in a 98-file whitespace diff. 227 automatic fixes: 60 blank lines carrying whitespace, 60 unsorted import blocks, 34 Optional[X] to X | None, 28 unused imports, 16 deprecated typing imports, 12 datetime.timezone.utc to datetime.UTC, and assorted smaller modernisations. Then `ruff format` over src and tests: 98 files reformatted, 35 already conforming. No file among the unused-import findings defines __all__ or is an __init__.py, so nothing here removes a re-export. `make test`: 658 passed, unchanged from HEAD. Two things observed while verifying, neither addressed here: `pytest tests/` cannot collect — tests/e2e/test_orchestration_e2e.py uses an `e2e` marker that is not registered, and the config is strict about markers. This fails identically at HEAD, so it predates this change; `make test` passes because it ignores tests/e2e, tests/integration and tests/contracts. test_tatlock_tool_call_logging_calculator is flaky. It failed once in a full run with these changes and passed on the next, passes in isolation with them, and fails in isolation at HEAD. It is order- or timing-dependent, not a regression from this commit — established by running the full suite both ways rather than by reasoning about which change could have caused it. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
+68
-72
@@ -6,24 +6,25 @@ The agent embodies a witty, capable British butler personality.
|
||||
"""
|
||||
|
||||
import secrets
|
||||
from typing import AsyncGenerator, Any
|
||||
from collections.abc import AsyncGenerator
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
|
||||
from pydantic_ai import Agent, RunContext
|
||||
|
||||
from src.agents.base import AgentInterface, OutputItem
|
||||
from src.agents.tatlock_core.tools import (
|
||||
calculate,
|
||||
get_current_datetime,
|
||||
calculate_time_offset,
|
||||
get_current_datetime,
|
||||
time_difference,
|
||||
)
|
||||
from src.core.config import config
|
||||
from src.core.logging_config import get_logger
|
||||
from src.core.tracing import (
|
||||
start_span, end_span, get_current_span,
|
||||
SpanType,
|
||||
add_tool_spans_from_messages,
|
||||
SpanType, SpanStatus,
|
||||
end_span,
|
||||
start_span,
|
||||
)
|
||||
|
||||
logger = get_logger(__name__)
|
||||
@@ -32,6 +33,7 @@ logger = get_logger(__name__)
|
||||
@dataclass
|
||||
class ToolCallTracker:
|
||||
"""Tracks tool calls for reporting to reasoning output."""
|
||||
|
||||
calls: list[str] = field(default_factory=list)
|
||||
|
||||
def log_call(self, message: str):
|
||||
@@ -239,7 +241,9 @@ class TatlockAgent(AgentInterface):
|
||||
|
||||
# Time difference calculator
|
||||
@self._agent.tool
|
||||
def calculate_time_difference(ctx: RunContext[ToolCallTracker], date1_str: str, date2_str: str = "now") -> str:
|
||||
def calculate_time_difference(
|
||||
ctx: RunContext[ToolCallTracker], date1_str: str, date2_str: str = "now"
|
||||
) -> str:
|
||||
"""
|
||||
Calculate the difference between two dates.
|
||||
|
||||
@@ -251,7 +255,9 @@ class TatlockAgent(AgentInterface):
|
||||
Human-readable description of the time difference
|
||||
"""
|
||||
if ctx.deps:
|
||||
ctx.deps.log_call(f"🕐 Calculating time difference between {date1_str} and {date2_str}")
|
||||
ctx.deps.log_call(
|
||||
f"🕐 Calculating time difference between {date1_str} and {date2_str}"
|
||||
)
|
||||
return time_difference(date1_str, date2_str)
|
||||
|
||||
# NOTE: Web search has been moved to The Librarian agent.
|
||||
@@ -271,7 +277,7 @@ class TatlockAgent(AgentInterface):
|
||||
temperature: float = 1.0,
|
||||
max_tokens: int | None = None,
|
||||
stop: list[str] | None = None,
|
||||
**kwargs: Any
|
||||
**kwargs: Any,
|
||||
) -> AsyncGenerator[OutputItem, None]:
|
||||
"""
|
||||
Generate response using PydanticAI with Ollama.
|
||||
@@ -305,18 +311,20 @@ class TatlockAgent(AgentInterface):
|
||||
type="message",
|
||||
id=f"msg_{generate_id()}",
|
||||
role="assistant",
|
||||
content=[{
|
||||
"type": "output_text",
|
||||
"text": "I'm afraid I didn't receive a message, sir. How may I assist you?",
|
||||
"annotations": []
|
||||
}],
|
||||
status="completed"
|
||||
content=[
|
||||
{
|
||||
"type": "output_text",
|
||||
"text": "I'm afraid I didn't receive a message, sir. How may I assist you?",
|
||||
"annotations": [],
|
||||
}
|
||||
],
|
||||
status="completed",
|
||||
)
|
||||
return
|
||||
|
||||
# Build message history (all messages except the last user message)
|
||||
# PydanticAI expects history as list of ModelRequest/ModelResponse objects
|
||||
from pydantic_ai.messages import ModelRequest, ModelResponse, UserPromptPart, TextPart
|
||||
from pydantic_ai.messages import ModelRequest, ModelResponse, TextPart, UserPromptPart
|
||||
|
||||
message_history = []
|
||||
for i, msg in enumerate(messages[:-1]): # All messages except the last one
|
||||
@@ -330,7 +338,9 @@ class TatlockAgent(AgentInterface):
|
||||
|
||||
# Debug: Check for problematic content
|
||||
if '"' in content or "'" in content:
|
||||
logger.debug(f"Message {i} ({role}) contains quotes. Content preview: {content[:100]}...")
|
||||
logger.debug(
|
||||
f"Message {i} ({role}) contains quotes. Content preview: {content[:100]}..."
|
||||
)
|
||||
|
||||
# Convert to PydanticAI message format
|
||||
try:
|
||||
@@ -339,9 +349,7 @@ class TatlockAgent(AgentInterface):
|
||||
ModelRequest(parts=[UserPromptPart(content=content)])
|
||||
)
|
||||
elif role == "assistant":
|
||||
message_history.append(
|
||||
ModelResponse(parts=[TextPart(content=content)])
|
||||
)
|
||||
message_history.append(ModelResponse(parts=[TextPart(content=content)]))
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating message history item {i}: {e}")
|
||||
logger.error(f"Problematic content: {repr(content)}")
|
||||
@@ -352,7 +360,9 @@ class TatlockAgent(AgentInterface):
|
||||
if message_history:
|
||||
for i, hist_msg in enumerate(message_history):
|
||||
msg_type = type(hist_msg).__name__
|
||||
content_preview = str(hist_msg.parts[0].content)[:50] if hist_msg.parts else "no parts"
|
||||
content_preview = (
|
||||
str(hist_msg.parts[0].content)[:50] if hist_msg.parts else "no parts"
|
||||
)
|
||||
logger.info(f" History[{i}]: {msg_type} - {content_preview}...")
|
||||
|
||||
# Generate reasoning output if requested
|
||||
@@ -362,10 +372,10 @@ class TatlockAgent(AgentInterface):
|
||||
id=f"reasoning_{generate_id()}",
|
||||
summary=[
|
||||
"Analyzing your request, sir...",
|
||||
"Formulating response based on available knowledge..."
|
||||
"Formulating response based on available knowledge...",
|
||||
],
|
||||
thinking="", # PydanticAI doesn't expose internal reasoning yet
|
||||
status="completed"
|
||||
status="completed",
|
||||
)
|
||||
|
||||
# Create a tool call tracker for this request
|
||||
@@ -382,7 +392,7 @@ class TatlockAgent(AgentInterface):
|
||||
result = await self.agent.run(
|
||||
user_message,
|
||||
message_history=message_history if message_history else None,
|
||||
deps=tracker
|
||||
deps=tracker,
|
||||
)
|
||||
final_text = result.output
|
||||
|
||||
@@ -393,7 +403,7 @@ class TatlockAgent(AgentInterface):
|
||||
id=f"reasoning_tools_{generate_id()}",
|
||||
summary=tracker.calls,
|
||||
thinking="",
|
||||
status="completed"
|
||||
status="completed",
|
||||
)
|
||||
|
||||
# Yield the complete message
|
||||
@@ -402,12 +412,8 @@ class TatlockAgent(AgentInterface):
|
||||
type="message",
|
||||
id=msg_id,
|
||||
role="assistant",
|
||||
content=[{
|
||||
"type": "output_text",
|
||||
"text": final_text,
|
||||
"annotations": []
|
||||
}],
|
||||
status="completed"
|
||||
content=[{"type": "output_text", "text": final_text, "annotations": []}],
|
||||
status="completed",
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
@@ -416,12 +422,14 @@ class TatlockAgent(AgentInterface):
|
||||
type="message",
|
||||
id=f"msg_{generate_id()}",
|
||||
role="assistant",
|
||||
content=[{
|
||||
"type": "output_text",
|
||||
"text": f"My apologies, sir. I encountered an error: {str(e)}",
|
||||
"annotations": []
|
||||
}],
|
||||
status="failed"
|
||||
content=[
|
||||
{
|
||||
"type": "output_text",
|
||||
"text": f"My apologies, sir. I encountered an error: {str(e)}",
|
||||
"annotations": [],
|
||||
}
|
||||
],
|
||||
status="failed",
|
||||
)
|
||||
|
||||
async def supports_tools(self) -> bool:
|
||||
@@ -490,7 +498,7 @@ class TatlockAgent(AgentInterface):
|
||||
enriched_message = f"{steward_note}\n\n{user_message}"
|
||||
|
||||
# Convert message history to PydanticAI format
|
||||
from pydantic_ai.messages import ModelRequest, ModelResponse, UserPromptPart, TextPart
|
||||
from pydantic_ai.messages import ModelRequest, ModelResponse, TextPart, UserPromptPart
|
||||
|
||||
pydantic_history = []
|
||||
for msg in message_history:
|
||||
@@ -501,17 +509,14 @@ class TatlockAgent(AgentInterface):
|
||||
continue
|
||||
|
||||
if role == "user":
|
||||
pydantic_history.append(
|
||||
ModelRequest(parts=[UserPromptPart(content=content)])
|
||||
)
|
||||
pydantic_history.append(ModelRequest(parts=[UserPromptPart(content=content)]))
|
||||
elif role == "assistant":
|
||||
pydantic_history.append(
|
||||
ModelResponse(parts=[TextPart(content=content)])
|
||||
)
|
||||
pydantic_history.append(ModelResponse(parts=[TextPart(content=content)]))
|
||||
|
||||
# Run with scoped tools and tracker
|
||||
# Force tool_choice to make LLM actually call tools
|
||||
from src.anthropic.model_selector import get_tool_choice_settings
|
||||
|
||||
result = await scoped_agent.run(
|
||||
enriched_message,
|
||||
message_history=pydantic_history if pydantic_history else None,
|
||||
@@ -575,7 +580,7 @@ class TatlockAgent(AgentInterface):
|
||||
enriched_message = f"{steward_note}\n\n{user_message}"
|
||||
|
||||
# Convert message history to PydanticAI format
|
||||
from pydantic_ai.messages import ModelRequest, ModelResponse, UserPromptPart, TextPart
|
||||
from pydantic_ai.messages import ModelRequest, ModelResponse, TextPart, UserPromptPart
|
||||
|
||||
pydantic_history = []
|
||||
for msg in message_history:
|
||||
@@ -586,13 +591,9 @@ class TatlockAgent(AgentInterface):
|
||||
continue
|
||||
|
||||
if role == "user":
|
||||
pydantic_history.append(
|
||||
ModelRequest(parts=[UserPromptPart(content=content)])
|
||||
)
|
||||
pydantic_history.append(ModelRequest(parts=[UserPromptPart(content=content)]))
|
||||
elif role == "assistant":
|
||||
pydantic_history.append(
|
||||
ModelResponse(parts=[TextPart(content=content)])
|
||||
)
|
||||
pydantic_history.append(ModelResponse(parts=[TextPart(content=content)]))
|
||||
|
||||
# Use run() instead of run_stream() to avoid Ollama 400 bug
|
||||
# with streaming + tool calls (PydanticAI issues #1292, #2256)
|
||||
@@ -600,7 +601,7 @@ class TatlockAgent(AgentInterface):
|
||||
result = await scoped_agent.run(
|
||||
enriched_message,
|
||||
message_history=pydantic_history if pydantic_history else None,
|
||||
deps=tool_tracker
|
||||
deps=tool_tracker,
|
||||
)
|
||||
|
||||
# Stream the final response in chunks to maintain UX
|
||||
@@ -608,7 +609,7 @@ class TatlockAgent(AgentInterface):
|
||||
chunk_size = 50 # characters per chunk
|
||||
|
||||
for i in range(0, len(response_text), chunk_size):
|
||||
yield response_text[i:i + chunk_size]
|
||||
yield response_text[i : i + chunk_size]
|
||||
|
||||
logger.info("tatlock_scoped_run_complete")
|
||||
|
||||
@@ -643,11 +644,12 @@ class TatlockAgent(AgentInterface):
|
||||
from pydantic_ai.messages import (
|
||||
ModelRequest,
|
||||
ModelResponse,
|
||||
UserPromptPart,
|
||||
TextPart,
|
||||
ToolCallPart,
|
||||
ToolReturnPart,
|
||||
UserPromptPart,
|
||||
)
|
||||
|
||||
from src.anthropic.model_selector import get_model
|
||||
|
||||
logger.info(
|
||||
@@ -663,7 +665,7 @@ class TatlockAgent(AgentInterface):
|
||||
SpanType.TATLOCK,
|
||||
metadata={
|
||||
"scoped_tool_count": len(scoped_tools),
|
||||
"tool_names": [getattr(t, '__name__', str(t)) for t in scoped_tools[:5]],
|
||||
"tool_names": [getattr(t, "__name__", str(t)) for t in scoped_tools[:5]],
|
||||
},
|
||||
)
|
||||
|
||||
@@ -690,16 +692,13 @@ class TatlockAgent(AgentInterface):
|
||||
continue
|
||||
|
||||
if role == "user":
|
||||
pydantic_history.append(
|
||||
ModelRequest(parts=[UserPromptPart(content=content)])
|
||||
)
|
||||
pydantic_history.append(ModelRequest(parts=[UserPromptPart(content=content)]))
|
||||
elif role == "assistant":
|
||||
pydantic_history.append(
|
||||
ModelResponse(parts=[TextPart(content=content)])
|
||||
)
|
||||
pydantic_history.append(ModelResponse(parts=[TextPart(content=content)]))
|
||||
|
||||
# Run with scoped tools and tracker
|
||||
from src.anthropic.model_selector import get_tool_choice_settings
|
||||
|
||||
result = await scoped_agent.run(
|
||||
enriched_message,
|
||||
message_history=pydantic_history if pydantic_history else None,
|
||||
@@ -782,7 +781,8 @@ class TatlockAgent(AgentInterface):
|
||||
Returns:
|
||||
str: Butler-toned response synthesized from all results
|
||||
"""
|
||||
from pydantic_ai.messages import ModelRequest, ModelResponse, UserPromptPart, TextPart
|
||||
from pydantic_ai.messages import ModelRequest, ModelResponse, TextPart, UserPromptPart
|
||||
|
||||
from src.anthropic.model_selector import get_model
|
||||
|
||||
logger.info(
|
||||
@@ -849,13 +849,9 @@ class TatlockAgent(AgentInterface):
|
||||
continue
|
||||
|
||||
if role == "user":
|
||||
pydantic_history.append(
|
||||
ModelRequest(parts=[UserPromptPart(content=content)])
|
||||
)
|
||||
pydantic_history.append(ModelRequest(parts=[UserPromptPart(content=content)]))
|
||||
elif role == "assistant":
|
||||
pydantic_history.append(
|
||||
ModelResponse(parts=[TextPart(content=content)])
|
||||
)
|
||||
pydantic_history.append(ModelResponse(parts=[TextPart(content=content)]))
|
||||
|
||||
# Run synthesis
|
||||
result = await synthesis_agent.run(
|
||||
@@ -885,9 +881,9 @@ class TatlockAgent(AgentInterface):
|
||||
async def get_capabilities(self) -> dict:
|
||||
"""Return current capabilities."""
|
||||
return {
|
||||
"streaming": True, # Streaming implemented
|
||||
"reasoning": True, # Basic reasoning summaries
|
||||
"tools": True, # Permanent tools: calculator, date/time, search
|
||||
"vision": False, # Future
|
||||
"audio": False, # Future
|
||||
"streaming": True, # Streaming implemented
|
||||
"reasoning": True, # Basic reasoning summaries
|
||||
"tools": True, # Permanent tools: calculator, date/time, search
|
||||
"vision": False, # Future
|
||||
"audio": False, # Future
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user