feat: implement Phase 2 two-tier architecture with Steward
Add comprehensive two-tier architecture where Steward analyzes requests and Tatlock executes with scoped tools. Includes full infrastructure for request preprocessing, tool tracking, benchmarking, and streaming. **Added:** - Steward agent for request analysis and capability recommendation - Household Registry for centralized capability management - Request preprocessing pipeline (Steward → Tatlock flow) - Tool usage tracking and benchmarking system - Streaming transparency (Steward reasoning visible in streams) - Structured logging with operation timing - Redis benchmark storage with 30-day expiry - Benchmark analysis CLI tools **Infrastructure:** - src/agents/steward/ - Steward agent implementation - src/agents/tatlock_core/ - Tatlock capability domain - src/core/preprocessing.py - Request preprocessing pipeline - src/core/tool_tracking.py - Tool call tracking - src/core/benchmarks.py - Benchmark recording system - src/core/household_registry.py - Capability registry - src/core/startup.py - Application startup coordination - src/core/logging_config.py - Structured logging setup **Integration:** - Responses API uses Steward for Tatlock requests - Chat Completions wraps Responses API for OpenAI compatibility - Streaming coordinator supports Steward + Tatlock flow - Tool scoping per request based on Steward recommendations **Testing:** - Integration tests for Steward-Tatlock flow - Benchmark and registry unit tests - Steward streaming tests See PHASE2_PLAN.md and PHASE2_COMPLETE.md for detailed documentation. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
+95
-91
@@ -9,7 +9,6 @@ import time
|
||||
import uuid
|
||||
from typing import AsyncGenerator
|
||||
|
||||
from src.agents.registry import ModelRegistry
|
||||
from src.chat import constants
|
||||
from src.chat.schemas import (
|
||||
ChatCompletionChunk,
|
||||
@@ -21,6 +20,8 @@ from src.chat.schemas import (
|
||||
ChatCompletionUsage,
|
||||
ChatMessage,
|
||||
)
|
||||
from src.responses.schemas import ResponseRequest
|
||||
from src.responses.service import create_response, create_response_with_steward
|
||||
|
||||
|
||||
async def create_chat_completion(
|
||||
@@ -41,49 +42,45 @@ async def create_chat_completion(
|
||||
completion_id = f"chatcmpl-{uuid.uuid4().hex[:24]}"
|
||||
created_at = int(time.time())
|
||||
|
||||
# Strip pipeline prefix if present
|
||||
model_id = request.model
|
||||
if "." in model_id:
|
||||
model_id = model_id.split(".", 1)[1]
|
||||
|
||||
# Get agent and generate response
|
||||
agent = ModelRegistry.get_agent(model_id)
|
||||
|
||||
# Convert Chat messages to Responses format
|
||||
# Convert Chat request to Responses request
|
||||
input_messages = [
|
||||
{"role": msg.role, "content": msg.content}
|
||||
for msg in request.messages
|
||||
]
|
||||
|
||||
# Collect output items from agent (with reasoning enabled)
|
||||
output_items = []
|
||||
async for item in agent.generate_response(
|
||||
messages=input_messages,
|
||||
response_request = ResponseRequest(
|
||||
model=request.model,
|
||||
input=input_messages,
|
||||
reasoning={"effort": "medium", "summary": "auto"}, # Enable reasoning
|
||||
temperature=request.temperature or 1.0,
|
||||
max_tokens=request.max_tokens,
|
||||
max_output_tokens=request.max_tokens,
|
||||
stop=request.stop if isinstance(request.stop, list) else ([request.stop] if request.stop else None),
|
||||
):
|
||||
output_items.append(item)
|
||||
)
|
||||
|
||||
# Build content with <think> tags
|
||||
# Call Responses API (will use Steward for Tatlock)
|
||||
model_id = request.model
|
||||
if "." in model_id:
|
||||
model_id = model_id.split(".", 1)[1]
|
||||
|
||||
use_steward = model_id.lower() == "tatlock"
|
||||
|
||||
if use_steward:
|
||||
response = await create_response_with_steward(response_request)
|
||||
else:
|
||||
response = await create_response(response_request)
|
||||
|
||||
# Convert Responses API output to Chat format
|
||||
content_parts = []
|
||||
|
||||
# Add reasoning as <think> blocks
|
||||
for item in output_items:
|
||||
for item in response.output:
|
||||
if item.type == "reasoning":
|
||||
reasoning_text = "\n".join(item.data.get("summary", []))
|
||||
reasoning_text = "\n".join(item.summary)
|
||||
content_parts.append(f"<think>\n{reasoning_text}\n</think>\n\n")
|
||||
elif item.type == "message":
|
||||
content_parts.append(item.data["content"][0]["text"])
|
||||
content_parts.append(item.content[0].text)
|
||||
|
||||
content = "".join(content_parts)
|
||||
|
||||
# Calculate token usage (approximate)
|
||||
prompt_text = " ".join(m.content for m in request.messages)
|
||||
prompt_tokens = len(prompt_text) // 4
|
||||
completion_tokens = len(content) // 4
|
||||
|
||||
return ChatCompletionResponse(
|
||||
id=completion_id,
|
||||
object=constants.CHAT_COMPLETION_OBJECT,
|
||||
@@ -100,9 +97,9 @@ async def create_chat_completion(
|
||||
)
|
||||
],
|
||||
usage=ChatCompletionUsage(
|
||||
prompt_tokens=prompt_tokens,
|
||||
completion_tokens=completion_tokens,
|
||||
total_tokens=prompt_tokens + completion_tokens,
|
||||
prompt_tokens=response.usage.input_tokens,
|
||||
completion_tokens=response.usage.output_tokens,
|
||||
total_tokens=response.usage.total_tokens,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -121,23 +118,34 @@ async def create_chat_completion_stream(
|
||||
Yields:
|
||||
Chat completion chunks with reasoning as <think> tags
|
||||
"""
|
||||
from src.responses.streaming import StreamingCoordinator, StreamEventType
|
||||
|
||||
completion_id = f"chatcmpl-{uuid.uuid4().hex[:24]}"
|
||||
created_at = int(time.time())
|
||||
|
||||
# Strip pipeline prefix if present
|
||||
model_id = request.model
|
||||
if "." in model_id:
|
||||
model_id = model_id.split(".", 1)[1]
|
||||
|
||||
# Get agent
|
||||
agent = ModelRegistry.get_agent(model_id)
|
||||
|
||||
# Convert Chat messages to Responses format
|
||||
# Convert Chat request to Responses request
|
||||
input_messages = [
|
||||
{"role": msg.role, "content": msg.content}
|
||||
for msg in request.messages
|
||||
]
|
||||
|
||||
response_request = ResponseRequest(
|
||||
model=request.model,
|
||||
input=input_messages,
|
||||
reasoning={"effort": "medium", "summary": "auto"},
|
||||
temperature=request.temperature or 1.0,
|
||||
max_output_tokens=request.max_tokens,
|
||||
stop=request.stop if isinstance(request.stop, list) else ([request.stop] if request.stop else None),
|
||||
stream=True,
|
||||
)
|
||||
|
||||
# Determine if we should use Steward
|
||||
model_id = request.model
|
||||
if "." in model_id:
|
||||
model_id = model_id.split(".", 1)[1]
|
||||
|
||||
use_steward = model_id.lower() == "tatlock"
|
||||
|
||||
# First chunk with role
|
||||
yield ChatCompletionChunk(
|
||||
id=completion_id,
|
||||
@@ -153,17 +161,18 @@ async def create_chat_completion_stream(
|
||||
],
|
||||
)
|
||||
|
||||
# Stream from agent with reasoning enabled
|
||||
# Stream from Responses API
|
||||
coordinator = StreamingCoordinator()
|
||||
in_reasoning = False
|
||||
async for item in agent.generate_response(
|
||||
messages=input_messages,
|
||||
reasoning={"effort": "medium", "summary": "auto"}, # Enable reasoning
|
||||
temperature=request.temperature or 1.0,
|
||||
max_tokens=request.max_tokens,
|
||||
stop=request.stop if isinstance(request.stop, list) else ([request.stop] if request.stop else None),
|
||||
):
|
||||
if item.type == "reasoning":
|
||||
# Start <think> block
|
||||
|
||||
if use_steward:
|
||||
stream_generator = coordinator.stream_response_with_steward(response_request)
|
||||
else:
|
||||
stream_generator = coordinator.stream_response(response_request)
|
||||
|
||||
async for event in stream_generator:
|
||||
if event.event == StreamEventType.REASONING_SUMMARY_DELTA:
|
||||
# Start <think> block if needed
|
||||
if not in_reasoning:
|
||||
yield ChatCompletionChunk(
|
||||
id=completion_id,
|
||||
@@ -180,24 +189,7 @@ async def create_chat_completion_stream(
|
||||
)
|
||||
in_reasoning = True
|
||||
|
||||
# Stream reasoning summary steps
|
||||
for step in item.data.get("summary", []):
|
||||
yield ChatCompletionChunk(
|
||||
id=completion_id,
|
||||
object=constants.CHAT_COMPLETION_CHUNK_OBJECT,
|
||||
created=created_at,
|
||||
model=request.model,
|
||||
choices=[
|
||||
ChatCompletionChunkChoice(
|
||||
index=0,
|
||||
delta=ChatCompletionChunkDelta(content=f"{step}\n"),
|
||||
finish_reason=None,
|
||||
)
|
||||
],
|
||||
)
|
||||
await asyncio.sleep(0.05) # Simulate typing
|
||||
|
||||
# Close <think> block
|
||||
# Stream reasoning delta
|
||||
yield ChatCompletionChunk(
|
||||
id=completion_id,
|
||||
object=constants.CHAT_COMPLETION_CHUNK_OBJECT,
|
||||
@@ -206,20 +198,15 @@ async def create_chat_completion_stream(
|
||||
choices=[
|
||||
ChatCompletionChunkChoice(
|
||||
index=0,
|
||||
delta=ChatCompletionChunkDelta(content="</think>\n\n"),
|
||||
delta=ChatCompletionChunkDelta(content=event.delta),
|
||||
finish_reason=None,
|
||||
)
|
||||
],
|
||||
)
|
||||
in_reasoning = False
|
||||
|
||||
elif item.type == "message":
|
||||
# Stream message content in chunks (preserves newlines, markdown, etc.)
|
||||
text = item.data["content"][0]["text"]
|
||||
chunk_size = 50 # characters per chunk
|
||||
|
||||
for i in range(0, len(text), chunk_size):
|
||||
chunk = text[i:i+chunk_size]
|
||||
elif event.event == StreamEventType.REASONING_SUMMARY_DONE:
|
||||
# Close <think> block
|
||||
if in_reasoning:
|
||||
yield ChatCompletionChunk(
|
||||
id=completion_id,
|
||||
object=constants.CHAT_COMPLETION_CHUNK_OBJECT,
|
||||
@@ -228,24 +215,41 @@ async def create_chat_completion_stream(
|
||||
choices=[
|
||||
ChatCompletionChunkChoice(
|
||||
index=0,
|
||||
delta=ChatCompletionChunkDelta(content=chunk),
|
||||
delta=ChatCompletionChunkDelta(content="</think>\n\n"),
|
||||
finish_reason=None,
|
||||
)
|
||||
],
|
||||
)
|
||||
await asyncio.sleep(0.02) # Faster since chunks are larger
|
||||
in_reasoning = False
|
||||
|
||||
# Final chunk with finish_reason
|
||||
yield ChatCompletionChunk(
|
||||
id=completion_id,
|
||||
object=constants.CHAT_COMPLETION_CHUNK_OBJECT,
|
||||
created=created_at,
|
||||
model=request.model,
|
||||
choices=[
|
||||
ChatCompletionChunkChoice(
|
||||
index=0,
|
||||
delta=ChatCompletionChunkDelta(),
|
||||
finish_reason=constants.FINISH_REASON_STOP,
|
||||
elif event.event == StreamEventType.OUTPUT_TEXT_DELTA:
|
||||
# Stream message content
|
||||
yield ChatCompletionChunk(
|
||||
id=completion_id,
|
||||
object=constants.CHAT_COMPLETION_CHUNK_OBJECT,
|
||||
created=created_at,
|
||||
model=request.model,
|
||||
choices=[
|
||||
ChatCompletionChunkChoice(
|
||||
index=0,
|
||||
delta=ChatCompletionChunkDelta(content=event.delta),
|
||||
finish_reason=None,
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
elif event.event == StreamEventType.RESPONSE_DONE:
|
||||
# Final chunk with finish_reason
|
||||
yield ChatCompletionChunk(
|
||||
id=completion_id,
|
||||
object=constants.CHAT_COMPLETION_CHUNK_OBJECT,
|
||||
created=created_at,
|
||||
model=request.model,
|
||||
choices=[
|
||||
ChatCompletionChunkChoice(
|
||||
index=0,
|
||||
delta=ChatCompletionChunkDelta(),
|
||||
finish_reason=constants.FINISH_REASON_STOP,
|
||||
)
|
||||
],
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user