This document provides an overview of the design and architecture of the AfterImage library.
The library is designed around a few core concepts:
- ConversationGenerator: The main entry point for generating conversations. Acts as a thin facade that delegates internally to focused components (Orchestrator, SamplingStrategy, QualityGate).
- Orchestrator: Manages async concurrency (semaphores, worker tasks,
asyncio.gather), stopping criteria, and progress reporting. Coordinates SamplingStrategy and QualityGate during generation runs. - SamplingStrategy: Encapsulates persona selection, context/document sampling, and instruction dispatch logic. Infers target usage counts from stopping callbacks and configures sampling for both context and persona pipelines.
- QualityGate: Wraps the evaluator (ConversationJudge) and retry logic for the
auto_improveworkflow. Determines whether a generated conversation meets quality thresholds or needs regeneration. - PersonaGenerator: Analyzes documents to generate diverse user personas, enhancing dataset variety.
- LLMProvider: An abstraction over different language model providers (Gemini, OpenAI compatible).
- EmbeddingProvider: Async-first text embeddings (
async def embed(texts) -> list[list[float]]). API backends (OpenAIEmbeddingProvider,GeminiEmbeddingProvider) use each vendor’s async client andSmartKeyPool;ProcessEmbeddingProviderruns SentenceTransformer in aProcessPoolExecutorso the asyncio loop is not blocked by local inference (install theembeddings-localextra forsentence-transformers). UseEmbeddingProviderFactory.create({...})inafterimage/providers/embedding_providers.py. - DatasetStorage: An abstraction for storing and loading generated conversations and documents. It supports JSONL and SQL backends.
- Callbacks: These allow for customization of the generation process.
- InstructionGeneratorCallback: Generates the initial questions or instructions (e.g.,
PersonaInstructionGeneratorCallback). - RespondentPromptModifier: Modifies the prompt for the respondent based on context (e.g.,
WithRAGRespondentPromptModifier).
- InstructionGeneratorCallback: Generates the initial questions or instructions (e.g.,
- Evaluation: Async
ConversationJudgecombines embedding metrics (viaEmbeddingProvider) and LLM rubrics (agenerate_structured), withCompositeEvaluatoraggregation (MEAN,WEIGHTED_MEAN,MIN).ConversationGenerator(auto_improve=True)builds a judge usingdefault_embedding_provider_configwhen no embedding backend is passed explicitly. - Monitoring: Real-time tracking of generation metrics (time, tokens, errors) with alert support.
- Reasoning Capture: OpenAI-compatible providers expose optional
reasoning_content/thinkingtext;AsyncConversationGeneratorpersists assistant reasoning intoConversationEntry.reasoning_contentwhen present. - OpenSimula (experimental): The
afterimage.simulasubpackage implements Simula-style mechanism design for synthetic data: reasoning-driven taxonomies (propose–critic–plan), weighted mix sampling, meta-prompt diversification with optional complexification, requirement critics with refinement, double-critic gating for MCQ, single-QA and MCQ task helpers, taxonomic coverage and Elo-style complexity scoring, andSimulaInstructionGeneratorCallbackto feed precomputed scenarios intoConversationGenerator. Seeafterimage/simula/README.mdandexamples/simula/README.md. Import viafrom afterimage.simula import OpenSimula(not re-exported from the package root__init__to keep the default import surface stable). - Adaptive Context Sampling: Document providers now keep per-document usage counts and sampling weights so instruction generation can bias toward underused contexts. When a context coverage stopping callback is present, its
target_visitsis propagated into provider weights; otherwise providers fall back to a soft-decay weighting strategy (1 / (usage + 1)). Usage is recorded only after a final row is produced successfully, and all sampled context ids are carried through row metadata for coverage accounting across contextual, persona, and tool-calling instruction callbacks. Metadata-to-context-id extraction is centralized so usage reporting and coverage counting share the same semantics. If a provider target is set explicitly, generator-side inference does not overwrite it, and generated instruction payloads keep theircontext_idsin per-instance state. - Fixed-Width Persona Generation: Persona generation now enforces an exactly-five-persona contract per generation step. Persona outputs are LLM-generated, whitespace-normalized, deduplicated, and retried up to three times before a document-level enrichment failure is surfaced back to the caller. Batch enrichment now follows a gather-then-commit flow: documents are enriched on deep copies, successful results are saved together only after the whole batch succeeds, and failed batches leave in-memory documents and storage untouched.
- Dynamic Persona Tree Depth:
PersonaGenerator.generate_from_documents()now treatsn_iterations=Noneas auto mode. In auto mode it resolves an effective per-document persona target from provider usage targets ortarget_data_count, accounting for how many contexts are merged into each row, then chooses the depth whose expected pool is closest to that target instead of always building a large tree. Partial persona-chain failures are tolerated per branch so one bad expansion does not discard an entire document's persona tree. - Depth-Aware Persona Sampling: Persona-based instruction callbacks flatten stored persona trees per document, preserve
generation_depth, and derive a per-document persona target from provider coverage targets, explicit request size, or fixed-number stopping callbacks. If the effective target is unknown, they keep the full pool instead of pruning heuristically. When demand is smaller than supply, shallow layers are kept first via top-down pruning and round-robin reuse. When demand is larger than supply, personas are reused with layer-normalized depth weights so upper layers truly receive more total reuse despite deeper layers having many more nodes. Selection state is synchronized for threaded sync generation, and selected persona depth is propagated aspersona_generation_depthin generation metadata. Sync and async evaluator retries rebuild the full row after regeneration and preserve any prompt-modifier-adjusted respondent prompt, so judges see the updated conversation under the same prompting conditions. Scenario coverage now includes auto-depth generation targets, small targets, layer-boundary targets, exact-pool matches, large oversampling targets, multi-context target inference, active-document inference cases, fixed-number stopping inference, and evaluator retry regressions. - Provider-Aware Concurrency: Async generators and persona generation resolve concurrency defaults per provider, with a higher default for DeepSeek workloads.
- Demo UI Storage Compatibility: Demo
CaptureStorageimplements the full storage interface expected by generators (includingload_documents) and keeps sync/async conversation save methods compatible with base storage contracts. - Demo UI Provider Consistency: Tool-calling generation now builds personas with the same DeepSeek model/provider configuration used by demo generators to avoid cross-provider key mismatches.
- Training Version Compatibility Guards: Demo training requirements constrain
transformersandtrlto a compatible range (transformers>=4.56.2,<5.0.0,trl>=0.29.1,<0.30.0) to avoid runtime API mismatches duringSFTConfigimport and trainer startup. The same stack is declared as thetrainingoptional extra inpyproject.toml(aligned withexamples/demo_ui/training_scripts/requirements.txt); install withpip install -e ".[training]"oruv sync --extra trainingso the Gradio-launchedtrain.pysubprocess can importtrland related packages. - Environment Template Files: Repository root includes a minimal
.env.exampleand local.envtemplate for demo runtime and training credentials (GEMINI_API_KEY,DEEPSEEK_API_KEY,HF_TOKEN, optionalHF_HUB_DISABLE_XET);.envis gitignored to keep secrets out of version control. - CLI Interface: The
afterimagecommand providesgenerate,validate, andexportsubcommands. Generation is driven by YAML config files that map to Pydantic models inconfig.py. Theexportcommand converts datasets to ShareGPT, Alpaca, or HuggingFace messages formats. - Agent Trace Dataset Generation: The
afterimage.agent_tracesubpackage provides environment-free synthetic agent-trace dataset generation (combining ESAT methodology with a sub-millisecond local Declarative Tool Simulation Framework). Key components includeSchemaArchitect(LLM Pydantic response schema generator with static AST verification feedback loop),SchemaVerifier(6 structural invariant checks),DeclarativeEngine(4-tier fallback generator withSimulationContextentity lookup pools),GridTaskSynthesizer(360-bucket grid +InverseFrequencySampler+ procedural task rewriter),ReActTrajectoryLoop(multi-turn teacher execution against local tools),TrajectoryJudge(9-point LLM quality rubric), andAsyncAgentTraceGeneratorfacade. Model defaults followgemini-3.5-flash-litefor execution/synthesis andgemini-3.6-flashfor schema architecture and trajectory judging. - Local Model Support: The
localprovider 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.
The code is organized into the following directories and files:
afterimage/: The main source code for the library.__init__.py: Exposes the main classes and functions.conversation_generator.py: Thin facade over Orchestrator, SamplingStrategy, and QualityGate. Maintains the full public API for backward compatibility.orchestrator.py: Async concurrency management — worker tasks, semaphores, stopping criteria, and progress reporting.sampling.py: Persona and context sampling coordination — target inference, coverage configuration, and usage recording.quality_gate.py: Evaluator wrapper with retry logic for the auto-improve workflow.config.py: Pydantic models for YAML config schema andload_config()loader.config_to_generator.py:build_generator()translates config into a ConversationGenerator.cli.py: Click-based CLI (afterimage generate/validate/export).exporters.py: Dataset format converters (ShareGPT, Alpaca, HuggingFace messages).base.py: Base classes for generators and callbacks. Sampling methods delegate to SamplingStrategy.callbacks.py: Implements default callbacks for instructions and persona handling.common.py: Common constants and data structures. It also holds provider-aware concurrency defaults.evaluator.py:ConversationJudgeand embedding defaults for auto-improve.key_management.py: Smart API key management with rate limiting.monitoring.py: Monitoring system implementation.persona_generator.py: [NEW] Logic for generating personas from documents.prompts.py: Default prompts (instruction generation, respondent persona etc.).quality.py: Quality checking logic.retrievers.py: Context retrieval strategies for RAG (ContextRetriever,RetrievalResult, optional*_context_with_metadata,QdrantRetriever,StaticContextRetriever, composite retrievers).storage.py: Storage backends (JSONL, SQL).types.py: Data models using Pydantic.simula/: OpenSimula (experimental) —OpenSimulaorchestrator,taxonomy_builder(optionalshow_progress+ tqdm),cli_logging(configure_example_console,silence_noisy_third_party_loggers),sampling,meta_prompt,critics,double_critic,evaluation,document_context, andtasks/(single QA, MCQ, multiturn instruction callback).evaluation/: The new evaluation framework.__init__.py: Exposes evaluation classes.base.py: Base classes for evaluators.evaluators.py: Concrete evaluation metrics.strategies.py: Quality improvement strategies.
providers/:__init__.py: Exposes provider classes.document_providers.py: Document source implementations (Memory, File, Directory, Qdrant). Providers expose weighted random sampling plus document usage reporting for context coverage management, with target usage counts inferred from stopping callbacks when available.llm_providers.py: LLM provider abstractions (Gemini, OpenAI, DeepSeek, OpenRouter, local OpenAI-compatible) andLLMFactory.local_provider.py:LocalLLMProviderfor OpenAI-compatible local servers (vLLM, Ollama, llama.cpp).embedding_providers.py: Async embedding providers (OpenAI, Gemini, process pool) and factory.
examples/configs/: YAML config examples (basic.yaml,rag.yaml,local.yaml).examples/demo_ui/: Gradio demo application.README.md: Page-by-page demo UI guide (routes, features, setup, troubleshooting).app.py: Ensures the repository root is included insys.pathwhen the demo is run directly asuv run examples/demo_ui/app.py.core/storage.py: Implements demo capture storage with full storage-protocol compatibility.pages/handlers/generation.py: Aligns persona-generation provider/model with the demo generator provider defaults.training_scripts/requirements.txt: Owns training stack compatibility bounds fortrlandtransformers.- Demo training subprocess:
train.pyaccepts--dataset(resolved relative totraining_scripts/cwd) so the UI-prepared merge/filter output always matches what SFT trains on;training_configloads.envviafind_dotenvsoHF_TOKENis found from repo root when the training cwd istraining_scripts/. On failure, the runner surfaces the last lines of subprocess output instead of a generic message only.
The library uses several design patterns to achieve its goals:
- Facade Pattern:
ConversationGeneratorpresents a simple public API while delegating toOrchestrator,SamplingStrategy, andQualityGateinternally. - Strategy Pattern:
LLMProvider,DatasetStorage,ContextRetriever,SamplingStrategy, andBaseEvaluatordefine swappable algorithms. - Factory Pattern:
LLMFactorycreatesLLMProviderinstances dynamically. - Callback Pattern: Customizes generation flow via
InstructionGeneratorCallbackandRespondentPromptModifier. - Composite Pattern:
CompositeEvaluatorcombines multiple evaluation metrics. - Template Method Pattern:
BaseGeneratorand callbacks define algorithmic skeletons with overridable steps. - Async/Await Pattern: All the generators utilizes Python's
asynciofor high-throughput concurrent generation. - Single Responsibility: Each extracted component owns one concern —
SamplingStrategyhandles sampling,QualityGatehandles evaluation gating,Orchestratorhandles concurrency.
The architecture of the library is modular and extensible. The core components are decoupled from each other, which makes it easy to replace or extend them.
CLI (afterimage generate/validate/export)
└── Config (YAML) → build_generator()
└── ConversationGenerator (facade)
├── Orchestrator — async concurrency, worker tasks, stopping criteria
│ ├── SamplingStrategy — persona/context sampling, target inference
│ └── QualityGate — evaluator wrapper, retry decisions
├── LLMProvider — model interaction (chat sessions, content generation)
│ ├── GeminiProvider — Google Gemini API
│ ├── OpenAIProvider — OpenAI / compatible APIs
│ ├── DeepSeekProvider — DeepSeek API
│ ├── LocalLLMProvider — local servers (vLLM, Ollama, llama.cpp)
│ └── SmartKeyPool — rate-limit management (hidden from upper layers)
├── Storage — JSONL / SQL backends
└── Callbacks — instruction generation, prompt modification, stopping
Data flow during generation:
ConversationGenerator.generate()delegates toOrchestrator.run()OrchestratorusesSamplingStrategyto configure persona/context samplingOrchestratorspawns concurrent worker tasks with semaphore-bounded parallelism- Each worker calls
ConversationGenerator.generate_single()for one conversation generate_single()runs the multi-turn loop, then passes the result throughQualityGateQualityGateevaluates quality and signals retry if the grade is below threshold- Accepted conversations are saved to
Storage
You can easily add a new LLM provider by implementing the LLMProvider protocol and registering it with the LLMFactory. Similarly, you can add a new storage backend by implementing the DatasetStorage protocol.
The evaluation framework is also designed to be extensible. You can add new evaluation metrics by implementing the BaseEvaluator protocol and adding it to the CompositeEvaluator.
The monitoring system is also extensible. You can add new metric handlers by implementing the MetricHandler protocol and adding it to the GenerationMonitor.
scripts/generate_qa.py: QA dataset generation script that usesAsyncConversationGeneratorwith a dynamic system prompt parts feature. Before generating QA pairs, it makes a single LLM API call (usinggoogle-genaiwith Pydantic structured output) to analyze the input document and generate context-appropriate system prompt "parts" (a role description and an answering instruction). These parts are used as the respondent prompt and also saved to the output JSON alongside QA pairs and formatted samples (following theformat_samplepattern withinput/outputfields).