Spaces:
Sleeping
Sleeping
| #!/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] | |
| # ``` | |