Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
- **Multiple Storage Backends**: Store conversations in JSONL files or SQL databases (SQLite, PostgreSQL, MySQL).
- **Save in JSONL Format**: Export datasets directly for downstream applications.
- **Quality Analysis**: Comprehensive dataset quality checks with visualization support.
- **Environment-Free Agent Traces**: Generate synthetic agent execution trajectories combining ESAT methodology with sub-millisecond local declarative simulation tools (`afterimage.agent_trace`).
- **Generation Monitoring**: Real-time monitoring of generation metrics with alerts and visualization.

**See @DESIGN.md for the code design and architecture.**
Expand Down
1 change: 1 addition & 0 deletions DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ The library is designed around a few core concepts:
- **Training Version Compatibility Guards**: Demo training requirements constrain `transformers` and `trl` to a compatible range (`transformers>=4.56.2,<5.0.0`, `trl>=0.29.1,<0.30.0`) to avoid runtime API mismatches during `SFTConfig` import and trainer startup. The same stack is declared as the `training` optional extra in `pyproject.toml` (aligned with `examples/demo_ui/training_scripts/requirements.txt`); install with `pip install -e ".[training]"` or `uv sync --extra training` so the Gradio-launched `train.py` subprocess can import `trl` and related packages.
- **Environment Template Files**: Repository root includes a minimal `.env.example` and local `.env` template for demo runtime and training credentials (`GEMINI_API_KEY`, `DEEPSEEK_API_KEY`, `HF_TOKEN`, optional `HF_HUB_DISABLE_XET`); `.env` is gitignored to keep secrets out of version control.
- **CLI Interface**: The `afterimage` command provides `generate`, `validate`, and `export` subcommands. Generation is driven by YAML config files that map to Pydantic models in `config.py`. The `export` command converts datasets to ShareGPT, Alpaca, or HuggingFace messages formats.
- **Agent Trace Dataset Generation**: The `afterimage.agent_trace` subpackage provides environment-free synthetic agent-trace dataset generation (combining ESAT methodology with a sub-millisecond local Declarative Tool Simulation Framework). Key components include `SchemaArchitect` (LLM Pydantic response schema generator with static AST verification feedback loop), `SchemaVerifier` (6 structural invariant checks), `DeclarativeEngine` (4-tier fallback generator with `SimulationContext` entity lookup pools), `GridTaskSynthesizer` (360-bucket grid + `InverseFrequencySampler` + procedural task rewriter), `ReActTrajectoryLoop` (multi-turn teacher execution against local tools), `TrajectoryJudge` (9-point LLM quality rubric), and `AsyncAgentTraceGenerator` facade. Model defaults follow `gemini-3.5-flash-lite` for execution/synthesis and `gemini-3.6-flash` for schema architecture and trajectory judging.
- **Local Model Support**: The `local` provider wraps the OpenAI-compatible API with local-friendly defaults: no API key required, no rate limiting via SmartKeyPool, extended timeouts (30s connect, 300s request), and clear connection error messages. Works with vLLM, Ollama, and llama.cpp servers.

## Directory Structure
Expand Down
50 changes: 49 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,16 @@ from a single YAML file or a composable Python API.

## News

### July 26, 2026 — Environment-Free Synthetic Agent Traces (`afterimage.agent_trace`)

**afterimage.agent_trace** combines the **ESAT** pipeline (*Environment-free Synthetic Data Generation for API-Calling Agents*, [arXiv:2607.16900](https://arxiv.org/abs/2607.16900)) with a sub-millisecond local **Declarative Tool Simulation Framework**. It enables training data generation for API-calling AI agents without pre-building backend applications or databases. Key highlights:
- **Sub-Millisecond Execution:** Local Python tool responses (< 1ms execution time, 0% LLM simulator hallucinations).
- **Static AST Verification:** `SchemaVerifier` checks 6 structural invariants with automatic LLM self-correction feedback loops.
- **360-Bucket Grid & Inverse Frequency:** Combinatorial task synthesis grid paired with inverse-frequency endpoint sampling.
- **Trajectory Curation:** Multi-turn ReAct teacher agent interaction (`gemini-3.5-flash-lite`) filtered by a 9-point rubric judge (`gemini-3.6-flash`).

See the [docs](https://afterimage.altai.dev/agent_trace.html) and runnable script [`examples/agent_trace_generator_demo.py`](examples/agent_trace_generator_demo.py).

### May 13, 2026 — Context2skill

**ctx2skill** is a new method to convert and iteratively optimize large contexts to skills that agents can use, originally proposed in [From context to skills: Can language models learn from context skillfully?](https://arxiv.org/html/2604.27660v1). See the [docs](https://afterimage.altai.dev/context_to_skill_tutorial.html) to learn how to use it.
Expand Down Expand Up @@ -86,7 +96,7 @@ Your documents + LLM → Realistic, diverse, quality-filtered training data

| Category | What's included |
|---|---|
| **Generation** | Multi-turn chat · Document-grounded QA · Persona-driven diversity · Structured output · Tool-calling |
| **Generation** | Multi-turn chat · Document-grounded QA · Persona-driven diversity · Structured output · Tool-calling · Environment-free Agent Traces (ESAT + Declarative Engine) |
| **Preference Data** | DPO · RLHF · UltraFeedback · Anthropic HH · ORPO |
| **Quality** | LLM-as-judge · Embedding-based metrics · Auto-improve retries · Composite scoring |
| **Providers** | Gemini · OpenAI · DeepSeek · OpenRouter · Local (vLLM / Ollama / llama.cpp) |
Expand Down Expand Up @@ -169,6 +179,12 @@ afterimage push -c your_config.yaml --repo-id your-org/your-dataset
afterimage preference -c your_config.yaml
```

**Generate environment-free synthetic agent traces:**

```bash
afterimage agent-trace --app-name "banking_app" -n 10 -o "outputs/agent_trajectories.jsonl"
```

**Analyze your dataset:**

```bash
Expand Down Expand Up @@ -200,6 +216,38 @@ async def main():
asyncio.run(main())
```

**Environment-free synthetic agent trace generation (`afterimage.agent_trace`):**

```python
import asyncio
import os
from afterimage.agent_trace import AsyncAgentTraceGenerator, ToolActionSpec, ToolParameterSpec

async def main():
generator = AsyncAgentTraceGenerator(
api_key=os.environ["GEMINI_API_KEY"],
architect_model="gemini-3.6-flash",
teacher_model="gemini-3.5-flash-lite",
judge_model="gemini-3.6-flash",
)
await generator.register_app_domain(
app_name="banking_app",
app_description="Customer banking application.",
actions=[
ToolActionSpec(
action_name="get_balance",
description="Fetch user account balance.",
parameters=[ToolParameterSpec(name="account_id", type="int")],
response_model_name="BalanceResponse",
)
],
)
trajectories = await generator.generate(num_trajectories=10, max_turns=5)
print(f"Generated {len(trajectories)} agent trajectories.")

asyncio.run(main())
```

**Document-grounded generation with personas:**

```python
Expand Down
1 change: 1 addition & 0 deletions afterimage/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import importlib.metadata

from afterimage.agent_trace import AsyncAgentTraceGenerator # noqa
from afterimage.async_conversation_generator import AsyncConversationGenerator # noqa
from afterimage.callbacks import (
AndStoppingCallback, # noqa
Expand Down
49 changes: 49 additions & 0 deletions afterimage/agent_trace/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
"""
AfterImage Agent Trace subpackage for environment-free synthetic agent-trace dataset generation.
"""

from .generator import AsyncAgentTraceGenerator
from .schema_architect import SchemaArchitect
from .simulation_engine import DeclarativeEngine, SimulationContext
from .task_synthesis import GridTaskSynthesizer, InverseFrequencySampler
from .tool_environment import DeclarativeEnvironment, DeclarativeTool
from .trajectory_generator import ReActTrajectoryLoop
from .trajectory_judge import TrajectoryJudge
from .types import (
AgentTrajectory,
AppDomainSpec,
GridTaskBucket,
JudgeVerdict,
RubricScores,
ToolActionSpec,
ToolCall,
ToolObservation,
ToolParameterSpec,
TrajectoryTurn,
)
from .verifier import SchemaVerifier, VerificationReport

__all__ = [
"AsyncAgentTraceGenerator",
"DeclarativeEngine",
"SimulationContext",
"DeclarativeEnvironment",
"DeclarativeTool",
"SchemaArchitect",
"SchemaVerifier",
"VerificationReport",
"GridTaskSynthesizer",
"InverseFrequencySampler",
"ReActTrajectoryLoop",
"TrajectoryJudge",
"AgentTrajectory",
"AppDomainSpec",
"GridTaskBucket",
"JudgeVerdict",
"RubricScores",
"ToolActionSpec",
"ToolCall",
"ToolObservation",
"ToolParameterSpec",
"TrajectoryTurn",
]
172 changes: 172 additions & 0 deletions afterimage/agent_trace/generator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
import asyncio
import logging
from typing import List, Optional, Union

from ..key_management import SmartKeyPool
from ..providers.llm_providers import LLMFactory, LLMProvider
from ..storage import BaseStorage, JSONLStorage
from ..types import Conversation, ConversationEntry, Role
from .schema_architect import SchemaArchitect
from .task_synthesis import GridTaskSynthesizer
from .tool_environment import DeclarativeEnvironment
from .trajectory_generator import ReActTrajectoryLoop
from .trajectory_judge import TrajectoryJudge
from .types import AgentTrajectory, AppDomainSpec, ToolActionSpec

logger = logging.getLogger(__name__)


class AsyncAgentTraceGenerator:
"""Async Environment-Free Synthetic Agent-Trace Dataset Generator Facade."""

def __init__(
self,
api_key: Optional[Union[str, List[str], SmartKeyPool]] = None,
llm_provider: Optional[LLMProvider] = None,
provider: str = "gemini",
architect_model: str = "gemini-3.6-flash",
teacher_model: str = "gemini-3.5-flash-lite",
judge_model: str = "gemini-3.6-flash",
storage: Optional[BaseStorage] = None,
):
if llm_provider:
self.llm_provider = llm_provider
else:
self.llm_provider = LLMFactory.create(
provider=provider,
api_key=api_key,
model_name=architect_model,
)

self.architect = SchemaArchitect(
llm_provider=self.llm_provider,
model_name=architect_model,
)
self.synthesizer = GridTaskSynthesizer(
llm_provider=self.llm_provider,
model_name=teacher_model,
)
self.teacher_loop = ReActTrajectoryLoop(
llm_provider=self.llm_provider,
model_name=teacher_model,
)
self.judge = TrajectoryJudge(
llm_provider=self.llm_provider,
model_name=judge_model,
)

self.environment = DeclarativeEnvironment()
self.storage = storage or JSONLStorage(
conversations_path="outputs/agent_trajectories.jsonl"
)

async def register_app_domain(
self, app_name: str, app_description: str, actions: List[ToolActionSpec]
) -> AppDomainSpec:
"""Runs SchemaArchitect to generate and register Pydantic response models for an app domain."""
app_spec, model_classes = await self.architect.generate_app_domain_schema(
app_name=app_name,
app_description=app_description,
actions=actions,
)
self.environment.register_app_domain(app_spec, model_classes=model_classes)
return app_spec

async def generate_single(self, max_turns: int = 6) -> Optional[AgentTrajectory]:
"""Synthesizes a single agent trajectory (task -> ReAct loop -> judge)."""
if not self.environment.app_domains:
raise ValueError(
"No app domains registered. Call register_app_domain() first."
)

# 1. Task synthesis via 360-bucket grid & task rewriter
task, selected_apps, bucket = await self.synthesizer.synthesize_task(
app_domains=self.environment.app_domains
)

# 2. ReAct teacher trajectory loop against DeclarativeEnvironment (< 1ms tool calls)
trajectory = await self.teacher_loop.run_trajectory(
task=task,
environment=self.environment,
domain_apps=selected_apps,
)
trajectory.metadata["grid_bucket"] = bucket.model_dump()

# 3. Trajectory Judge Quality Filtering
verdict = await self.judge.evaluate_trajectory(trajectory)
trajectory.judge_verdict = verdict

if verdict.is_valid:
return trajectory
return None

async def generate(
self,
num_trajectories: int = 10,
max_turns: int = 6,
max_concurrency: int = 4,
) -> List[AgentTrajectory]:
"""Generates multiple synthetic agent trajectories concurrently."""
sem = asyncio.Semaphore(max_concurrency)
accepted_trajectories: List[AgentTrajectory] = []

async def _worker() -> Optional[AgentTrajectory]:
async with sem:
try:
return await self.generate_single(max_turns=max_turns)
except Exception as e:
logger.warning(
f"Error during trajectory generation worker: {e}", exc_info=True
)
return None

tasks = [_worker() for _ in range(num_trajectories)]
results = await asyncio.gather(*tasks, return_exceptions=True)

conversations = []
for res in results:
if isinstance(res, Exception):
logger.error(f"Worker encountered unhandled exception: {res}")
continue
if isinstance(res, AgentTrajectory):
accepted_trajectories.append(res)
conv = self._trajectory_to_conversation(res)
conversations.append(conv)

if conversations:
self.storage.save_conversations(conversations)

return accepted_trajectories

def _trajectory_to_conversation(self, traj: AgentTrajectory) -> Conversation:
"""Converts an AgentTrajectory into AfterImage's base Conversation schema."""
entries: List[ConversationEntry] = [
ConversationEntry(role=Role.USER, content=traj.task)
]
for t in traj.turns:
entry_text = f"Thought: {t.agent_thought}"
if t.tool_call:
entry_text += f"\nAction: {t.tool_call.app}.{t.tool_call.action}\nAction Input: {t.tool_call.parameters}"
entries.append(ConversationEntry(role=Role.ASSISTANT, content=entry_text))

if t.observation:
entries.append(
ConversationEntry(
role=Role.USER,
content=f"Observation: {t.observation.observation}",
)
)

if traj.final_answer:
entries.append(
ConversationEntry(
role=Role.ASSISTANT, content=f"Final Answer: {traj.final_answer}"
)
)

metadata = traj.metadata
if traj.judge_verdict:
metadata["judge_verdict"] = traj.judge_verdict.model_dump()
metadata["trajectory_id"] = traj.trajectory_id
Comment thread
monatis marked this conversation as resolved.
Outdated

return Conversation(conversations=entries, metadata=metadata)
Loading
Loading