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:
2026-08-11 17:25:18 +02:00
co-authored by Claude
parent 57fa6c13fc
commit 78066fab1b
103 changed files with 1601 additions and 1749 deletions
+68 -72
View File
@@ -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
}