logic-engine / tests /test_pydantic_ai_integration.py
ghostdrive1's picture
Upload folder using huggingface_hub
116524e verified
Raw
History Blame Contribute Delete
19.2 kB
"""Integration tests for PydanticAI-backed ACE roles with real API calls.
Requires AWS credentials for Bedrock access.
Run with: uv run pytest tests/test_pydantic_ai_integration.py -v -s --no-cov
"""
from __future__ import annotations
import os
import pytest
from dotenv import load_dotenv
load_dotenv()
# Skip entire module if no API credentials
pytestmark = pytest.mark.requires_api
HAS_API = bool(os.environ.get("OPENAI_API_KEY"))
if not HAS_API:
pytest.skip("OPENAI_API_KEY not set", allow_module_level=True)
from ace.core.outputs import (
AgentOutput,
ReflectorOutput,
SkillManagerOutput,
)
from ace.core.skillbook import Skillbook, UpdateBatch
from ace.implementations import Agent, Reflector, SkillManager
from ace.runners.litellm import ACELiteLLM
from ace.core.environments import Sample, SimpleEnvironment
MODEL = "bedrock/us.anthropic.claude-haiku-4-5-20251001-v1:0"
class TestAgentRole:
"""Test Agent role produces valid structured output."""
def test_basic_question(self):
agent = Agent(MODEL)
sb = Skillbook()
output = agent.generate(
question="What is the capital of France?",
context="Answer in one word.",
skillbook=sb,
)
assert isinstance(output, AgentOutput)
assert len(output.reasoning) > 0, "reasoning should be non-empty"
assert len(output.final_answer) > 0, "final_answer should be non-empty"
assert (
"paris" in output.final_answer.lower()
), f"Expected 'Paris' in answer, got: {output.final_answer}"
assert isinstance(output.skill_ids, list)
assert "usage" in output.raw, f"raw should contain usage, got: {output.raw}"
assert output.raw["usage"]["prompt_tokens"] > 0
assert output.raw["usage"]["completion_tokens"] > 0
print(f"\n Agent answer: {output.final_answer}")
print(f" Usage: {output.raw['usage']}")
def test_with_skillbook(self):
agent = Agent(MODEL)
sb = Skillbook()
sb.add_skill(
"math",
"Use decomposition: break large multiplications into (a*10 + b) parts",
skill_id="math-001",
)
output = agent.generate(
question="What is 17 × 23?",
context="Show your work step by step.",
skillbook=sb,
)
assert isinstance(output, AgentOutput)
assert len(output.final_answer) > 0
assert (
"391" in output.final_answer
), f"Expected '391' in answer, got: {output.final_answer}"
print(f"\n Agent answer: {output.final_answer}")
print(f" Reasoning excerpt: {output.reasoning[:300]}...")
print(f" Cited skills: {output.skill_ids}")
def test_with_reflection(self):
agent = Agent(MODEL)
sb = Skillbook()
sb.add_skill(
"physics",
"Always include the unit when stating temperatures",
skill_id="phys-001",
)
output = agent.generate(
question="What temperature does water boil at in Fahrenheit? Reply with just the number and unit.",
context="This is a factual science question. Answer concisely.",
skillbook=sb,
reflection="Your previous answer was incorrect. The correct answer is 212°F at standard atmospheric pressure.",
)
assert isinstance(output, AgentOutput)
assert len(output.final_answer) > 0
# The reflection explicitly states 212°F — verify the model uses it
full_text = f"{output.final_answer} {output.reasoning}"
assert (
"212" in full_text
), f"Expected '212' somewhere in output, got answer: {output.final_answer}"
print(f"\n Agent answer with reflection: {output.final_answer}")
class TestReflectorRole:
"""Test Reflector role produces valid structured analysis."""
def test_correct_answer_reflection(self):
reflector = Reflector(MODEL)
sb = Skillbook()
sb.add_skill("math", "Break down multiplication", skill_id="math-001")
agent_output = AgentOutput(
reasoning="Following [math-001], I decomposed 15×24 as 15×20 + 15×4 = 300 + 60 = 360",
final_answer="360",
skill_ids=["math-001"],
)
output = reflector.reflect(
question="What is 15 × 24?",
agent_output=agent_output,
skillbook=sb,
ground_truth="360",
feedback="Correct!",
)
assert isinstance(output, ReflectorOutput)
assert len(output.reasoning) > 0
assert len(output.correct_approach) > 0
assert len(output.key_insight) > 0
assert "usage" in output.raw
print(f"\n Key insight: {output.key_insight}")
def test_wrong_answer_reflection(self):
reflector = Reflector(MODEL)
sb = Skillbook()
agent_output = AgentOutput(
reasoning="I calculated 15×24 = 15×20 + 15×4 = 310 + 60 = 370",
final_answer="370",
)
output = reflector.reflect(
question="What is 15 × 24?",
agent_output=agent_output,
skillbook=sb,
ground_truth="360",
feedback="Incorrect. The answer is 360.",
)
assert isinstance(output, ReflectorOutput)
assert len(output.error_identification) > 0, "Should identify the error"
assert len(output.root_cause_analysis) > 0, "Should analyze root cause"
print(f"\n Error identified: {output.error_identification[:200]}")
print(f" Root cause: {output.root_cause_analysis[:200]}")
print(f" Key insight: {output.key_insight[:200]}")
class TestSkillManagerRole:
"""Test SkillManager role produces valid skillbook updates."""
def test_add_new_skill(self):
sm = SkillManager(MODEL)
sb = Skillbook()
reflection = ReflectorOutput(
reasoning="The agent failed because it didn't decompose the problem",
error_identification="Tried to multiply directly without decomposition",
root_cause_analysis="Missing strategy for breaking down multiplication",
correct_approach="Use decomposition: 15×24 = 15×(20+4) = 300+60 = 360",
key_insight="Break large multiplications into manageable parts",
)
output = sm.update_skills(
reflections=(reflection,),
skillbook=sb,
question_context="Mental arithmetic",
progress="0/1 correct",
)
assert isinstance(output, SkillManagerOutput)
assert isinstance(output.update, UpdateBatch)
assert len(output.update.reasoning) > 0
assert "usage" in output.raw
print(f"\n Reasoning: {output.update.reasoning[:200]}")
print(f" Operations: {len(output.update.operations)}")
for op in output.update.operations:
print(
f" {op.type}: {(op.insight or op.issue)[:80] if (op.insight or op.issue) else 'N/A'}"
)
def test_tag_existing_skill(self):
sm = SkillManager(MODEL)
sb = Skillbook()
sb.add_skill(
"math",
"Use decomposition for multiplication",
skill_id="math-001",
)
reflection = ReflectorOutput(
reasoning="The agent correctly applied decomposition strategy",
correct_approach="Decomposition worked well",
key_insight="Decomposition strategy is effective",
)
output = sm.update_skills(
reflections=(reflection,),
skillbook=sb,
question_context="Mental arithmetic",
progress="1/1 correct",
)
assert isinstance(output, SkillManagerOutput)
print(f"\n Operations: {len(output.update.operations)}")
for op in output.update.operations:
print(
f" {op.type} {op.skill_id or ''}: "
f"{(op.insight or op.issue) or op.metadata}"
)
class TestACELiteLLMIntegration:
"""Test the full ACELiteLLM flow with real API calls."""
def test_ask(self):
ace = ACELiteLLM.from_model(MODEL)
answer = ace.ask("What is 2 + 2?")
assert "4" in answer, f"Expected '4' in answer, got: {answer}"
print(f"\n ask() answer: {answer}")
def test_ask_and_learn_from_feedback(self):
ace = ACELiteLLM.from_model(MODEL)
answer = ace.ask("What is the chemical symbol for gold?")
print(f"\n Answer: {answer}")
assert len(answer) > 0
result = ace.learn_from_feedback(
feedback="Correct! Gold's symbol Au comes from the Latin 'aurum'.",
ground_truth="Au",
)
assert result is True, "learn_from_feedback should return True"
print(f" Skills after learning: {len(ace.skillbook.skills())}")
for skill in ace.skillbook.skills():
print(f" [{skill.id}] {skill.content[:80]}")
def test_full_learning_pipeline(self):
"""End-to-end: learn from samples, verify skillbook grows."""
ace = ACELiteLLM.from_model(MODEL)
env = SimpleEnvironment()
samples = [
Sample(
question="What is the speed of light in km/s?",
ground_truth="approximately 300,000 km/s",
),
]
results = ace.learn(samples, environment=env)
assert len(results) == 1
assert results[0].error is None, f"Pipeline error: {results[0].error}"
print(f"\n Pipeline completed. Skills: {len(ace.skillbook.skills())}")
for skill in ace.skillbook.skills():
print(f" [{skill.id}] {skill.content[:80]}")
def test_save_and_load_after_learning(self, tmp_path):
"""Skills survive save/load cycle."""
ace = ACELiteLLM.from_model(MODEL)
answer = ace.ask("What is H2O?")
ace.learn_from_feedback("Correct!", ground_truth="Water")
path = str(tmp_path / "skillbook.json")
skills_before = len(ace.skillbook.skills())
ace.save(path)
ace2 = ACELiteLLM.from_model(MODEL, skillbook_path=path)
assert len(ace2.skillbook.skills()) == skills_before
print(f"\n Saved and loaded {skills_before} skills successfully")
class TestRetryAndConsistency:
"""Test structured output consistency across multiple calls."""
def test_structured_output_consistency(self):
"""Multiple calls should always produce valid structured output."""
agent = Agent(MODEL)
sb = Skillbook()
questions = [
("What is 7 × 8?", "56"),
("What is the capital of Japan?", "Tokyo"),
("Who wrote Romeo and Juliet?", "Shakespeare"),
]
for q, expected in questions:
output = agent.generate(
question=q,
context="Answer concisely.",
skillbook=sb,
)
assert isinstance(output, AgentOutput), f"Wrong type for '{q}'"
assert len(output.reasoning) > 0, f"Empty reasoning for '{q}'"
assert len(output.final_answer) > 0, f"Empty answer for '{q}'"
assert isinstance(output.raw, dict), f"raw not dict for '{q}'"
assert "usage" in output.raw, f"No usage in raw for '{q}'"
assert (
expected.lower() in output.final_answer.lower()
), f"Expected '{expected}' in answer for '{q}', got: {output.final_answer}"
print(f"\n Q: {q} -> A: {output.final_answer}")
class TestRRStepIntegration:
"""Test the PydanticAI-based Recursive Reflector (RRStep) with real API calls."""
RR_MODEL = "bedrock/us.anthropic.claude-haiku-4-5-20251001-v1:0"
@pytest.mark.integration
def test_rr_basic_reflection(self):
"""RRStep.reflect returns a valid ReflectorOutput with non-empty fields."""
from ace.steps.rr_step import RRStep
from ace.implementations.rr.config import RecursiveConfig
config = RecursiveConfig(
max_requests=15,
timeout=15.0,
)
rr = RRStep(model=self.RR_MODEL, config=config)
sb = Skillbook()
agent_out = AgentOutput(
reasoning="I recall that the capital of Australia is Sydney because it is the largest city.",
final_answer="Sydney",
skill_ids=[],
)
output = rr.reflect(
question="What is the capital of Australia?",
agent_output=agent_out,
skillbook=sb,
ground_truth="Canberra",
feedback="Incorrect. The capital of Australia is Canberra, not Sydney.",
)
assert isinstance(
output, ReflectorOutput
), f"Expected ReflectorOutput, got {type(output)}"
assert len(output.reasoning) > 0, "reasoning should be non-empty"
assert len(output.key_insight) > 0, "key_insight should be non-empty"
assert isinstance(output.raw, dict), "raw should be a dict"
# Verify rr_trace metadata is populated
rr_trace = output.raw.get("rr_trace", {})
assert isinstance(rr_trace, dict), "rr_trace should be a dict in raw"
assert "total_iterations" in rr_trace, "rr_trace should have total_iterations"
print(f"\n Reasoning: {output.reasoning[:300]}")
print(f" Key insight: {output.key_insight[:200]}")
print(f" RR trace: {rr_trace}")
@pytest.mark.integration
def test_rr_with_skillbook(self):
"""RRStep.reflect with a populated skillbook references or tags skills."""
from ace.steps.rr_step import RRStep
from ace.implementations.rr.config import RecursiveConfig
config = RecursiveConfig(
max_requests=25,
timeout=15.0,
)
rr = RRStep(model=self.RR_MODEL, config=config)
sb = Skillbook()
sb.add_skill(
"geography",
"Always verify capital cities — the largest city is often not the capital",
skill_id="geo-001",
)
agent_out = AgentOutput(
reasoning="The largest city in Brazil is Sao Paulo, so it must be the capital.",
final_answer="Sao Paulo",
skill_ids=[],
)
output = rr.reflect(
question="What is the capital of Brazil?",
agent_output=agent_out,
skillbook=sb,
ground_truth="Brasilia",
feedback="Incorrect. The capital of Brazil is Brasilia.",
)
assert isinstance(output, ReflectorOutput)
assert len(output.reasoning) > 0
assert len(output.key_insight) > 0
# The RR should produce a meaningful analysis referencing the
# capital city error.
full_text = f"{output.reasoning} {output.key_insight}"
has_analysis = (
"capital" in full_text.lower()
or "largest" in full_text.lower()
or "brasilia" in full_text.lower()
)
assert has_analysis, (
"Expected the reflector to analyze the capital city error. "
f"reasoning={output.reasoning[:200]}"
)
print(f"\n Key insight: {output.key_insight[:200]}")
@pytest.mark.integration
def test_rr_step_protocol(self):
"""RRStep used as a StepProtocol: __call__(ctx) populates reflections."""
from ace.steps.rr_step import RRStep
from ace.implementations.rr.config import RecursiveConfig
from ace.core.context import ACEStepContext, SkillbookView
config = RecursiveConfig(
max_requests=15,
timeout=15.0,
)
rr = RRStep(model=self.RR_MODEL, config=config)
sb = Skillbook()
trace = {
"question": "What is 15 x 24?",
"ground_truth": "360",
"feedback": "Incorrect. The correct answer is 360.",
"steps": [
{
"role": "agent",
"reasoning": "15 x 24 = 15 x 20 + 15 x 4 = 310 + 60 = 370",
"answer": "370",
"skill_ids": [],
},
],
}
ctx = ACEStepContext(
trace=trace,
skillbook=SkillbookView(sb),
)
result_ctx = rr(ctx)
assert result_ctx.reflections is not None, "reflections should be set"
assert len(result_ctx.reflections) > 0, "reflections should be non-empty"
for reflection in result_ctx.reflections:
assert isinstance(reflection, ReflectorOutput)
assert len(reflection.reasoning) > 0
print(f"\n Reflections count: {len(result_ctx.reflections)}")
print(f" First reasoning: {result_ctx.reflections[0].reasoning[:300]}")
print(f" First key_insight: {result_ctx.reflections[0].key_insight[:200]}")
@pytest.mark.integration
def test_rr_execute_code_tool_used(self):
"""Verify the agent uses execute_code (total_iterations > 0 in rr_trace)."""
from ace.steps.rr_step import RRStep
from ace.implementations.rr.config import RecursiveConfig
config = RecursiveConfig(
max_requests=15,
timeout=15.0,
)
rr = RRStep(model=self.RR_MODEL, config=config)
sb = Skillbook()
agent_out = AgentOutput(
reasoning=(
"I need to find the square root of 144. "
"I think it might be 14 since 14 x 14 is close to 144."
),
final_answer="14",
skill_ids=[],
)
output = rr.reflect(
question="What is the square root of 144?",
agent_output=agent_out,
skillbook=sb,
ground_truth="12",
feedback="Incorrect. The square root of 144 is 12, not 14.",
)
assert isinstance(output, ReflectorOutput)
rr_trace = output.raw.get("rr_trace", {})
total_iterations = rr_trace.get("total_iterations", 0)
assert total_iterations > 0, (
f"Expected execute_code to be called at least once "
f"(total_iterations > 0), got {total_iterations}. "
f"rr_trace={rr_trace}"
)
print(f"\n Total iterations (execute_code calls): {total_iterations}")
print(f" Timed out: {rr_trace.get('timed_out', 'N/A')}")
print(f" Key insight: {output.key_insight[:200]}")
print(f" Reasoning: {output.reasoning[:300]}")
if __name__ == "__main__":
pytest.main([__file__, "-v", "-s", "--no-cov"])