Skip to content

Latest commit

 

History

History
507 lines (406 loc) · 13.8 KB

File metadata and controls

507 lines (406 loc) · 13.8 KB

Agora Architecture: Composable Agent Primitives

Philosophy

Agora is built on horizontal composable primitives, not vertical use-case agents. This means:

  • Horizontal = Reusable building blocks (role, swarm, bulk, pipeline)
  • Vertical = Single-purpose specialists (GTM expert, website analyzer, etc.)

Users compose primitives via YAML to create custom agents. Agora provides the building blocks, users build the agents.

Core Architecture

┌─────────────────────────────────────────────────────────┐
│ YAML Agent Configurations (api/_lib/agents/*/config.yaml) │
│  - Users define agents by composing primitives         │
│  - type: role | swarm | bulk | pipeline                │
└─────────────────────────────────────────────────────────┘
                          ↓
┌─────────────────────────────────────────────────────────┐
│ Agent Loader (api/_lib/utils/agent_loader.py)          │
│  - Reads YAML configs                                  │
│  - Instantiates primitives based on type               │
│  - Returns configured primitive instance               │
└─────────────────────────────────────────────────────────┘
                          ↓
┌─────────────────────────────────────────────────────────┐
│ Primitive System (api/_lib/primitives/)                │
│  - BasePrimitive (abstract interface)                  │
│  - RolePrimitive (simple LLM call)                     │
│  - SwarmPrimitive (parallel decomposition)              │
│  - BulkPrimitive (batch processing)                    │
│  - PipelinePrimitive (sequential steps)                │
└─────────────────────────────────────────────────────────┘
                          ↓
┌─────────────────────────────────────────────────────────┐
│ LLM Execution (api/_lib/utils/openai_client.py)        │
│  - llm_call_messages() - Non-streaming                 │
│  - llm_call_streaming() - SSE streaming                │
└─────────────────────────────────────────────────────────┘

Primitive Types

1. Role Primitive (type: role)

Purpose: Simple LLM call with configurable system prompt

Use Cases:

  • Basic chat agents
  • Specialized personas (academic writer, code reviewer)
  • Single-step transformations

YAML Config Example:

name: "Academic Writer"
type: role
description: "Writes academic-style content"
model: "gpt-4o-mini"
temperature: 0.7
max_tokens: 2000
system_prompt: |
  You are an expert academic writer. Write in formal,
  scholarly style with proper citations and structure.

Implementation: api/_lib/primitives/role.py

How it works:

  1. Builds message array with system prompt + context + query
  2. Calls LLM
  3. Returns response

2. Swarm Primitive (type: swarm)

Purpose: Parallel task decomposition and synthesis

Use Cases:

  • Multi-perspective research
  • Complex queries requiring multiple angles
  • Divide-and-conquer problems

YAML Config Example:

name: "Research Swarm"
type: swarm
description: "Multi-agent parallel research"
model: "gpt-4o-mini"
max_parallel: 3
temperature: 0.5
max_tokens: 2000
system_prompt: |
  You are a research coordinator. Synthesize findings
  from multiple research specialists into a coherent answer.
sub_agent_prompt: |
  You are a focused research specialist. Answer the
  given question accurately and concisely.

Implementation: api/_lib/primitives/swarm.py

How it works:

  1. Decompose: LLM breaks query into N independent sub-tasks
  2. Execute: Run sub-tasks in parallel (asyncio.gather)
  3. Synthesize: LLM combines results into final answer

Flow Diagram:

Query: "Research climate change impact on agriculture"
         ↓
    [Decompose]
    /    |    \
   v     v     v
[Sub1] [Sub2] [Sub3]  ← Parallel execution
 ↓      ↓      ↓
[R1]   [R2]   [R3]
   \    |    /
    [Synthesize]
         ↓
   Final Answer

3. Bulk Primitive (type: bulk)

Purpose: Process lists/batches item-by-item with structured output

Use Cases:

  • "Analyze these 10 companies"
  • "Summarize each of these articles"
  • "Score these resumes"

YAML Config Example:

name: "Batch Analyzer"
type: bulk
description: "Process multiple items in sequence"
model: "gpt-4o-mini"
temperature: 0.5
max_tokens: 500  # Per item
system_prompt: |
  You are a data analyst. Process each item systematically
  and provide structured output.
item_prompt_template: |
  Analyze this item:
  {{item}}

  Provide: name, summary, score (1-10), key_insights

Implementation: api/_lib/primitives/bulk.py

How it works:

  1. Extract list items from query (LLM or regex)
  2. Process each item sequentially
  3. Format results as structured table/JSON
  4. Return aggregated output

Flow Diagram:

Query: "Analyze: CompanyA, CompanyB, CompanyC"
         ↓
   [Extract Items]
    ["CompanyA", "CompanyB", "CompanyC"]
         ↓
  [Process Each]
    Item 1 → [LLM] → Result 1
    Item 2 → [LLM] → Result 2
    Item 3 → [LLM] → Result 3
         ↓
  [Format Results]
    | Name | Score | Summary |
    |------|-------|---------|
    | A    | 8/10  | ...     |
    | B    | 6/10  | ...     |
    | C    | 9/10  | ...     |

4. Pipeline Primitive (type: pipeline)

Purpose: Sequential multi-step workflows where each step feeds the next

Use Cases:

  • Research → Draft → Review → Publish
  • Data extraction → Analysis → Visualization
  • Multi-stage reasoning (CoT)

YAML Config Example:

name: "Research Pipeline"
type: pipeline
description: "Multi-step research and writing workflow"
model: "gpt-4o-mini"
steps:
  - name: "research"
    prompt: "Research this topic thoroughly: {{query}}"
    max_tokens: 1500
  - name: "outline"
    prompt: "Create outline based on research: {{research}}"
    max_tokens: 500
  - name: "draft"
    prompt: "Write full article from outline: {{outline}}"
    max_tokens: 2000
  - name: "review"
    prompt: "Review and improve: {{draft}}"
    max_tokens: 2000

Implementation: api/_lib/primitives/pipeline.py

How it works:

  1. Execute steps sequentially
  2. Each step receives outputs from previous steps as variables
  3. Return final step output (or all intermediate outputs)

Flow Diagram:

Query: "Write about quantum computing"
         ↓
    [Step 1: Research]
    {{query}} → LLM → research_output
         ↓
    [Step 2: Outline]
    {{research}} → LLM → outline_output
         ↓
    [Step 3: Draft]
    {{outline}} → LLM → draft_output
         ↓
    [Step 4: Review]
    {{draft}} → LLM → final_output
         ↓
      Response

Agent Loader System

File: api/_lib/utils/agent_loader.py

Purpose: Dynamically instantiate primitives from YAML configs

Primitive Registry:

PRIMITIVE_REGISTRY = {
    "role": RolePrimitive,
    "swarm": SwarmPrimitive,
    "bulk": BulkPrimitive,
    "pipeline": PipelinePrimitive,
}

Loading Process:

def create_agent_from_config(agent_name: str) -> BasePrimitive:
    # 1. Load config.yaml
    config = load_agent_config(agent_name)

    # 2. Load prompt.txt if exists
    if prompt_path.exists():
        config["system_prompt"] = prompt_path.read_text()

    # 3. Get primitive type from config
    primitive_type = config.get("type", "role")  # Default to role

    # 4. Instantiate primitive
    primitive_class = PRIMITIVE_REGISTRY[primitive_type]
    return primitive_class(config)

Creating New Agents (User Perspective)

Users create agents by:

  1. Create agent directory: api/_lib/agents/my_agent/
  2. Write config.yaml:
    name: "My Custom Agent"
    type: bulk  # Choose primitive type
    description: "My custom agent description"
    model: "gpt-4o-mini"
    # ... primitive-specific config
  3. Write prompt.txt (optional):
    You are a specialized assistant for...
    
  4. Deploy: Agent automatically available via /api/agents endpoint

Creating New Primitives (Developer Perspective)

To add a new primitive type:

  1. Create primitive file: api/_lib/primitives/my_primitive.py
  2. Extend BasePrimitive:
    from .base import BasePrimitive
    
    class MyPrimitive(BasePrimitive):
        async def run(self, query, context, stream=False, **kwargs):
            # Implement primitive logic
            pass
  3. Register primitive:
    # In agent_loader.py
    PRIMITIVE_REGISTRY = {
        "role": RolePrimitive,
        "swarm": SwarmPrimitive,
        "bulk": BulkPrimitive,
        "pipeline": PipelinePrimitive,
        "my_primitive": MyPrimitive,  # Add here
    }
  4. Document: Update this file with primitive description

Design Principles

1. Composability

Primitives are building blocks, not endpoints. Users combine them to create custom agents.

2. Declarative Configuration

Agents are defined via YAML, not code. This makes them:

  • Easy to create
  • Version-controllable
  • Shareable across teams

3. Separation of Concerns

  • Primitives = How to execute (implementation)
  • Agents = What to execute (configuration)
  • Agent Loader = Wiring layer

4. Extensibility

Adding new primitives doesn't break existing agents. New primitives are added to registry, old agents keep working.


Comparison: Horizontal vs Vertical

❌ Vertical Approach (What NOT to do)

api/_lib/agents/
├── gtm_expert/        ← Single-purpose specialist
├── website_analyzer/  ← Single-purpose specialist
├── prompt_optimizer/  ← Single-purpose specialist
└── ocr_pdf/          ← Single-purpose specialist

Problems:

  • Each agent is a one-off
  • No reusability
  • Requires code changes to add agents
  • Not composable

✅ Horizontal Approach (Agora's design)

api/_lib/primitives/
├── role.py       ← Reusable building block
├── swarm.py      ← Reusable building block
├── bulk.py       ← Reusable building block
└── pipeline.py   ← Reusable building block

api/_lib/agents/
├── academic_writer/config.yaml    (type: role)
├── research_team/config.yaml      (type: swarm)
├── company_analyzer/config.yaml   (type: bulk)
└── content_pipeline/config.yaml   (type: pipeline)

Benefits:

  • Primitives are reused across agents
  • Users create agents via YAML (no code)
  • New primitives benefit all users
  • Composable and extensible

Future Primitives (Roadmap)

Memory Primitive (type: memory)

Stateful agents with long-term memory

Use Cases:

  • Conversational agents that remember context
  • Personalized assistants
  • Multi-session workflows

Router Primitive (type: router)

Dynamic agent selection based on query classification

Use Cases:

  • Auto-routing to best primitive
  • Multi-agent orchestration
  • Query triaging

MCP Primitive (type: mcp)

Model Context Protocol integration for tool use

Use Cases:

  • File system access
  • Database queries
  • External API calls

API Endpoints

GET /api/agents

List all available agents (auto-discovered from api/_lib/agents/ directory)

Response:

{
  "agents": [
    {"name": "academic_writer", "type": "role", "description": "..."},
    {"name": "research_team", "type": "swarm", "description": "..."},
    {"name": "company_analyzer", "type": "bulk", "description": "..."}
  ],
  "count": 3
}

POST /api/chat

Execute an agent (non-streaming)

Request:

{
  "query": "Research quantum computing applications",
  "agent_name": "research_team"
}

Response:

{
  "response": "...",
  "agent_used": "research_team",
  "tokens_used": 1523
}

GET /api/chat-stream

Execute an agent (SSE streaming)

Query Params: ?query=...&agent_name=research_team

Response: Server-Sent Events


Testing Primitives

Each primitive should have:

  1. Unit tests: Test primitive logic in isolation
  2. Integration tests: Test with agent loader
  3. E2E tests: Test via API endpoints

Example Test Structure:

tests/
├── primitives/
│   ├── test_role.py
│   ├── test_swarm.py
│   ├── test_bulk.py
│   └── test_pipeline.py
├── agents/
│   └── test_agent_loader.py
└── e2e/
    └── test_chat_api.py

Summary

Agora's architecture is fundamentally about composability:

  • Primitives = Horizontal building blocks
  • Agents = YAML configurations of primitives
  • Agent Loader = Dynamic instantiation system

Users create custom agents by composing primitives, not by writing code. Developers create new primitives to expand the platform's capabilities.

This architecture enables:

  • ✅ Easy agent creation (YAML only)
  • ✅ Reusable components (primitives)
  • ✅ Extensible platform (new primitives)
  • ✅ Clear separation of concerns (primitive vs config)
  • ✅ Composable workflows (pipeline, swarm)

The goal: Make building AI agents as simple as writing a YAML file.