logic-engine / examples /ace /ace_demo.py
ghostdrive1's picture
Upload folder using huggingface_hub
116524e verified
Raw
History Blame Contribute Delete
18.1 kB
#!/usr/bin/env python3
# %% [markdown]
# # ACE Next β€” Interactive Demo
#
# This notebook walks through the refactored `ace` pipeline.
# It covers:
#
# 1. **Runners** β€” `ACE` (full pipeline) and `TraceAnalyser` (learning-only)
# 2. **Steps** β€” individual pipeline steps and `learning_tail()`
# 3. **Manual pipeline construction** β€” composing steps by hand
# 4. **Custom environments** β€” writing your own evaluator
# 5. **Checkpointing & deduplication** β€” production features
# 6. **Observability with Opik** β€” pipeline traces and LLM cost tracking
# 7. **Skillbook persistence** β€” save / reload
# 8. **TraceAnalyser** β€” learning from pre-recorded traces
#
# **Requirements:** `uv sync` from the repo root.
# Set your LLM API key before running:
# ```bash
# export OPENAI_API_KEY="sk-..."
# ```
# %% [markdown]
# ## 1. Setup & Imports
# %%
import os
import sys
import tempfile
from pathlib import Path
import nest_asyncio
nest_asyncio.apply()
# Ensure the project root is on sys.path so `ace`, `ace`, and `pipeline`
# are importable regardless of where the notebook kernel starts.
_here = Path(__file__).resolve().parent if "__file__" in dir() else Path.cwd()
_root = _here
for _p in [_here] + list(_here.parents):
if (_p / "pipeline" / "__init__.py").exists():
_root = _p
break
sys.path.insert(0, str(_root))
from dotenv import load_dotenv
load_dotenv(_root / ".env")
print(f"Project root: {_root}")
print("Setup OK")
# %% [markdown]
# ## 2. Core Imports
#
# Everything lives in `ace` β€” fully self-contained, zero cross-imports.
# %%
from ace import (
# Runners
ACE,
TraceAnalyser,
# Role implementations
Agent,
Reflector,
SkillManager,
# Core types
Sample,
Skillbook,
SimpleEnvironment,
TaskEnvironment,
EnvironmentResult,
)
from ace.core import AgentOutput, ACEStepContext, SkillbookView
print("All imports OK")
# %% [markdown]
# ## 3. Configure the LLM Client
#
# We use LiteLLM which supports 100+ providers. Swap the model string
# for any provider: `gpt-4o-mini`, `claude-sonnet-4-5-20250929`,
# `bedrock/us.anthropic.claude-haiku-4-5-20251001-v1:0`, etc.
# %%
MODEL = os.getenv("ACE_MODEL", "us.anthropic.claude-haiku-4-5-20251001-v1:0")
print(f"Model: {MODEL}")
# %% [markdown]
# ## 4. Build Roles
#
# The three ACE roles share the same LLM client. Each is independently
# customisable (prompt templates, retries, etc.).
# %%
agent = Agent(MODEL)
reflector = Reflector(MODEL)
skill_manager = SkillManager(MODEL)
print("Roles created: Agent, Reflector, SkillManager")
# %% [markdown]
# ## 5. Define Training Samples
# %%
samples = [
Sample(question="What is the capital of France?", ground_truth="Paris"),
Sample(question="What is the capital of Japan?", ground_truth="Tokyo"),
Sample(question="What is the capital of Brazil?", ground_truth="Brasilia"),
Sample(question="What is the capital of Australia?", ground_truth="Canberra"),
Sample(question="What is the capital of Nigeria?", ground_truth="Abuja"),
]
print(f"Prepared {len(samples)} training samples")
# %% [markdown]
# ---
# ## 6. ACE Runner β€” Full Adaptive Pipeline
#
# The `ACE` runner is the full closed-loop pipeline:
# ```
# Agent β†’ Evaluate β†’ Reflect β†’ Tag β†’ Update β†’ Apply
# ```
#
# It takes `Sample` objects and an optional `TaskEnvironment`.
# %% [markdown]
# ### 6a. With SimpleEnvironment
#
# `SimpleEnvironment` checks if the ground truth appears in the agent's
# answer (case-insensitive substring match).
# %%
skillbook = Skillbook()
ace = ACE.from_roles(
agent=agent,
reflector=reflector,
skill_manager=skill_manager,
environment=SimpleEnvironment(),
skillbook=skillbook,
)
results = ace.run(samples[:3], epochs=1)
print(f"Processed {len(results)} samples\n")
for r in results:
if r.error:
print(f" ERROR at {r.failed_at}: {r.error}")
elif r.output:
ctx: ACEStepContext = r.output
answer = ctx.agent_output.final_answer if ctx.agent_output else "N/A"
print(f" Q: {r.sample.question}")
print(f" A: {answer}")
# %%
print(f"\nSkillbook after 1 epoch:")
print(f" Stats: {skillbook.stats()}")
for skill in skillbook.skills()[:5]:
print(f" - [{skill.id}] {skill.content}")
# %% [markdown]
# ### 6b. Custom Environment
#
# Create your own evaluator by subclassing `TaskEnvironment`.
# %%
class ExactMatchEnvironment(TaskEnvironment):
"""Strict evaluation: answer must exactly match ground truth."""
def evaluate(self, sample: Sample, agent_output: AgentOutput) -> EnvironmentResult:
expected = (sample.ground_truth or "").strip().lower()
predicted = agent_output.final_answer.strip().lower()
correct = expected in predicted
return EnvironmentResult(
feedback=(
"Correct!" if correct else f"Wrong. Expected: {sample.ground_truth}"
),
ground_truth=sample.ground_truth,
metrics={"accuracy": 1.0 if correct else 0.0},
)
print("ExactMatchEnvironment defined")
# %%
skillbook2 = Skillbook()
ace2 = ACE.from_roles(
agent=Agent(MODEL),
reflector=Reflector(MODEL),
skill_manager=SkillManager(MODEL),
environment=ExactMatchEnvironment(),
skillbook=skillbook2,
)
results2 = ace2.run(samples[:2], epochs=1)
for r in results2:
if r.output:
ctx = r.output
print(f" Q: {r.sample.question}")
print(f" A: {ctx.agent_output.final_answer if ctx.agent_output else 'N/A'}")
if ctx.reflections:
print(f" Insight: {ctx.reflections[0].key_insight}")
print()
# %% [markdown]
# ### 6c. Without Environment
#
# When no environment is provided, `EvaluateStep` is a no-op. The Reflector
# still learns from ground-truth comparison in the trace.
# %%
skillbook3 = Skillbook()
ace3 = ACE.from_roles(
agent=Agent(MODEL),
reflector=Reflector(MODEL),
skill_manager=SkillManager(MODEL),
skillbook=skillbook3,
# No environment β€” EvaluateStep passes through
)
results3 = ace3.run(samples[:2], epochs=1)
print(f"Processed {len(results3)} samples (no environment)")
print(f"Skills learned: {skillbook3.stats()}")
# %% [markdown]
# ### 6d. Multi-Epoch Training
#
# Multiple epochs let the agent revisit samples with an evolving skillbook.
# Skills accumulate and refine across passes.
# %%
skillbook4 = Skillbook()
ace4 = ACE.from_roles(
agent=Agent(MODEL),
reflector=Reflector(MODEL),
skill_manager=SkillManager(MODEL),
environment=SimpleEnvironment(),
skillbook=skillbook4,
)
results4 = ace4.run(samples, epochs=2)
print(f"Total results across 2 epochs: {len(results4)}")
print(f"Skills learned: {skillbook4.stats()}")
# Print per-epoch accuracy
for epoch in range(1, 3):
epoch_results = [r for r in results4 if r.output and r.output.epoch == epoch]
correct = sum(
1
for r in epoch_results
if r.output
and r.output.agent_output
and (r.sample.ground_truth or "").lower()
in r.output.agent_output.final_answer.lower()
)
print(f" Epoch {epoch}: {correct}/{len(epoch_results)} correct")
# %% [markdown]
# ---
# ## 7. Manual Step-by-Step Pipeline
#
# Under the hood, runners compose `Pipeline` objects from individual steps.
# Here we build one by hand to see exactly what each step does.
# All pipeline classes and steps are importable directly from `ace`.
# %%
from ace import (
Pipeline,
AgentStep,
EvaluateStep,
learning_tail,
)
skillbook5 = Skillbook()
env = SimpleEnvironment()
# Build the full pipeline manually
pipe = Pipeline(
[
AgentStep(Agent(MODEL), skillbook5),
EvaluateStep(env),
*learning_tail(Reflector(MODEL), SkillManager(MODEL), skillbook5),
]
)
print(f"Pipeline steps: {len(pipe._steps)}")
print(f" requires: {pipe.requires}")
print(f" provides: {pipe.provides}")
# %% [markdown]
# ### Run a single sample through the manual pipeline
# %%
sample = samples[0]
# Build the context the same way ACE._build_context() does
ctx = ACEStepContext(
sample=sample,
skillbook=SkillbookView(skillbook5),
epoch=1,
total_epochs=1,
step_index=0,
total_steps=1,
global_sample_index=0,
)
print(f"Before pipeline:")
print(f" Skills: {skillbook5.stats()}")
print(f" agent_output: {ctx.agent_output}")
# Run the full pipeline on a single context
from pipeline.protocol import SampleResult
results_manual = pipe.run([ctx])
print(f"\nAfter pipeline:")
for r in results_manual:
if r.error:
print(f" ERROR: {r.error}")
elif r.output:
out: ACEStepContext = r.output
print(
f" Agent answer: {out.agent_output.final_answer if out.agent_output else 'N/A'}"
)
print(
f" Reflector insight: {out.reflections[0].key_insight if out.reflections else 'N/A'}"
)
print(f" Skills now: {skillbook5.stats()}")
# %% [markdown]
# ### Using `learning_tail()` as a building block
#
# `learning_tail()` returns the standard learning steps:
# `[ReflectStep, UpdateStep]` (the agentic SkillManager mutates the
# skillbook directly via its tools). Optional deduplication and
# checkpoint steps are appended.
# %%
skillbook6 = Skillbook()
tail = learning_tail(
Reflector(MODEL),
SkillManager(MODEL),
skillbook6,
)
print(f"learning_tail() returns {len(tail)} steps:")
for step in tail:
print(f" - {type(step).__name__}")
# %% [markdown]
# ---
# ## 8. Checkpointing
#
# Save the skillbook every N successful samples so you can resume after
# interruption or compare skillbook evolution over time.
# %%
skillbook7 = Skillbook()
with tempfile.TemporaryDirectory() as tmpdir:
ace7 = ACE.from_roles(
agent=Agent(MODEL),
reflector=Reflector(MODEL),
skill_manager=SkillManager(MODEL),
environment=SimpleEnvironment(),
skillbook=skillbook7,
checkpoint_dir=tmpdir,
checkpoint_interval=2, # save every 2 successful samples
)
results7 = ace7.run(samples, epochs=1)
saved = sorted(Path(tmpdir).glob("*.json"))
print("Checkpoint files:")
for f in saved:
print(f" {f.name} ({f.stat().st_size} bytes)")
# %% [markdown]
# ---
# ## 9. Deduplication
#
# Merge near-duplicate skills to keep the skillbook compact. The
# `DeduplicationManager` runs periodically during training.
# %%
from ace import DeduplicationManager, SimilarityDetector
from ace.protocols import DeduplicationConfig
skillbook8 = Skillbook()
dedup = DeduplicationManager(DeduplicationConfig(similarity_threshold=0.85))
ace8 = ACE.from_roles(
agent=Agent(MODEL),
reflector=Reflector(MODEL),
skill_manager=SkillManager(MODEL),
environment=SimpleEnvironment(),
skillbook=skillbook8,
dedup_manager=dedup,
dedup_interval=3, # run dedup every 3 samples
)
results8 = ace8.run(samples, epochs=1)
print(f"Skills after training with dedup: {skillbook8.stats()}")
# %% [markdown]
# ---
# ## 10. Skillbook Persistence β€” Save & Reload
#
# Save the learned skillbook to disk and reload it in a future session.
# %%
with tempfile.TemporaryDirectory() as tmpdir:
path = Path(tmpdir) / "learned_skillbook.json"
# Save
skillbook.save_to_file(str(path))
print(f"Saved to {path.name} ({path.stat().st_size} bytes)")
# Reload
reloaded = Skillbook.load_from_file(str(path))
print(f"Reloaded: {reloaded.stats()}")
print(f"Stats match: {reloaded.stats() == skillbook.stats()}")
# %% [markdown]
# ---
# ## 11. TraceAnalyser β€” Learning from Pre-Recorded Traces
#
# `TraceAnalyser` runs the learning tail only β€” no Agent, no Evaluate.
# Feed it raw trace dicts (the same shape ReflectStep expects) and it
# builds a skillbook from historical data.
# %%
# Simulate some pre-recorded traces (e.g., from browser-use history logs)
traces = [
{
"question": "Book a flight from NYC to London",
"reasoning": "Step 1: Opened booking site. Step 2: Searched flights. Step 3: Selected cheapest option.",
"answer": "Booked flight AA100 for $450",
"skill_ids": [],
"feedback": "Task succeeded in 3 steps",
"ground_truth": None,
},
{
"question": "Find the cheapest hotel in Paris",
"reasoning": "Step 1: Opened hotel site. Step 2: Set filters. Step 3: Sorted by price. Step 4: Cookie popup blocked view.",
"answer": "Failed: could not dismiss cookie popup",
"skill_ids": [],
"feedback": "Task failed β€” cookie popup blocked interaction after step 3",
"ground_truth": None,
},
{
"question": "Check weather in Tokyo",
"reasoning": "Step 1: Navigated to weather.com. Step 2: Searched Tokyo. Step 3: Read forecast.",
"answer": "Tokyo: 22C, partly cloudy",
"skill_ids": [],
"feedback": "Task succeeded in 3 steps β€” fast and accurate",
"ground_truth": None,
},
]
skillbook9 = Skillbook()
analyser = TraceAnalyser.from_roles(
reflector=Reflector(MODEL),
skill_manager=SkillManager(MODEL),
skillbook=skillbook9,
)
results9 = analyser.run(traces, epochs=1)
print(f"Analysed {len(results9)} traces")
print(f"Skills learned: {skillbook9.stats()}")
for skill in skillbook9.skills()[:5]:
print(f" - [{skill.section}] {skill.content}")
# %% [markdown]
# ### Multi-epoch trace analysis
#
# Each epoch re-processes all traces with the evolving skillbook.
# Early epochs extract obvious patterns; later epochs refine.
# %%
skillbook10 = Skillbook()
analyser2 = TraceAnalyser.from_roles(
reflector=Reflector(MODEL),
skill_manager=SkillManager(MODEL),
skillbook=skillbook10,
)
results10 = analyser2.run(traces, epochs=2)
print(f"Total results across 2 epochs: {len(results10)}")
print(f"Skills after 2 epochs: {skillbook10.stats()}")
# %% [markdown]
# ---
# ## 12. Mixed Workflow β€” TraceAnalyser then ACE
#
# A common pattern: build an initial skillbook from historical traces,
# then deploy with live learning.
# %%
# Phase 1: Build skillbook from historical data
shared_skillbook = Skillbook()
analyser_phase1 = TraceAnalyser.from_roles(
reflector=Reflector(MODEL),
skill_manager=SkillManager(MODEL),
skillbook=shared_skillbook,
)
analyser_phase1.run(traces, epochs=1)
print(f"Phase 1 β€” TraceAnalyser:")
print(f" Skills from traces: {shared_skillbook.stats()}")
# Phase 2: Deploy with live ACE learning (reuse the evolved skillbook)
ace_phase2 = ACE.from_roles(
agent=Agent(MODEL),
reflector=Reflector(MODEL),
skill_manager=SkillManager(MODEL),
environment=SimpleEnvironment(),
skillbook=shared_skillbook,
)
results_phase2 = ace_phase2.run(samples[:3], epochs=1)
print(f"\nPhase 2 β€” ACE live learning:")
print(f" Processed {len(results_phase2)} samples")
print(f" Skills after live learning: {shared_skillbook.stats()}")
# %% [markdown]
# ---
# ## 13. Error Handling
#
# Failed samples are captured in `SampleResult.error` β€” the pipeline
# never drops a sample silently. Other samples continue processing.
# %%
bad_samples = [
samples[0],
Sample(question="", ground_truth=""), # edge case: empty question
samples[1],
]
skillbook11 = Skillbook()
ace11 = ACE.from_roles(
agent=Agent(MODEL),
reflector=Reflector(MODEL),
skill_manager=SkillManager(MODEL),
environment=SimpleEnvironment(),
skillbook=skillbook11,
)
results11 = ace11.run(bad_samples, epochs=1)
for i, r in enumerate(results11, 1):
status = "OK" if r.error is None else f"FAIL ({r.failed_at})"
if r.output and r.output.agent_output:
answer = r.output.agent_output.final_answer
else:
answer = "N/A"
print(f" [{i}] {status:20s} answer={answer}")
# %% [markdown]
# ---
# ## 14. Inspecting the SkillbookView
#
# Steps receive a read-only `SkillbookView` on the context.
# This prevents accidental mutations from within pipeline steps.
# %%
sb = Skillbook()
view = SkillbookView(sb)
print(f"SkillbookView: {view}")
print(f" len: {len(view)}")
print(f" stats: {view.stats()}")
print(f" prompt: {view.as_prompt()[:200]}...")
# Iterate over skills in the view
for skill in view:
print(f" - {skill.id}: {skill.content}")
# %% [markdown]
# ---
# ## Summary
#
# | What | How |
# |------|-----|
# | Full pipeline | `ACE.from_roles(agent=..., reflector=..., skill_manager=...)` |
# | With environment | `ACE.from_roles(..., environment=SimpleEnvironment())` |
# | Without environment | `ACE.from_roles(...)` β€” EvaluateStep is a no-op |
# | Multi-epoch | `ace.run(samples, epochs=3)` |
# | Checkpointing | `ACE.from_roles(..., checkpoint_dir="./ckpts", checkpoint_interval=10)` |
# | Deduplication | `ACE.from_roles(..., dedup_manager=dedup, dedup_interval=5)` |
# | Trace analysis | `TraceAnalyser.from_roles(reflector=..., skill_manager=...)` |
# | Save skillbook | `ace.save("path.json")` or `skillbook.save_to_file("path.json")` |
# | Load skillbook | `Skillbook.load_from_file("path.json")` |
# | Manual steps | `Pipeline([AgentStep(a), EvaluateStep(e), *learning_tail(r, sm, sb)])` |
# | Learning tail | `learning_tail(reflector, skill_manager, skillbook)` |
#
# **Pipeline:**
# ```
# ACE: Agent β†’ Evaluate β†’ Reflect β†’ Tag β†’ Update β†’ Apply β†’ [Dedup] β†’ [Checkpoint] β†’ [Opik]
# TraceAnalyser: Reflect β†’ Tag β†’ Update β†’ Apply β†’ [Dedup] β†’ [Checkpoint] β†’ [Opik]
# ```