Merge branch 'staging' into fix/testing
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(npm install:*)",
|
||||
"Bash(npm test:*)",
|
||||
"Skill(building-agents-construction)",
|
||||
"Skill(building-agents-construction:*)",
|
||||
"Bash(PYTHONPATH=core:exports pytest:*)",
|
||||
"mcp__agent-builder__create_session",
|
||||
"mcp__agent-builder__get_session_status",
|
||||
"mcp__agent-builder__set_goal",
|
||||
"mcp__agent-builder__list_mcp_servers",
|
||||
"mcp__agent-builder__test_node",
|
||||
"mcp__agent-builder__add_node",
|
||||
"mcp__agent-builder__add_edge",
|
||||
"mcp__agent-builder__validate_graph"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -16,15 +16,151 @@ Step-by-step guide for building goal-driven agent packages.
|
||||
|
||||
**Prerequisites:** Read `building-agents-core` for fundamental concepts.
|
||||
|
||||
## Step-by-Step Guide
|
||||
## CRITICAL: entry_points Format Reference
|
||||
|
||||
### Step 1: Create Package Structure
|
||||
**⚠️ Common Mistake Prevention:**
|
||||
|
||||
When user requests an agent, **immediately create the package**:
|
||||
The `entry_points` parameter in GraphSpec has a specific format that is easy to get wrong. This section exists because this mistake has caused production bugs.
|
||||
|
||||
### Correct Format
|
||||
|
||||
```python
|
||||
# 1. Create directory
|
||||
entry_points = {"start": "first-node-id"}
|
||||
```
|
||||
|
||||
**Examples from working agents:**
|
||||
|
||||
```python
|
||||
# From exports/outbound_sales_agent/agent.py
|
||||
entry_node = "lead-qualification"
|
||||
entry_points = {"start": "lead-qualification"}
|
||||
|
||||
# From exports/support_ticket_agent/agent.py (FIXED)
|
||||
entry_node = "parse-ticket"
|
||||
entry_points = {"start": "parse-ticket"}
|
||||
```
|
||||
|
||||
### WRONG Formats (DO NOT USE)
|
||||
|
||||
```python
|
||||
# ❌ WRONG: Using node ID as key with input keys as value
|
||||
entry_points = {
|
||||
"parse-ticket": ["ticket_content", "customer_id", "ticket_id"]
|
||||
}
|
||||
# Error: ValidationError: Input should be a valid string, got list
|
||||
|
||||
# ❌ WRONG: Using set instead of dict
|
||||
entry_points = {"parse-ticket"}
|
||||
# Error: ValidationError: Input should be a valid dictionary, got set
|
||||
|
||||
# ❌ WRONG: Missing "start" key
|
||||
entry_points = {"entry": "parse-ticket"}
|
||||
# Error: Graph execution fails, cannot find entry point
|
||||
```
|
||||
|
||||
### Validation Check
|
||||
|
||||
After writing graph configuration, ALWAYS validate:
|
||||
|
||||
```python
|
||||
# Check 1: Must be a dict
|
||||
assert isinstance(entry_points, dict), f"entry_points must be dict, got {type(entry_points)}"
|
||||
|
||||
# Check 2: Must have "start" key
|
||||
assert "start" in entry_points, f"entry_points must have 'start' key, got keys: {entry_points.keys()}"
|
||||
|
||||
# Check 3: "start" value must match entry_node
|
||||
assert entry_points["start"] == entry_node, f"entry_points['start']={entry_points['start']} must match entry_node={entry_node}"
|
||||
|
||||
# Check 4: Value must be a string (node ID)
|
||||
assert isinstance(entry_points["start"], str), f"entry_points['start'] must be string, got {type(entry_points['start'])}"
|
||||
```
|
||||
|
||||
**Why this matters:** GraphSpec uses Pydantic validation. The wrong format causes ValidationError at runtime, which blocks all agent execution and tests. This bug is not caught until you try to run the agent.
|
||||
|
||||
## Building Session Management with MCP
|
||||
|
||||
**MANDATORY**: Use the agent-builder MCP server's BuildSession system for automatic bookkeeping and persistence.
|
||||
|
||||
### Available MCP Session Tools
|
||||
|
||||
```python
|
||||
# Create new session (call FIRST before building)
|
||||
mcp__agent-builder__create_session(name="Support Ticket Agent")
|
||||
# Returns: session_id, automatically sets as active session
|
||||
|
||||
# Get current session status (use for progress tracking)
|
||||
status = mcp__agent-builder__get_session_status()
|
||||
# Returns: {
|
||||
# "session_id": "build_20250122_...",
|
||||
# "name": "Support Ticket Agent",
|
||||
# "has_goal": true,
|
||||
# "node_count": 5,
|
||||
# "edge_count": 7,
|
||||
# "nodes": ["parse-ticket", "categorize", ...],
|
||||
# "edges": [("parse-ticket", "categorize"), ...]
|
||||
# }
|
||||
|
||||
# List all saved sessions
|
||||
mcp__agent-builder__list_sessions()
|
||||
|
||||
# Load previous session
|
||||
mcp__agent-builder__load_session_by_id(session_id="build_...")
|
||||
|
||||
# Delete session
|
||||
mcp__agent-builder__delete_session(session_id="build_...")
|
||||
```
|
||||
|
||||
### How MCP Session Works
|
||||
|
||||
The BuildSession class (in `core/framework/mcp/agent_builder_server.py`) automatically:
|
||||
- **Persists to disk** after every operation (`_save_session()` called automatically)
|
||||
- **Tracks all components**: goal, nodes, edges, mcp_servers
|
||||
- **Maintains timestamps**: created_at, last_modified
|
||||
- **Stores to**: `~/.claude-code-agent-builder/sessions/`
|
||||
|
||||
When you call MCP tools like:
|
||||
- `mcp__agent-builder__set_goal(...)` - Automatically added to session.goal and saved
|
||||
- `mcp__agent-builder__add_node(...)` - Automatically added to session.nodes and saved
|
||||
- `mcp__agent-builder__add_edge(...)` - Automatically added to session.edges and saved
|
||||
|
||||
**No manual bookkeeping needed** - the MCP server handles it all!
|
||||
|
||||
### Show Progress to User
|
||||
|
||||
```python
|
||||
# Get session status to show progress
|
||||
status = json.loads(mcp__agent-builder__get_session_status())
|
||||
|
||||
print(f"\n📊 Building Progress:")
|
||||
print(f" Session: {status['name']}")
|
||||
print(f" Goal defined: {status['has_goal']}")
|
||||
print(f" Nodes: {status['node_count']}")
|
||||
print(f" Edges: {status['edge_count']}")
|
||||
print(f" Nodes added: {', '.join(status['nodes'])}")
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Automatic persistence - survive crashes/restarts
|
||||
- Clear audit trail - all operations logged
|
||||
- Session resume - continue from where you left off
|
||||
- Progress tracking built-in
|
||||
- No manual state management needed
|
||||
|
||||
## Step-by-Step Guide
|
||||
|
||||
### Step 1: Create Building Session & Package Structure
|
||||
|
||||
When user requests an agent, **immediately create MCP session and package**:
|
||||
|
||||
```python
|
||||
# 0. FIRST: Create MCP building session
|
||||
agent_name = "technical_research_agent" # snake_case
|
||||
session_result = mcp__agent-builder__create_session(name=agent_name.replace('_', ' ').title())
|
||||
session_id = json.loads(session_result)["session_id"]
|
||||
print(f"✅ Created building session: {session_id}")
|
||||
|
||||
# 1. Create directory
|
||||
package_path = f"exports/{agent_name}"
|
||||
|
||||
Bash(f"mkdir -p {package_path}/nodes")
|
||||
@@ -174,14 +310,22 @@ Edit(
|
||||
Open exports/technical_research_agent/agent.py to see the goal!
|
||||
```
|
||||
|
||||
**Note:** Goal is automatically tracked in MCP session. Use `mcp__agent-builder__get_session_status()` to check progress.
|
||||
|
||||
### Step 3: Add Nodes (Incremental)
|
||||
|
||||
**⚠️ IMPORTANT:** Before adding any node with tools, you MUST:
|
||||
**⚠️ CRITICAL VALIDATION REQUIREMENTS:**
|
||||
|
||||
Before adding any node with tools:
|
||||
1. Call `mcp__agent-builder__list_mcp_tools()` to discover available tools
|
||||
2. Verify each tool exists in the response
|
||||
3. If a tool doesn't exist, inform the user and ask how to proceed
|
||||
|
||||
After writing each node:
|
||||
4. **MANDATORY**: Validate with `mcp__agent-builder__test_node()` before proceeding
|
||||
5. **MANDATORY**: Check MCP session status to track progress
|
||||
6. Only proceed to next node after validation passes
|
||||
|
||||
For each node, **write immediately after approval**:
|
||||
|
||||
```python
|
||||
@@ -234,24 +378,36 @@ Open exports/technical_research_agent/nodes/__init__.py to see it!
|
||||
|
||||
**Repeat for each node.** User watches the file grow.
|
||||
|
||||
#### Optional: Validate Node with MCP Tools
|
||||
#### MANDATORY: Validate Each Node with MCP Tools
|
||||
|
||||
After writing a node, you can optionally use MCP tools for validation:
|
||||
After writing EVERY node, you MUST validate before proceeding:
|
||||
|
||||
```python
|
||||
# Node is already written to file. Now validate it:
|
||||
mcp__agent-builder__test_node(
|
||||
# Node is already written to file. Now VALIDATE IT (REQUIRED):
|
||||
validation_result = json.loads(mcp__agent-builder__test_node(
|
||||
node_id="analyze-request",
|
||||
test_input='{"query": "test query"}',
|
||||
mock_llm_response='{"analysis": "mock output"}'
|
||||
)
|
||||
))
|
||||
|
||||
# Returns validation result showing node behavior
|
||||
# This is OPTIONAL - for bookkeeping/validation only
|
||||
# The node already exists in the file!
|
||||
# Check validation result
|
||||
if validation_result["valid"]:
|
||||
# Show user validation passed
|
||||
print(f"✅ Node validation passed: analyze-request")
|
||||
|
||||
# Show session progress
|
||||
status = json.loads(mcp__agent-builder__get_session_status())
|
||||
print(f"📊 Session progress: {status['node_count']} nodes added")
|
||||
else:
|
||||
# STOP - Do not proceed until fixed
|
||||
print(f"❌ Node validation FAILED:")
|
||||
for error in validation_result["errors"]:
|
||||
print(f" - {error}")
|
||||
print("⚠️ Must fix node before proceeding to next component")
|
||||
# Ask user how to proceed
|
||||
```
|
||||
|
||||
**Key Point:** The node was written to `nodes/__init__.py` FIRST. The MCP tool is just for validation.
|
||||
**CRITICAL:** Do NOT proceed to the next node until validation passes. Bugs caught here prevent wasted work later.
|
||||
|
||||
### Step 4: Connect Edges
|
||||
|
||||
@@ -282,10 +438,15 @@ Edit(
|
||||
)
|
||||
|
||||
# Write entry points and terminal nodes
|
||||
# ⚠️ CRITICAL: entry_points format must be {"start": "node_id"}
|
||||
# Common mistake: {"node_id": ["input_keys"]} is WRONG
|
||||
# Correct format: {"start": "first-node-id"}
|
||||
# Reference: See exports/outbound_sales_agent/agent.py for example
|
||||
|
||||
graph_config = f'''
|
||||
# Graph configuration
|
||||
entry_node = "{entry_node_id}"
|
||||
entry_points = {entry_points}
|
||||
entry_points = {{"start": "{entry_node_id}"}} # CRITICAL: Must be {{"start": "node-id"}}
|
||||
pause_nodes = {pause_nodes}
|
||||
terminal_nodes = {terminal_nodes}
|
||||
|
||||
@@ -311,23 +472,101 @@ Edit(
|
||||
5 edges connecting 6 nodes
|
||||
```
|
||||
|
||||
#### Optional: Validate Graph Structure
|
||||
#### MANDATORY: Validate Graph Structure
|
||||
|
||||
After writing edges, optionally validate with MCP tools:
|
||||
After writing edges, you MUST validate before proceeding to finalization:
|
||||
|
||||
```python
|
||||
# Edges already written to agent.py. Now validate structure:
|
||||
mcp__agent-builder__validate_graph()
|
||||
# Edges already written to agent.py. Now VALIDATE STRUCTURE (REQUIRED):
|
||||
graph_validation = json.loads(mcp__agent-builder__validate_graph())
|
||||
|
||||
# Returns: unreachable nodes, missing connections, etc.
|
||||
# This is OPTIONAL - for validation only
|
||||
# Check for structural issues
|
||||
if graph_validation["valid"]:
|
||||
print("✅ Graph structure validated successfully")
|
||||
|
||||
# Show session summary
|
||||
status = json.loads(mcp__agent-builder__get_session_status())
|
||||
print(f" - Nodes: {status['node_count']}")
|
||||
print(f" - Edges: {status['edge_count']}")
|
||||
print(f" - Entry point: {entry_node_id}")
|
||||
else:
|
||||
print("❌ Graph validation FAILED:")
|
||||
for error in graph_validation["errors"]:
|
||||
print(f" ERROR: {error}")
|
||||
print("\n⚠️ Must fix graph structure before finalizing agent")
|
||||
# Ask user how to proceed
|
||||
|
||||
# Additional validation: Check entry_points format
|
||||
if not isinstance(entry_points, dict):
|
||||
print("❌ CRITICAL ERROR: entry_points must be a dict")
|
||||
print(f" Current value: {entry_points} (type: {type(entry_points)})")
|
||||
print(" Correct format: {'start': 'node-id'}")
|
||||
# STOP - This is the mistake that caused the support_ticket_agent bug
|
||||
|
||||
if entry_points.get("start") != entry_node_id:
|
||||
print("❌ CRITICAL ERROR: entry_points['start'] must match entry_node")
|
||||
print(f" entry_points: {entry_points}")
|
||||
print(f" entry_node: {entry_node_id}")
|
||||
print(" They must be consistent!")
|
||||
```
|
||||
|
||||
**CRITICAL:** Do NOT proceed to Step 5 (finalization) until graph validation passes. This checkpoint prevents structural bugs from reaching production.
|
||||
|
||||
### Step 5: Finalize Agent Class
|
||||
|
||||
Write the agent class:
|
||||
**Pre-flight checks before finalization:**
|
||||
|
||||
```python
|
||||
# MANDATORY: Verify all validations passed before finalizing
|
||||
print("\n🔍 Pre-finalization Checklist:")
|
||||
|
||||
# Get current session status
|
||||
status = json.loads(mcp__agent-builder__get_session_status())
|
||||
|
||||
checks_passed = True
|
||||
|
||||
# Check 1: Goal defined
|
||||
if not status["has_goal"]:
|
||||
print("❌ No goal defined")
|
||||
checks_passed = False
|
||||
else:
|
||||
print(f"✅ Goal defined: {status['goal_name']}")
|
||||
|
||||
# Check 2: Nodes added
|
||||
if status["node_count"] == 0:
|
||||
print("❌ No nodes added")
|
||||
checks_passed = False
|
||||
else:
|
||||
print(f"✅ {status['node_count']} nodes added: {', '.join(status['nodes'])}")
|
||||
|
||||
# Check 3: Edges added
|
||||
if status["edge_count"] == 0:
|
||||
print("❌ No edges added")
|
||||
checks_passed = False
|
||||
else:
|
||||
print(f"✅ {status['edge_count']} edges added")
|
||||
|
||||
# Check 4: Entry points format correct
|
||||
if not isinstance(entry_points, dict) or "start" not in entry_points:
|
||||
print("❌ CRITICAL: entry_points format incorrect")
|
||||
print(f" Current: {entry_points}")
|
||||
print(" Required: {'start': 'node-id'}")
|
||||
checks_passed = False
|
||||
else:
|
||||
print(f"✅ Entry points valid: {entry_points}")
|
||||
|
||||
if not checks_passed:
|
||||
print("\n⚠️ CANNOT PROCEED to finalization until all checks pass")
|
||||
print(" Fix the issues above first")
|
||||
# Ask user how to proceed or stop here
|
||||
return
|
||||
|
||||
print("\n✅ All pre-flight checks passed - proceeding to finalization\n")
|
||||
```
|
||||
|
||||
Write the agent class:
|
||||
|
||||
````python
|
||||
agent_class_code = f'''
|
||||
|
||||
class {agent_class_name}:
|
||||
@@ -500,7 +739,7 @@ python -m {agent_name} run --input '{{"key": "value"}}'
|
||||
|
||||
# Interactive shell
|
||||
python -m {agent_name} shell
|
||||
```
|
||||
````
|
||||
|
||||
## As Python Module
|
||||
|
||||
@@ -516,17 +755,19 @@ result = await default_agent.run({{"key": "value"}})
|
||||
- `nodes/__init__.py` - Node definitions
|
||||
- `config.py` - Runtime configuration
|
||||
- `__main__.py` - CLI interface
|
||||
'''
|
||||
'''
|
||||
|
||||
Write(
|
||||
file_path=f"{package_path}/README.md",
|
||||
content=readme_content
|
||||
file_path=f"{package_path}/README.md",
|
||||
content=readme_content
|
||||
)
|
||||
|
||||
```
|
||||
|
||||
**Show user:**
|
||||
|
||||
```
|
||||
|
||||
✅ Agent class written to agent.py
|
||||
✅ Package exports finalized in __init__.py
|
||||
✅ README.md generated
|
||||
@@ -534,11 +775,28 @@ Write(
|
||||
🎉 Agent complete: exports/technical_research_agent/
|
||||
|
||||
Commands:
|
||||
python -m technical_research_agent info
|
||||
python -m technical_research_agent validate
|
||||
python -m technical_research_agent run --input '{"topic": "..."}'
|
||||
python -m technical_research_agent info
|
||||
python -m technical_research_agent validate
|
||||
python -m technical_research_agent run --input '{"topic": "..."}'
|
||||
```
|
||||
|
||||
**Final session summary:**
|
||||
|
||||
```python
|
||||
# Show final MCP session status
|
||||
status = json.loads(mcp__agent-builder__get_session_status())
|
||||
|
||||
print("\n📊 Build Session Summary:")
|
||||
print(f" Session ID: {status['session_id']}")
|
||||
print(f" Agent: {status['name']}")
|
||||
print(f" Goal: {status['goal_name']}")
|
||||
print(f" Nodes: {status['node_count']}")
|
||||
print(f" Edges: {status['edge_count']}")
|
||||
print(f" MCP Servers: {status['mcp_servers_count']}")
|
||||
print("\n✅ Agent construction complete with full validation")
|
||||
print(f"\nSession saved to: ~/.claude-code-agent-builder/sessions/{status['session_id']}.json")
|
||||
````
|
||||
|
||||
## CLI Template
|
||||
|
||||
```python
|
||||
@@ -623,7 +881,7 @@ def shell():
|
||||
if __name__ == "__main__":
|
||||
cli()
|
||||
'''
|
||||
```
|
||||
````
|
||||
|
||||
## Testing During Build
|
||||
|
||||
@@ -677,11 +935,13 @@ response = AskUserQuestion(
|
||||
After completing construction:
|
||||
|
||||
**If agent structure complete:**
|
||||
|
||||
- Validate: `python -m agent_name validate`
|
||||
- Test basic execution: `python -m agent_name info`
|
||||
- Proceed to testing-agent skill for comprehensive tests
|
||||
|
||||
**If implementation needed:**
|
||||
|
||||
- Check for STATUS.md or IMPLEMENTATION_GUIDE.md in agent directory
|
||||
- May need Python functions or MCP tool integration
|
||||
|
||||
|
||||
+1
-3
@@ -9,12 +9,10 @@ workdir/
|
||||
.next/
|
||||
out/
|
||||
|
||||
# Environment files (generated from config.yaml)
|
||||
# Environment files
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
honeycomb/.env
|
||||
hive/.env
|
||||
|
||||
# User configuration (copied from .example)
|
||||
config.yaml
|
||||
|
||||
@@ -5,13 +5,13 @@
|
||||
"args": ["-m", "framework.mcp.agent_builder_server"],
|
||||
"cwd": "core",
|
||||
"env": {
|
||||
"PYTHONPATH": "../aden-tools/src"
|
||||
"PYTHONPATH": "../tools/src"
|
||||
}
|
||||
},
|
||||
"aden-tools": {
|
||||
"tools": {
|
||||
"command": "python",
|
||||
"args": ["mcp_server.py", "--stdio"],
|
||||
"cwd": "aden-tools",
|
||||
"cwd": "tools",
|
||||
"env": {
|
||||
"PYTHONPATH": "src"
|
||||
}
|
||||
|
||||
+24
-22
@@ -1,6 +1,6 @@
|
||||
# Contributing to Hive
|
||||
# Contributing to Aden Agent Framework
|
||||
|
||||
Thank you for your interest in contributing to Hive! This document provides guidelines and information for contributors.
|
||||
Thank you for your interest in contributing to the Aden Agent Framework! This document provides guidelines and information for contributors.
|
||||
|
||||
## Code of Conduct
|
||||
|
||||
@@ -12,24 +12,21 @@ By participating in this project, you agree to abide by our [Code of Conduct](CO
|
||||
2. Clone your fork: `git clone https://github.com/YOUR_USERNAME/hive.git`
|
||||
3. Create a feature branch: `git checkout -b feature/your-feature-name`
|
||||
4. Make your changes
|
||||
5. Run tests: `npm run test`
|
||||
5. Run tests: `PYTHONPATH=core:exports python -m pytest`
|
||||
6. Commit your changes following our commit conventions
|
||||
7. Push to your fork and submit a Pull Request
|
||||
|
||||
## Development Setup
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
npm install
|
||||
# Install Python packages
|
||||
./scripts/setup-python.sh
|
||||
|
||||
# Copy configuration
|
||||
cp config.yaml.example config.yaml
|
||||
# Verify installation
|
||||
python -c "import framework; import aden_tools; print('✓ Setup complete')"
|
||||
|
||||
# Generate environment files
|
||||
npm run setup
|
||||
|
||||
# Start development environment
|
||||
docker compose up
|
||||
# Install Claude Code skills (optional)
|
||||
./quickstart.sh
|
||||
```
|
||||
|
||||
## Commit Convention
|
||||
@@ -77,28 +74,33 @@ feat(component): add new feature description
|
||||
|
||||
## Project Structure
|
||||
|
||||
- `honeycomb/` - React frontend application
|
||||
- `hive/` - Node.js backend API
|
||||
- `core/` - Core framework (agent runtime, graph executor, protocols)
|
||||
- `tools/` - MCP Tools Package (19 tools for agent capabilities)
|
||||
- `exports/` - Agent packages and examples
|
||||
- `docs/` - Documentation
|
||||
- `scripts/` - Build and utility scripts
|
||||
- `.claude/` - Claude Code skills for building/testing agents
|
||||
|
||||
## Code Style
|
||||
|
||||
- Use TypeScript for all new code
|
||||
- Follow existing code patterns
|
||||
- Use Python 3.11+ for all new code
|
||||
- Follow PEP 8 style guide
|
||||
- Add type hints to function signatures
|
||||
- Write docstrings for classes and public functions
|
||||
- Use meaningful variable and function names
|
||||
- Add comments for complex logic
|
||||
- Keep functions focused and small
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
npm run test
|
||||
# Run all tests for the framework
|
||||
cd core && python -m pytest
|
||||
|
||||
# Run tests for a specific package
|
||||
npm run test --workspace=honeycomb
|
||||
npm run test --workspace=hive
|
||||
# Run all tests for tools
|
||||
cd tools && python -m pytest
|
||||
|
||||
# Run tests for a specific agent
|
||||
PYTHONPATH=core:exports python -m agent_name test
|
||||
```
|
||||
|
||||
## Questions?
|
||||
|
||||
+400
-838
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,347 @@
|
||||
# Agent Development Environment Setup
|
||||
|
||||
Complete setup guide for building and running goal-driven agents with the Aden Agent Framework.
|
||||
|
||||
## Quick Setup
|
||||
|
||||
```bash
|
||||
# Run the automated setup script
|
||||
./scripts/setup-python.sh
|
||||
```
|
||||
|
||||
This will:
|
||||
|
||||
- Check Python version (requires 3.10+, recommends 3.11+)
|
||||
- Install the core framework package (`framework`)
|
||||
- Install the tools package (`aden_tools`)
|
||||
- Fix package compatibility issues (openai + litellm)
|
||||
- Verify all installations
|
||||
|
||||
## Manual Setup (Alternative)
|
||||
|
||||
If you prefer to set up manually or the script fails:
|
||||
|
||||
### 1. Install Core Framework
|
||||
|
||||
```bash
|
||||
cd core
|
||||
pip install -e .
|
||||
```
|
||||
|
||||
### 2. Install Tools Package
|
||||
|
||||
```bash
|
||||
cd tools
|
||||
pip install -e .
|
||||
```
|
||||
|
||||
### 3. Upgrade OpenAI Package
|
||||
|
||||
```bash
|
||||
# litellm requires openai >= 1.0.0
|
||||
pip install --upgrade "openai>=1.0.0"
|
||||
```
|
||||
|
||||
### 4. Verify Installation
|
||||
|
||||
```bash
|
||||
python -c "import framework; print('✓ framework OK')"
|
||||
python -c "import aden_tools; print('✓ aden_tools OK')"
|
||||
python -c "import litellm; print('✓ litellm OK')"
|
||||
```
|
||||
|
||||
## Requirements
|
||||
|
||||
### Python Version
|
||||
|
||||
- **Minimum:** Python 3.10
|
||||
- **Recommended:** Python 3.11 or 3.12
|
||||
- **Tested on:** Python 3.11, 3.12, 3.13
|
||||
|
||||
### System Requirements
|
||||
|
||||
- pip (latest version)
|
||||
- 2GB+ RAM
|
||||
- Internet connection (for LLM API calls)
|
||||
|
||||
### API Keys (Optional)
|
||||
|
||||
For running agents with real LLMs:
|
||||
|
||||
```bash
|
||||
export ANTHROPIC_API_KEY="your-key-here"
|
||||
```
|
||||
|
||||
## Running Agents
|
||||
|
||||
All agent commands must be run from the project root with `PYTHONPATH` set:
|
||||
|
||||
```bash
|
||||
# From /home/timothy/oss/hive/ directory
|
||||
PYTHONPATH=core:exports python -m agent_name COMMAND
|
||||
```
|
||||
|
||||
### Example: Support Ticket Agent
|
||||
|
||||
```bash
|
||||
# Validate agent structure
|
||||
PYTHONPATH=core:exports python -m support_ticket_agent validate
|
||||
|
||||
# Show agent information
|
||||
PYTHONPATH=core:exports python -m support_ticket_agent info
|
||||
|
||||
# Run agent with input
|
||||
PYTHONPATH=core:exports python -m support_ticket_agent run --input '{
|
||||
"ticket_content": "My login is broken. Error 401.",
|
||||
"customer_id": "CUST-123",
|
||||
"ticket_id": "TKT-456"
|
||||
}'
|
||||
|
||||
# Run in mock mode (no LLM calls)
|
||||
PYTHONPATH=core:exports python -m support_ticket_agent run --mock --input '{...}'
|
||||
```
|
||||
|
||||
### Example: Other Agents
|
||||
|
||||
```bash
|
||||
# Market Research Agent
|
||||
PYTHONPATH=core:exports python -m market_research_agent info
|
||||
|
||||
# Outbound Sales Agent
|
||||
PYTHONPATH=core:exports python -m outbound_sales_agent validate
|
||||
|
||||
# Personal Assistant Agent
|
||||
PYTHONPATH=core:exports python -m personal_assistant_agent run --input '{...}'
|
||||
```
|
||||
|
||||
## Building New Agents
|
||||
|
||||
Use Claude Code CLI with the agent building skills:
|
||||
|
||||
### 1. Install Skills (One-time)
|
||||
|
||||
```bash
|
||||
./quickstart.sh
|
||||
```
|
||||
|
||||
This installs:
|
||||
|
||||
- `/building-agents` - Build new agents
|
||||
- `/testing-agent` - Test agents
|
||||
|
||||
### 2. Build an Agent
|
||||
|
||||
```
|
||||
claude> /building-agents
|
||||
```
|
||||
|
||||
Follow the prompts to:
|
||||
|
||||
1. Define your agent's goal
|
||||
2. Design the workflow nodes
|
||||
3. Connect edges
|
||||
4. Generate the agent package
|
||||
|
||||
### 3. Test Your Agent
|
||||
|
||||
```
|
||||
claude> /testing-agent
|
||||
```
|
||||
|
||||
Creates comprehensive test suites for your agent.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "ModuleNotFoundError: No module named 'framework'"
|
||||
|
||||
**Solution:** Install the core package:
|
||||
|
||||
```bash
|
||||
cd core && pip install -e .
|
||||
```
|
||||
|
||||
### "ModuleNotFoundError: No module named 'aden_tools'"
|
||||
|
||||
**Solution:** Install the tools package:
|
||||
|
||||
```bash
|
||||
cd tools && pip install -e .
|
||||
```
|
||||
|
||||
Or run the setup script:
|
||||
|
||||
```bash
|
||||
./scripts/setup-python.sh
|
||||
```
|
||||
|
||||
### "ModuleNotFoundError: No module named 'openai.\_models'"
|
||||
|
||||
**Cause:** Outdated `openai` package (0.27.x) incompatible with `litellm`
|
||||
|
||||
**Solution:** Upgrade openai:
|
||||
|
||||
```bash
|
||||
pip install --upgrade "openai>=1.0.0"
|
||||
```
|
||||
|
||||
### "No module named 'support_ticket_agent'"
|
||||
|
||||
**Cause:** Not running from project root or missing PYTHONPATH
|
||||
|
||||
**Solution:** Ensure you're in `/home/timothy/oss/hive/` and use:
|
||||
|
||||
```bash
|
||||
PYTHONPATH=core:exports python -m support_ticket_agent validate
|
||||
```
|
||||
|
||||
### Agent imports fail with "broken installation"
|
||||
|
||||
**Symptom:** `pip list` shows packages pointing to non-existent directories
|
||||
|
||||
**Solution:** Reinstall packages properly:
|
||||
|
||||
```bash
|
||||
# Remove broken installations
|
||||
pip uninstall -y framework tools aden-tools
|
||||
|
||||
# Reinstall correctly
|
||||
cd /home/timothy/oss/hive
|
||||
./scripts/setup-python.sh
|
||||
```
|
||||
|
||||
## Package Structure
|
||||
|
||||
The Hive framework consists of three Python packages:
|
||||
|
||||
```
|
||||
hive/
|
||||
├── core/ # Core framework (runtime, graph executor, LLM providers)
|
||||
│ ├── framework/
|
||||
│ ├── pyproject.toml
|
||||
│ └── requirements.txt
|
||||
│
|
||||
├── tools/ # Tools and MCP servers
|
||||
│ ├── src/
|
||||
│ │ └── aden_tools/ # Actual package location
|
||||
│ ├── pyproject.toml
|
||||
│ └── README.md
|
||||
│
|
||||
└── exports/ # Agent packages (your agents go here)
|
||||
├── support_ticket_agent/
|
||||
├── market_research_agent/
|
||||
├── outbound_sales_agent/
|
||||
└── personal_assistant_agent/
|
||||
```
|
||||
|
||||
### Why PYTHONPATH is Required
|
||||
|
||||
The packages are installed in **editable mode** (`pip install -e`), which means:
|
||||
|
||||
- `framework` and `aden_tools` are globally importable (no PYTHONPATH needed)
|
||||
- `exports` is NOT installed as a package (PYTHONPATH required)
|
||||
|
||||
This design allows agents in `exports/` to be:
|
||||
|
||||
- Developed independently
|
||||
- Version controlled separately
|
||||
- Deployed as standalone packages
|
||||
|
||||
## Development Workflow
|
||||
|
||||
### 1. Setup (Once)
|
||||
|
||||
```bash
|
||||
./scripts/setup-python.sh
|
||||
```
|
||||
|
||||
### 2. Build Agent (Claude Code)
|
||||
|
||||
```
|
||||
claude> /building-agents
|
||||
Enter goal: "Build an agent that processes customer support tickets"
|
||||
```
|
||||
|
||||
### 3. Validate Agent
|
||||
|
||||
```bash
|
||||
PYTHONPATH=core:exports python -m support_ticket_agent validate
|
||||
```
|
||||
|
||||
### 4. Test Agent
|
||||
|
||||
```
|
||||
claude> /testing-agent
|
||||
```
|
||||
|
||||
### 5. Run Agent
|
||||
|
||||
```bash
|
||||
PYTHONPATH=core:exports python -m support_ticket_agent run --input '{...}'
|
||||
```
|
||||
|
||||
## IDE Setup
|
||||
|
||||
### VSCode
|
||||
|
||||
Add to `.vscode/settings.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"python.analysis.extraPaths": [
|
||||
"${workspaceFolder}/core",
|
||||
"${workspaceFolder}/exports"
|
||||
],
|
||||
"python.autoComplete.extraPaths": [
|
||||
"${workspaceFolder}/core",
|
||||
"${workspaceFolder}/exports"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### PyCharm
|
||||
|
||||
1. Open Project Settings → Project Structure
|
||||
2. Mark `core` as Sources Root
|
||||
3. Mark `exports` as Sources Root
|
||||
|
||||
## Environment Variables
|
||||
|
||||
### Required for LLM Operations
|
||||
|
||||
```bash
|
||||
export ANTHROPIC_API_KEY="sk-ant-..."
|
||||
```
|
||||
|
||||
### Optional Configuration
|
||||
|
||||
```bash
|
||||
# Credentials storage location (default: ~/.aden/credentials)
|
||||
export ADEN_CREDENTIALS_PATH="/custom/path"
|
||||
|
||||
# Agent storage location (default: /tmp)
|
||||
export AGENT_STORAGE_PATH="/custom/storage"
|
||||
```
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- **Framework Documentation:** [core/README.md](core/README.md)
|
||||
- **Tools Documentation:** [tools/README.md](tools/README.md)
|
||||
- **Example Agents:** [exports/](exports/)
|
||||
- **Agent Building Guide:** [.claude/skills/building-agents-construction/SKILL.md](.claude/skills/building-agents-construction/SKILL.md)
|
||||
- **Testing Guide:** [.claude/skills/testing-agent/SKILL.md](.claude/skills/testing-agent/SKILL.md)
|
||||
|
||||
## Contributing
|
||||
|
||||
When contributing agent packages:
|
||||
|
||||
1. Place agents in `exports/agent_name/`
|
||||
2. Follow the standard agent structure (see existing agents)
|
||||
3. Include README.md with usage instructions
|
||||
4. Add tests if using `/testing-agent`
|
||||
5. Document required environment variables
|
||||
|
||||
## Support
|
||||
|
||||
- **Issues:** https://github.com/adenhq/hive/issues
|
||||
- **Discord:** https://discord.com/invite/MXE49hrKDk
|
||||
- **Documentation:** https://docs.adenhq.com/
|
||||
+339
@@ -0,0 +1,339 @@
|
||||
<p align="center">
|
||||
<img width="100%" alt="Hive Banner" src="https://storage.googleapis.com/aden-prod-assets/website/aden-title-card.png" />
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="README.md">English</a> |
|
||||
<a href="README.zh-CN.md">简体中文</a> |
|
||||
<a href="README.es.md">Español</a> |
|
||||
<a href="README.pt.md">Português</a> |
|
||||
<a href="README.ja.md">日本語</a> |
|
||||
<a href="README.ru.md">Русский</a>
|
||||
</p>
|
||||
|
||||
[](https://github.com/adenhq/hive/blob/main/LICENSE)
|
||||
[](https://www.ycombinator.com/companies/aden)
|
||||
[](https://hub.docker.com/u/adenhq)
|
||||
[](https://discord.com/invite/MXE49hrKDk)
|
||||
[](https://x.com/aden_hq)
|
||||
[](https://www.linkedin.com/company/teamaden/)
|
||||
|
||||
<p align="center">
|
||||
<img src="https://img.shields.io/badge/AI_Agents-Self--Improving-brightgreen?style=flat-square" alt="AI Agents" />
|
||||
<img src="https://img.shields.io/badge/Multi--Agent-Systems-blue?style=flat-square" alt="Multi-Agent" />
|
||||
<img src="https://img.shields.io/badge/Goal--Driven-Development-purple?style=flat-square" alt="Goal-Driven" />
|
||||
<img src="https://img.shields.io/badge/Human--in--the--Loop-orange?style=flat-square" alt="HITL" />
|
||||
<img src="https://img.shields.io/badge/Production--Ready-red?style=flat-square" alt="Production" />
|
||||
</p>
|
||||
<p align="center">
|
||||
<img src="https://img.shields.io/badge/OpenAI-supported-412991?style=flat-square&logo=openai" alt="OpenAI" />
|
||||
<img src="https://img.shields.io/badge/Anthropic-supported-d4a574?style=flat-square" alt="Anthropic" />
|
||||
<img src="https://img.shields.io/badge/Google_Gemini-supported-4285F4?style=flat-square&logo=google" alt="Gemini" />
|
||||
<img src="https://img.shields.io/badge/MCP-19_Tools-00ADD8?style=flat-square" alt="MCP" />
|
||||
</p>
|
||||
|
||||
## Descripción General
|
||||
|
||||
Construye agentes de IA confiables y auto-mejorables sin codificar flujos de trabajo. Define tu objetivo a través de una conversación con un agente de codificación, y el framework genera un grafo de nodos con código de conexión creado dinámicamente. Cuando algo falla, el framework captura los datos del error, evoluciona el agente a través del agente de codificación y lo vuelve a desplegar. Los nodos de intervención humana integrados, la gestión de credenciales y el monitoreo en tiempo real te dan control sin sacrificar la adaptabilidad.
|
||||
|
||||
Visita [adenhq.com](https://adenhq.com) para documentación completa, ejemplos y guías.
|
||||
|
||||
## ¿Qué es Aden?
|
||||
|
||||
<p align="center">
|
||||
<img width="100%" alt="Aden Architecture" src="docs/assets/aden-architecture-diagram.jpg" />
|
||||
</p>
|
||||
|
||||
Aden es una plataforma para construir, desplegar, operar y adaptar agentes de IA:
|
||||
|
||||
- **Construir** - Un Agente de Codificación genera Agentes de Trabajo especializados (Ventas, Marketing, Operaciones) a partir de objetivos en lenguaje natural
|
||||
- **Desplegar** - Despliegue headless con integración CI/CD y gestión completa del ciclo de vida de API
|
||||
- **Operar** - Monitoreo en tiempo real, observabilidad y guardarraíles de ejecución mantienen los agentes confiables
|
||||
- **Adaptar** - Evaluación continua, supervisión y adaptación aseguran que los agentes mejoren con el tiempo
|
||||
- **Infraestructura** - Memoria compartida, integraciones LLM, herramientas y habilidades impulsan cada agente
|
||||
|
||||
## Enlaces Rápidos
|
||||
|
||||
- **[Documentación](https://docs.adenhq.com/)** - Guías completas y referencia de API
|
||||
- **[Guía de Auto-Hospedaje](https://docs.adenhq.com/getting-started/quickstart)** - Despliega Hive en tu infraestructura
|
||||
- **[Registro de Cambios](https://github.com/adenhq/hive/releases)** - Últimas actualizaciones y versiones
|
||||
<!-- - **[Hoja de Ruta](https://adenhq.com/roadmap)** - Funciones y planes próximos -->
|
||||
- **[Reportar Problemas](https://github.com/adenhq/hive/issues)** - Reportes de bugs y solicitudes de funciones
|
||||
|
||||
## Inicio Rápido
|
||||
|
||||
### Prerrequisitos
|
||||
|
||||
- [Python 3.11+](https://www.python.org/downloads/) - Para desarrollo de agentes
|
||||
- [Docker](https://docs.docker.com/get-docker/) (v20.10+) - Opcional, para herramientas en contenedores
|
||||
|
||||
### Instalación
|
||||
|
||||
```bash
|
||||
# Clonar el repositorio
|
||||
git clone https://github.com/adenhq/hive.git
|
||||
cd hive
|
||||
|
||||
# Ejecutar configuración del entorno Python
|
||||
./scripts/setup-python.sh
|
||||
```
|
||||
|
||||
Esto instala:
|
||||
- **framework** - Runtime del agente principal y ejecutor de grafos
|
||||
- **aden_tools** - 19 herramientas MCP para capacidades de agentes
|
||||
- Todas las dependencias requeridas
|
||||
|
||||
### Construye Tu Primer Agente
|
||||
|
||||
```bash
|
||||
# Instalar habilidades de Claude Code (una vez)
|
||||
./quickstart.sh
|
||||
|
||||
# Construir un agente usando Claude Code
|
||||
claude> /building-agents
|
||||
|
||||
# Probar tu agente
|
||||
claude> /testing-agent
|
||||
|
||||
# Ejecutar tu agente
|
||||
PYTHONPATH=core:exports python -m your_agent_name run --input '{...}'
|
||||
```
|
||||
|
||||
**[📖 Guía de Configuración Completa](ENVIRONMENT_SETUP.md)** - Instrucciones detalladas para desarrollo de agentes
|
||||
|
||||
## Características
|
||||
|
||||
- **Desarrollo Orientado a Objetivos** - Define objetivos en lenguaje natural; el agente de codificación genera el grafo de agentes y el código de conexión para lograrlos
|
||||
- **Agentes Auto-Adaptables** - El framework captura fallos, actualiza objetivos y actualiza el grafo de agentes
|
||||
- **Conexiones de Nodos Dinámicas** - Sin aristas predefinidas; el código de conexión es generado por cualquier LLM capaz basado en tus objetivos
|
||||
- **Nodos Envueltos en SDK** - Cada nodo obtiene memoria compartida, memoria RLM local, monitoreo, herramientas y acceso LLM de serie
|
||||
- **Humano en el Bucle** - Nodos de intervención que pausan la ejecución para entrada humana con tiempos de espera y escalación configurables
|
||||
- **Observabilidad en Tiempo Real** - Streaming WebSocket para monitoreo en vivo de ejecución de agentes, decisiones y comunicación entre nodos
|
||||
- **Control de Costos y Presupuesto** - Establece límites de gasto, limitadores y políticas de degradación automática de modelos
|
||||
- **Listo para Producción** - Auto-hospedable, construido para escala y confiabilidad
|
||||
|
||||
## Por Qué Aden
|
||||
|
||||
Los frameworks de agentes tradicionales requieren que diseñes manualmente flujos de trabajo, definas interacciones de agentes y manejes fallos de forma reactiva. Aden invierte este paradigma—**describes resultados, y el sistema se construye solo**.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph BUILD["🏗️ BUILD"]
|
||||
GOAL["Define Goal<br/>+ Success Criteria"] --> NODES["Add Nodes<br/>LLM/Router/Function"]
|
||||
NODES --> EDGES["Connect Edges<br/>on_success/failure/conditional"]
|
||||
EDGES --> TEST["Test & Validate"] --> APPROVE["Approve & Export"]
|
||||
end
|
||||
|
||||
subgraph EXPORT["📦 EXPORT"]
|
||||
direction TB
|
||||
JSON["agent.json<br/>(GraphSpec)"]
|
||||
TOOLS["tools.py<br/>(Functions)"]
|
||||
MCP["mcp_servers.json<br/>(Integrations)"]
|
||||
end
|
||||
|
||||
subgraph RUN["🚀 RUNTIME"]
|
||||
LOAD["AgentRunner<br/>Load + Parse"] --> SETUP["Setup Runtime<br/>+ ToolRegistry"]
|
||||
SETUP --> EXEC["GraphExecutor<br/>Execute Nodes"]
|
||||
|
||||
subgraph DECISION["Decision Recording"]
|
||||
DEC1["runtime.decide()<br/>intent → options → choice"]
|
||||
DEC2["runtime.record_outcome()<br/>success, result, metrics"]
|
||||
end
|
||||
end
|
||||
|
||||
subgraph INFRA["⚙️ INFRASTRUCTURE"]
|
||||
CTX["NodeContext<br/>memory • llm • tools"]
|
||||
STORE[("FileStorage<br/>Runs & Decisions")]
|
||||
end
|
||||
|
||||
APPROVE --> EXPORT
|
||||
EXPORT --> LOAD
|
||||
EXEC --> DECISION
|
||||
EXEC --> CTX
|
||||
DECISION --> STORE
|
||||
STORE -.->|"Analyze & Improve"| NODES
|
||||
|
||||
style BUILD fill:#ffbe42,stroke:#cc5d00,stroke-width:3px,color:#333
|
||||
style EXPORT fill:#fff59d,stroke:#ed8c00,stroke-width:2px,color:#333
|
||||
style RUN fill:#ffb100,stroke:#cc5d00,stroke-width:3px,color:#333
|
||||
style DECISION fill:#ffcc80,stroke:#ed8c00,stroke-width:2px,color:#333
|
||||
style INFRA fill:#e8763d,stroke:#cc5d00,stroke-width:3px,color:#fff
|
||||
style STORE fill:#ed8c00,stroke:#cc5d00,stroke-width:2px,color:#fff
|
||||
```
|
||||
|
||||
### La Ventaja de Aden
|
||||
|
||||
| Frameworks Tradicionales | Aden |
|
||||
|--------------------------|------|
|
||||
| Codificar flujos de trabajo de agentes | Describir objetivos en lenguaje natural |
|
||||
| Definición manual de grafos | Grafos de agentes auto-generados |
|
||||
| Manejo reactivo de errores | Auto-evolución proactiva |
|
||||
| Configuraciones de herramientas estáticas | Nodos dinámicos envueltos en SDK |
|
||||
| Configuración de monitoreo separada | Observabilidad en tiempo real integrada |
|
||||
| Gestión de presupuesto DIY | Controles de costos y degradación integrados |
|
||||
|
||||
### Cómo Funciona
|
||||
|
||||
1. **Define Tu Objetivo** → Describe lo que quieres lograr en lenguaje simple
|
||||
2. **El Agente de Codificación Genera** → Crea el grafo de agentes, código de conexión y casos de prueba
|
||||
3. **Los Trabajadores Ejecutan** → Los nodos envueltos en SDK se ejecutan con observabilidad completa y acceso a herramientas
|
||||
4. **El Plano de Control Monitorea** → Métricas en tiempo real, aplicación de presupuesto, gestión de políticas
|
||||
5. **Auto-Mejora** → En caso de fallo, el sistema evoluciona el grafo y lo vuelve a desplegar automáticamente
|
||||
|
||||
## Cómo se Compara Aden
|
||||
|
||||
Aden adopta un enfoque fundamentalmente diferente al desarrollo de agentes. Mientras que la mayoría de los frameworks requieren que codifiques flujos de trabajo o definas manualmente grafos de agentes, Aden usa un **agente de codificación para generar todo tu sistema de agentes** a partir de objetivos en lenguaje natural. Cuando los agentes fallan, el framework no solo registra errores—**evoluciona automáticamente el grafo de agentes** y lo vuelve a desplegar.
|
||||
|
||||
> **Nota:** Para la tabla de comparación detallada de frameworks y preguntas frecuentes, consulta el [README.md](README.md) en inglés.
|
||||
|
||||
### Cuándo Elegir Aden
|
||||
|
||||
Elige Aden cuando necesites:
|
||||
|
||||
- Agentes que **se auto-mejoren a partir de fallos** sin intervención manual
|
||||
- **Desarrollo orientado a objetivos** donde describes resultados, no flujos de trabajo
|
||||
- **Confiabilidad en producción** con recuperación y redespliegue automáticos
|
||||
- **Iteración rápida** en arquitecturas de agentes sin reescribir código
|
||||
- **Observabilidad completa** con monitoreo en tiempo real y supervisión humana
|
||||
|
||||
Elige otros frameworks cuando necesites:
|
||||
|
||||
- **Flujos de trabajo predecibles y con tipos seguros** (PydanticAI, Mastra)
|
||||
- **RAG y procesamiento de documentos** (LlamaIndex, Haystack)
|
||||
- **Investigación sobre emergencia de agentes** (CAMEL)
|
||||
- **Voz/multimodal en tiempo real** (TEN Framework)
|
||||
- **Encadenamiento simple de componentes** (LangChain, Swarm)
|
||||
|
||||
## Estructura del Proyecto
|
||||
|
||||
```
|
||||
hive/
|
||||
├── core/ # Framework principal - Runtime de agentes, ejecutor de grafos, protocolos
|
||||
├── tools/ # Paquete de Herramientas MCP - 19 herramientas para capacidades de agentes
|
||||
├── exports/ # Paquetes de Agentes - Agentes pre-construidos y ejemplos
|
||||
├── docs/ # Documentación y guías
|
||||
├── scripts/ # Scripts de construcción y utilidades
|
||||
├── .claude/ # Habilidades de Claude Code para construir agentes
|
||||
├── ENVIRONMENT_SETUP.md # Guía de configuración de Python para desarrollo de agentes
|
||||
├── DEVELOPER.md # Guía del desarrollador
|
||||
├── CONTRIBUTING.md # Directrices de contribución
|
||||
└── ROADMAP.md # Hoja de ruta del producto
|
||||
```
|
||||
|
||||
## Desarrollo
|
||||
|
||||
### Desarrollo de Agentes en Python
|
||||
|
||||
Para construir y ejecutar agentes orientados a objetivos con el framework:
|
||||
|
||||
```bash
|
||||
# Configuración única
|
||||
./scripts/setup-python.sh
|
||||
|
||||
# Esto instala:
|
||||
# - paquete framework (runtime principal)
|
||||
# - paquete aden_tools (19 herramientas MCP)
|
||||
# - Todas las dependencias
|
||||
|
||||
# Construir nuevos agentes usando habilidades de Claude Code
|
||||
claude> /building-agents
|
||||
|
||||
# Probar agentes
|
||||
claude> /testing-agent
|
||||
|
||||
# Ejecutar agentes
|
||||
PYTHONPATH=core:exports python -m agent_name run --input '{...}'
|
||||
```
|
||||
|
||||
Consulta [ENVIRONMENT_SETUP.md](ENVIRONMENT_SETUP.md) para instrucciones de configuración completas.
|
||||
|
||||
## Documentación
|
||||
|
||||
- **[Guía del Desarrollador](DEVELOPER.md)** - Guía completa para desarrolladores
|
||||
- [Primeros Pasos](docs/getting-started.md) - Instrucciones de configuración rápida
|
||||
- [Guía de Configuración](docs/configuration.md) - Todas las opciones de configuración
|
||||
- [Visión General de Arquitectura](docs/architecture.md) - Diseño y estructura del sistema
|
||||
|
||||
## Hoja de Ruta
|
||||
|
||||
El Framework de Agentes Aden tiene como objetivo ayudar a los desarrolladores a construir agentes auto-adaptativos orientados a resultados. Encuentra nuestra hoja de ruta aquí
|
||||
|
||||
[ROADMAP.md](ROADMAP.md)
|
||||
|
||||
```mermaid
|
||||
timeline
|
||||
title Aden Agent Framework Roadmap
|
||||
section Foundation
|
||||
Architecture : Node-Based Architecture : Python SDK : LLM Integration (OpenAI, Anthropic, Google) : Communication Protocol
|
||||
Coding Agent : Goal Creation Session : Worker Agent Creation : MCP Tools Integration
|
||||
Worker Agent : Human-in-the-Loop : Callback Handlers : Intervention Points : Streaming Interface
|
||||
Tools : File Use : Memory (STM/LTM) : Web Search : Web Scraper : Audit Trail
|
||||
Core : Eval System : Pydantic Validation : Docker Deployment : Documentation : Sample Agents
|
||||
section Expansion
|
||||
Intelligence : Guardrails : Streaming Mode : Semantic Search
|
||||
Platform : JavaScript SDK : Custom Tool Integrator : Credential Store
|
||||
Deployment : Self-Hosted : Cloud Services : CI/CD Pipeline
|
||||
Templates : Sales Agent : Marketing Agent : Analytics Agent : Training Agent : Smart Form Agent
|
||||
```
|
||||
|
||||
## Comunidad y Soporte
|
||||
|
||||
Usamos [Discord](https://discord.com/invite/MXE49hrKDk) para soporte, solicitudes de funciones y discusiones de la comunidad.
|
||||
|
||||
- Discord - [Únete a nuestra comunidad](https://discord.com/invite/MXE49hrKDk)
|
||||
- Twitter/X - [@adenhq](https://x.com/aden_hq)
|
||||
- LinkedIn - [Página de la Empresa](https://www.linkedin.com/company/teamaden/)
|
||||
|
||||
## Contribuir
|
||||
|
||||
¡Damos la bienvenida a las contribuciones! Por favor consulta [CONTRIBUTING.md](CONTRIBUTING.md) para las directrices.
|
||||
|
||||
1. Haz fork del repositorio
|
||||
2. Crea tu rama de funcionalidad (`git checkout -b feature/amazing-feature`)
|
||||
3. Haz commit de tus cambios (`git commit -m 'Add amazing feature'`)
|
||||
4. Haz push a la rama (`git push origin feature/amazing-feature`)
|
||||
5. Abre un Pull Request
|
||||
|
||||
## Únete a Nuestro Equipo
|
||||
|
||||
**¡Estamos contratando!** Únete a nosotros en roles de ingeniería, investigación y comercialización.
|
||||
|
||||
[Ver Posiciones Abiertas](https://jobs.adenhq.com/a8cec478-cdbc-473c-bbd4-f4b7027ec193/applicant)
|
||||
|
||||
## Seguridad
|
||||
|
||||
Para preocupaciones de seguridad, por favor consulta [SECURITY.md](SECURITY.md).
|
||||
|
||||
## Licencia
|
||||
|
||||
Este proyecto está licenciado bajo la Licencia Apache 2.0 - consulta el archivo [LICENSE](LICENSE) para más detalles.
|
||||
|
||||
## Preguntas Frecuentes (FAQ)
|
||||
|
||||
> **Nota:** Para las preguntas frecuentes completas, consulta el [README.md](README.md) en inglés.
|
||||
|
||||
**P: ¿Aden depende de LangChain u otros frameworks de agentes?**
|
||||
|
||||
No. Aden está construido desde cero sin dependencias de LangChain, CrewAI u otros frameworks de agentes. El framework está diseñado para ser ligero y flexible, generando grafos de agentes dinámicamente en lugar de depender de componentes predefinidos.
|
||||
|
||||
**P: ¿Qué proveedores de LLM soporta Aden?**
|
||||
|
||||
Aden soporta más de 100 proveedores de LLM a través de la integración de LiteLLM, incluyendo OpenAI (GPT-4, GPT-4o), Anthropic (modelos Claude), Google Gemini, Mistral, Groq y muchos más. Simplemente configura la variable de entorno de la clave API apropiada y especifica el nombre del modelo.
|
||||
|
||||
**P: ¿Aden es de código abierto?**
|
||||
|
||||
Sí, Aden es completamente de código abierto bajo la Licencia Apache 2.0. Fomentamos activamente las contribuciones y colaboración de la comunidad.
|
||||
|
||||
**P: ¿Qué hace que Aden sea diferente de otros frameworks de agentes?**
|
||||
|
||||
Aden genera todo tu sistema de agentes a partir de objetivos en lenguaje natural usando un agente de codificación—no codificas flujos de trabajo ni defines grafos manualmente. Cuando los agentes fallan, el framework captura automáticamente los datos del fallo, evoluciona el grafo de agentes y lo vuelve a desplegar. Este ciclo de auto-mejora es único de Aden.
|
||||
|
||||
**P: ¿Aden soporta flujos de trabajo con humano en el bucle?**
|
||||
|
||||
Sí, Aden soporta completamente flujos de trabajo con humano en el bucle a través de nodos de intervención que pausan la ejecución para entrada humana. Estos incluyen tiempos de espera configurables y políticas de escalación, permitiendo colaboración fluida entre expertos humanos y agentes de IA.
|
||||
|
||||
---
|
||||
|
||||
<p align="center">
|
||||
Hecho con 🔥 Pasión en San Francisco
|
||||
</p>
|
||||
+339
@@ -0,0 +1,339 @@
|
||||
<p align="center">
|
||||
<img width="100%" alt="Hive Banner" src="https://storage.googleapis.com/aden-prod-assets/website/aden-title-card.png" />
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="README.md">English</a> |
|
||||
<a href="README.zh-CN.md">简体中文</a> |
|
||||
<a href="README.es.md">Español</a> |
|
||||
<a href="README.pt.md">Português</a> |
|
||||
<a href="README.ja.md">日本語</a> |
|
||||
<a href="README.ru.md">Русский</a>
|
||||
</p>
|
||||
|
||||
[](https://github.com/adenhq/hive/blob/main/LICENSE)
|
||||
[](https://www.ycombinator.com/companies/aden)
|
||||
[](https://hub.docker.com/u/adenhq)
|
||||
[](https://discord.com/invite/MXE49hrKDk)
|
||||
[](https://x.com/aden_hq)
|
||||
[](https://www.linkedin.com/company/teamaden/)
|
||||
|
||||
<p align="center">
|
||||
<img src="https://img.shields.io/badge/AI_Agents-Self--Improving-brightgreen?style=flat-square" alt="AI Agents" />
|
||||
<img src="https://img.shields.io/badge/Multi--Agent-Systems-blue?style=flat-square" alt="Multi-Agent" />
|
||||
<img src="https://img.shields.io/badge/Goal--Driven-Development-purple?style=flat-square" alt="Goal-Driven" />
|
||||
<img src="https://img.shields.io/badge/Human--in--the--Loop-orange?style=flat-square" alt="HITL" />
|
||||
<img src="https://img.shields.io/badge/Production--Ready-red?style=flat-square" alt="Production" />
|
||||
</p>
|
||||
<p align="center">
|
||||
<img src="https://img.shields.io/badge/OpenAI-supported-412991?style=flat-square&logo=openai" alt="OpenAI" />
|
||||
<img src="https://img.shields.io/badge/Anthropic-supported-d4a574?style=flat-square" alt="Anthropic" />
|
||||
<img src="https://img.shields.io/badge/Google_Gemini-supported-4285F4?style=flat-square&logo=google" alt="Gemini" />
|
||||
<img src="https://img.shields.io/badge/MCP-19_Tools-00ADD8?style=flat-square" alt="MCP" />
|
||||
</p>
|
||||
|
||||
## 概要
|
||||
|
||||
ワークフローをハードコーディングせずに、信頼性の高い自己改善型AIエージェントを構築できます。コーディングエージェントとの会話を通じて目標を定義すると、フレームワークが動的に作成された接続コードを持つノードグラフを生成します。問題が発生すると、フレームワークは障害データをキャプチャし、コーディングエージェントを通じてエージェントを進化させ、再デプロイします。組み込みのヒューマンインザループノード、認証情報管理、リアルタイムモニタリングにより、適応性を損なうことなく制御を維持できます。
|
||||
|
||||
完全なドキュメント、例、ガイドについては [adenhq.com](https://adenhq.com) をご覧ください。
|
||||
|
||||
## Adenとは
|
||||
|
||||
<p align="center">
|
||||
<img width="100%" alt="Aden Architecture" src="docs/assets/aden-architecture-diagram.jpg" />
|
||||
</p>
|
||||
|
||||
Adenは、AIエージェントの構築、デプロイ、運用、適応のためのプラットフォームです:
|
||||
|
||||
- **構築** - コーディングエージェントが自然言語の目標から専門的なワーカーエージェント(セールス、マーケティング、オペレーション)を生成
|
||||
- **デプロイ** - CI/CD統合と完全なAPIライフサイクル管理を備えたヘッドレスデプロイメント
|
||||
- **運用** - リアルタイムモニタリング、可観測性、ランタイムガードレールがエージェントの信頼性を維持
|
||||
- **適応** - 継続的な評価、監督、適応により、エージェントは時間とともに改善
|
||||
- **インフラ** - 共有メモリ、LLM統合、ツール、スキルがすべてのエージェントを支援
|
||||
|
||||
## クイックリンク
|
||||
|
||||
- **[ドキュメント](https://docs.adenhq.com/)** - 完全なガイドとAPIリファレンス
|
||||
- **[セルフホスティングガイド](https://docs.adenhq.com/getting-started/quickstart)** - インフラストラクチャへのHiveデプロイ
|
||||
- **[変更履歴](https://github.com/adenhq/hive/releases)** - 最新の更新とリリース
|
||||
<!-- - **[ロードマップ](https://adenhq.com/roadmap)** - 今後の機能と計画 -->
|
||||
- **[問題を報告](https://github.com/adenhq/hive/issues)** - バグレポートと機能リクエスト
|
||||
|
||||
## クイックスタート
|
||||
|
||||
### 前提条件
|
||||
|
||||
- [Python 3.11+](https://www.python.org/downloads/) - エージェント開発用
|
||||
- [Docker](https://docs.docker.com/get-docker/) (v20.10+) - オプション、コンテナ化されたツール用
|
||||
|
||||
### インストール
|
||||
|
||||
```bash
|
||||
# リポジトリをクローン
|
||||
git clone https://github.com/adenhq/hive.git
|
||||
cd hive
|
||||
|
||||
# Python環境セットアップを実行
|
||||
./scripts/setup-python.sh
|
||||
```
|
||||
|
||||
これにより以下がインストールされます:
|
||||
- **framework** - コアエージェントランタイムとグラフエグゼキュータ
|
||||
- **aden_tools** - エージェント機能のための19個のMCPツール
|
||||
- すべての必要な依存関係
|
||||
|
||||
### 最初のエージェントを構築
|
||||
|
||||
```bash
|
||||
# Claude Codeスキルをインストール(1回のみ)
|
||||
./quickstart.sh
|
||||
|
||||
# Claude Codeを使用してエージェントを構築
|
||||
claude> /building-agents
|
||||
|
||||
# エージェントをテスト
|
||||
claude> /testing-agent
|
||||
|
||||
# エージェントを実行
|
||||
PYTHONPATH=core:exports python -m your_agent_name run --input '{...}'
|
||||
```
|
||||
|
||||
**[📖 完全セットアップガイド](ENVIRONMENT_SETUP.md)** - エージェント開発の詳細な手順
|
||||
|
||||
## 機能
|
||||
|
||||
- **目標駆動開発** - 自然言語で目標を定義;コーディングエージェントがそれを達成するためのエージェントグラフと接続コードを生成
|
||||
- **自己適応エージェント** - フレームワークが障害をキャプチャし、目標を更新し、エージェントグラフを更新
|
||||
- **動的ノード接続** - 事前定義されたエッジなし;接続コードは目標に基づいて任意の対応LLMによって生成
|
||||
- **SDKラップノード** - すべてのノードが共有メモリ、ローカルRLMメモリ、モニタリング、ツール、LLMアクセスを標準装備
|
||||
- **ヒューマンインザループ** - 設定可能なタイムアウトとエスカレーションを備えた、人間の入力のために実行を一時停止する介入ノード
|
||||
- **リアルタイム可観測性** - エージェント実行、決定、ノード間通信のライブモニタリングのためのWebSocketストリーミング
|
||||
- **コストと予算管理** - 支出制限、スロットル、自動モデル劣化ポリシーを設定
|
||||
- **本番環境対応** - セルフホスト可能、スケールと信頼性のために構築
|
||||
|
||||
## なぜAdenか
|
||||
|
||||
従来のエージェントフレームワークでは、ワークフローを手動で設計し、エージェントの相互作用を定義し、障害を事後的に処理する必要があります。Adenはこのパラダイムを逆転させます—**結果を記述すれば、システムが自ら構築します**。
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph BUILD["🏗️ BUILD"]
|
||||
GOAL["Define Goal<br/>+ Success Criteria"] --> NODES["Add Nodes<br/>LLM/Router/Function"]
|
||||
NODES --> EDGES["Connect Edges<br/>on_success/failure/conditional"]
|
||||
EDGES --> TEST["Test & Validate"] --> APPROVE["Approve & Export"]
|
||||
end
|
||||
|
||||
subgraph EXPORT["📦 EXPORT"]
|
||||
direction TB
|
||||
JSON["agent.json<br/>(GraphSpec)"]
|
||||
TOOLS["tools.py<br/>(Functions)"]
|
||||
MCP["mcp_servers.json<br/>(Integrations)"]
|
||||
end
|
||||
|
||||
subgraph RUN["🚀 RUNTIME"]
|
||||
LOAD["AgentRunner<br/>Load + Parse"] --> SETUP["Setup Runtime<br/>+ ToolRegistry"]
|
||||
SETUP --> EXEC["GraphExecutor<br/>Execute Nodes"]
|
||||
|
||||
subgraph DECISION["Decision Recording"]
|
||||
DEC1["runtime.decide()<br/>intent → options → choice"]
|
||||
DEC2["runtime.record_outcome()<br/>success, result, metrics"]
|
||||
end
|
||||
end
|
||||
|
||||
subgraph INFRA["⚙️ INFRASTRUCTURE"]
|
||||
CTX["NodeContext<br/>memory • llm • tools"]
|
||||
STORE[("FileStorage<br/>Runs & Decisions")]
|
||||
end
|
||||
|
||||
APPROVE --> EXPORT
|
||||
EXPORT --> LOAD
|
||||
EXEC --> DECISION
|
||||
EXEC --> CTX
|
||||
DECISION --> STORE
|
||||
STORE -.->|"Analyze & Improve"| NODES
|
||||
|
||||
style BUILD fill:#ffbe42,stroke:#cc5d00,stroke-width:3px,color:#333
|
||||
style EXPORT fill:#fff59d,stroke:#ed8c00,stroke-width:2px,color:#333
|
||||
style RUN fill:#ffb100,stroke:#cc5d00,stroke-width:3px,color:#333
|
||||
style DECISION fill:#ffcc80,stroke:#ed8c00,stroke-width:2px,color:#333
|
||||
style INFRA fill:#e8763d,stroke:#cc5d00,stroke-width:3px,color:#fff
|
||||
style STORE fill:#ed8c00,stroke:#cc5d00,stroke-width:2px,color:#fff
|
||||
```
|
||||
|
||||
### Adenの優位性
|
||||
|
||||
| 従来のフレームワーク | Aden |
|
||||
|----------------------|------|
|
||||
| エージェントワークフローをハードコード | 自然言語で目標を記述 |
|
||||
| 手動でグラフを定義 | 自動生成されるエージェントグラフ |
|
||||
| 事後的なエラー処理 | プロアクティブな自己進化 |
|
||||
| 静的なツール設定 | 動的なSDKラップノード |
|
||||
| 別途モニタリング設定 | 組み込みのリアルタイム可観測性 |
|
||||
| DIY予算管理 | 統合されたコスト制御と劣化 |
|
||||
|
||||
### 仕組み
|
||||
|
||||
1. **目標を定義** → 達成したいことを平易な言葉で記述
|
||||
2. **コーディングエージェントが生成** → エージェントグラフ、接続コード、テストケースを作成
|
||||
3. **ワーカーが実行** → SDKラップノードが完全な可観測性とツールアクセスで実行
|
||||
4. **コントロールプレーンが監視** → リアルタイムメトリクス、予算執行、ポリシー管理
|
||||
5. **自己改善** → 障害時、システムがグラフを進化させ自動的に再デプロイ
|
||||
|
||||
## Adenの比較
|
||||
|
||||
Adenはエージェント開発に根本的に異なるアプローチを採用しています。ほとんどのフレームワークがワークフローをハードコードするか、エージェントグラフを手動で定義することを要求するのに対し、Adenは**コーディングエージェントを使用して自然言語の目標からエージェントシステム全体を生成**します。エージェントが失敗した場合、フレームワークは単にエラーをログに記録するだけでなく—**自動的にエージェントグラフを進化させ**、再デプロイします。
|
||||
|
||||
> **注意:** 詳細なフレームワーク比較表とよくある質問については、英語の[README.md](README.md)を参照してください。
|
||||
|
||||
### Adenを選ぶべきとき
|
||||
|
||||
Adenを選択する場合:
|
||||
|
||||
- 手動介入なしに**失敗から自己改善する**エージェントが必要
|
||||
- ワークフローではなく結果を記述する**目標駆動開発**が必要
|
||||
- 自動回復と再デプロイを備えた**本番環境の信頼性**が必要
|
||||
- コードを書き直すことなくエージェントアーキテクチャを**迅速に反復**する必要がある
|
||||
- リアルタイムモニタリングと人間の監督を備えた**完全な可観測性**が必要
|
||||
|
||||
他のフレームワークを選択する場合:
|
||||
|
||||
- **型安全で予測可能なワークフロー**(PydanticAI、Mastra)
|
||||
- **RAGとドキュメント処理**(LlamaIndex、Haystack)
|
||||
- **エージェント創発の研究**(CAMEL)
|
||||
- **リアルタイム音声/マルチモーダル**(TEN Framework)
|
||||
- **シンプルなコンポーネント連鎖**(LangChain、Swarm)
|
||||
|
||||
## プロジェクト構造
|
||||
|
||||
```
|
||||
hive/
|
||||
├── core/ # コアフレームワーク - エージェントランタイム、グラフエグゼキュータ、プロトコル
|
||||
├── tools/ # MCPツールパッケージ - エージェント機能のための19個のツール
|
||||
├── exports/ # エージェントパッケージ - 事前構築されたエージェントと例
|
||||
├── docs/ # ドキュメントとガイド
|
||||
├── scripts/ # ビルドとユーティリティスクリプト
|
||||
├── .claude/ # エージェント構築用のClaude Codeスキル
|
||||
├── ENVIRONMENT_SETUP.md # エージェント開発用のPythonセットアップガイド
|
||||
├── DEVELOPER.md # 開発者ガイド
|
||||
├── CONTRIBUTING.md # 貢献ガイドライン
|
||||
└── ROADMAP.md # プロダクトロードマップ
|
||||
```
|
||||
|
||||
## 開発
|
||||
|
||||
### Pythonエージェント開発
|
||||
|
||||
フレームワークで目標駆動エージェントを構築および実行するには:
|
||||
|
||||
```bash
|
||||
# 1回限りのセットアップ
|
||||
./scripts/setup-python.sh
|
||||
|
||||
# これにより以下がインストールされます:
|
||||
# - frameworkパッケージ(コアランタイム)
|
||||
# - aden_toolsパッケージ(19個のMCPツール)
|
||||
# - すべての依存関係
|
||||
|
||||
# Claude Codeスキルを使用して新しいエージェントを構築
|
||||
claude> /building-agents
|
||||
|
||||
# エージェントをテスト
|
||||
claude> /testing-agent
|
||||
|
||||
# エージェントを実行
|
||||
PYTHONPATH=core:exports python -m agent_name run --input '{...}'
|
||||
```
|
||||
|
||||
完全なセットアップ手順については、[ENVIRONMENT_SETUP.md](ENVIRONMENT_SETUP.md)を参照してください。
|
||||
|
||||
## ドキュメント
|
||||
|
||||
- **[開発者ガイド](DEVELOPER.md)** - 開発者向け総合ガイド
|
||||
- [はじめに](docs/getting-started.md) - クイックセットアップ手順
|
||||
- [設定ガイド](docs/configuration.md) - すべての設定オプション
|
||||
- [アーキテクチャ概要](docs/architecture.md) - システム設計と構造
|
||||
|
||||
## ロードマップ
|
||||
|
||||
Adenエージェントフレームワークは、開発者が結果志向で自己適応するエージェントを構築できるよう支援することを目指しています。ロードマップはこちらをご覧ください
|
||||
|
||||
[ROADMAP.md](ROADMAP.md)
|
||||
|
||||
```mermaid
|
||||
timeline
|
||||
title Aden Agent Framework Roadmap
|
||||
section Foundation
|
||||
Architecture : Node-Based Architecture : Python SDK : LLM Integration (OpenAI, Anthropic, Google) : Communication Protocol
|
||||
Coding Agent : Goal Creation Session : Worker Agent Creation : MCP Tools Integration
|
||||
Worker Agent : Human-in-the-Loop : Callback Handlers : Intervention Points : Streaming Interface
|
||||
Tools : File Use : Memory (STM/LTM) : Web Search : Web Scraper : Audit Trail
|
||||
Core : Eval System : Pydantic Validation : Docker Deployment : Documentation : Sample Agents
|
||||
section Expansion
|
||||
Intelligence : Guardrails : Streaming Mode : Semantic Search
|
||||
Platform : JavaScript SDK : Custom Tool Integrator : Credential Store
|
||||
Deployment : Self-Hosted : Cloud Services : CI/CD Pipeline
|
||||
Templates : Sales Agent : Marketing Agent : Analytics Agent : Training Agent : Smart Form Agent
|
||||
```
|
||||
|
||||
## コミュニティとサポート
|
||||
|
||||
サポート、機能リクエスト、コミュニティディスカッションには[Discord](https://discord.com/invite/MXE49hrKDk)を使用しています。
|
||||
|
||||
- Discord - [コミュニティに参加](https://discord.com/invite/MXE49hrKDk)
|
||||
- Twitter/X - [@adenhq](https://x.com/aden_hq)
|
||||
- LinkedIn - [会社ページ](https://www.linkedin.com/company/teamaden/)
|
||||
|
||||
## 貢献
|
||||
|
||||
貢献を歓迎します!ガイドラインについては[CONTRIBUTING.md](CONTRIBUTING.md)をご覧ください。
|
||||
|
||||
1. リポジトリをフォーク
|
||||
2. 機能ブランチを作成 (`git checkout -b feature/amazing-feature`)
|
||||
3. 変更をコミット (`git commit -m 'Add amazing feature'`)
|
||||
4. ブランチにプッシュ (`git push origin feature/amazing-feature`)
|
||||
5. プルリクエストを開く
|
||||
|
||||
## チームに参加
|
||||
|
||||
**採用中です!** エンジニアリング、リサーチ、マーケティングの役職で私たちに参加してください。
|
||||
|
||||
[オープンポジションを見る](https://jobs.adenhq.com/a8cec478-cdbc-473c-bbd4-f4b7027ec193/applicant)
|
||||
|
||||
## セキュリティ
|
||||
|
||||
セキュリティに関する懸念については、[SECURITY.md](SECURITY.md)をご覧ください。
|
||||
|
||||
## ライセンス
|
||||
|
||||
このプロジェクトはApache License 2.0の下でライセンスされています - 詳細は[LICENSE](LICENSE)ファイルをご覧ください。
|
||||
|
||||
## よくある質問 (FAQ)
|
||||
|
||||
> **注意:** よくある質問の完全版については、英語の[README.md](README.md)を参照してください。
|
||||
|
||||
**Q: AdenはLangChainや他のエージェントフレームワークに依存していますか?**
|
||||
|
||||
いいえ。AdenはLangChain、CrewAI、その他のエージェントフレームワークに依存せずにゼロから構築されています。フレームワークは軽量で柔軟に設計されており、事前定義されたコンポーネントに依存するのではなく、エージェントグラフを動的に生成します。
|
||||
|
||||
**Q: AdenはどのLLMプロバイダーをサポートしていますか?**
|
||||
|
||||
AdenはLiteLLM統合を通じて100以上のLLMプロバイダーをサポートしており、OpenAI(GPT-4、GPT-4o)、Anthropic(Claudeモデル)、Google Gemini、Mistral、Groqなどが含まれます。適切なAPIキー環境変数を設定し、モデル名を指定するだけです。
|
||||
|
||||
**Q: Adenはオープンソースですか?**
|
||||
|
||||
はい、AdenはApache License 2.0の下で完全にオープンソースです。コミュニティの貢献とコラボレーションを積極的に奨励しています。
|
||||
|
||||
**Q: Adenは他のエージェントフレームワークと何が違いますか?**
|
||||
|
||||
Adenはコーディングエージェントを使用して自然言語の目標からエージェントシステム全体を生成します—ワークフローをハードコードしたり、グラフを手動で定義したりする必要はありません。エージェントが失敗すると、フレームワークは自動的に障害データをキャプチャし、エージェントグラフを進化させ、再デプロイします。この自己改善ループはAden独自のものです。
|
||||
|
||||
**Q: Adenはヒューマンインザループワークフローをサポートしていますか?**
|
||||
|
||||
はい、Adenは人間の入力のために実行を一時停止する介入ノードを通じて、ヒューマンインザループワークフローを完全にサポートしています。設定可能なタイムアウトとエスカレーションポリシーが含まれており、人間の専門家とAIエージェントのシームレスなコラボレーションを可能にします。
|
||||
|
||||
---
|
||||
|
||||
<p align="center">
|
||||
サンフランシスコで 🔥 情熱を込めて作成
|
||||
</p>
|
||||
@@ -2,6 +2,15 @@
|
||||
<img width="100%" alt="Hive Banner" src="https://storage.googleapis.com/aden-prod-assets/website/aden-title-card.png" />
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="README.md">English</a> |
|
||||
<a href="README.zh-CN.md">简体中文</a> |
|
||||
<a href="README.es.md">Español</a> |
|
||||
<a href="README.pt.md">Português</a> |
|
||||
<a href="README.ja.md">日本語</a> |
|
||||
<a href="README.ru.md">Русский</a>
|
||||
</p>
|
||||
|
||||
[](https://github.com/adenhq/hive/blob/main/LICENSE)
|
||||
[](https://www.ycombinator.com/companies/aden)
|
||||
[](https://hub.docker.com/u/adenhq)
|
||||
@@ -29,6 +38,20 @@ Build reliable, self-improving AI agents without hardcoding workflows. Define yo
|
||||
|
||||
Visit [adenhq.com](https://adenhq.com) for complete documentation, examples, and guides.
|
||||
|
||||
## What is Aden
|
||||
|
||||
<p align="center">
|
||||
<img width="100%" alt="Aden Architecture" src="docs/assets/aden-architecture-diagram.jpg" />
|
||||
</p>
|
||||
|
||||
Aden is a platform for building, deploying, operating, and adapting AI agents:
|
||||
|
||||
- **Build** - A Coding Agent generates specialized Worker Agents (Sales, Marketing, Ops) from natural language goals
|
||||
- **Deploy** - Headless deployment with CI/CD integration and full API lifecycle management
|
||||
- **Operate** - Real-time monitoring, observability, and runtime guardrails keep agents reliable
|
||||
- **Adapt** - Continuous evaluation, supervision, and adaptation ensure agents improve over time
|
||||
- **Infra** - Shared memory, LLM integrations, tools, and skills power every agent
|
||||
|
||||
## Quick Links
|
||||
|
||||
- **[Documentation](https://docs.adenhq.com/)** - Complete guides and API reference
|
||||
@@ -41,8 +64,8 @@ Visit [adenhq.com](https://adenhq.com) for complete documentation, examples, and
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- [Docker](https://docs.docker.com/get-docker/) (v20.10+)
|
||||
- [Docker Compose](https://docs.docker.com/compose/install/) (v2.0+)
|
||||
- [Python 3.11+](https://www.python.org/downloads/) for agent development
|
||||
- [Docker](https://docs.docker.com/get-docker/) (v20.10+) - Optional, for containerized tools
|
||||
|
||||
### Installation
|
||||
|
||||
@@ -51,19 +74,32 @@ Visit [adenhq.com](https://adenhq.com) for complete documentation, examples, and
|
||||
git clone https://github.com/adenhq/hive.git
|
||||
cd hive
|
||||
|
||||
# Copy and configure
|
||||
cp config.yaml.example config.yaml
|
||||
|
||||
# Run setup and start services
|
||||
npm run setup
|
||||
docker compose up
|
||||
# Run Python environment setup
|
||||
./scripts/setup-python.sh
|
||||
```
|
||||
|
||||
**Access the application:**
|
||||
This installs:
|
||||
- **framework** - Core agent runtime and graph executor
|
||||
- **aden_tools** - 19 MCP tools for agent capabilities
|
||||
- All required dependencies
|
||||
|
||||
- Dashboard: http://localhost:3000
|
||||
- API: http://localhost:4000
|
||||
- Health: http://localhost:4000/health
|
||||
### Build Your First Agent
|
||||
|
||||
```bash
|
||||
# Install Claude Code skills (one-time)
|
||||
./quickstart.sh
|
||||
|
||||
# Build an agent using Claude Code
|
||||
claude> /building-agents
|
||||
|
||||
# Test your agent
|
||||
claude> /testing-agent
|
||||
|
||||
# Run your agent
|
||||
PYTHONPATH=core:exports python -m your_agent_name run --input '{...}'
|
||||
```
|
||||
|
||||
**[📖 Complete Setup Guide](ENVIRONMENT_SETUP.md)** - Detailed instructions for agent development
|
||||
|
||||
## Features
|
||||
|
||||
@@ -127,14 +163,14 @@ flowchart LR
|
||||
|
||||
### The Aden Advantage
|
||||
|
||||
| Traditional Frameworks | Aden |
|
||||
|------------------------|------|
|
||||
| Hardcode agent workflows | Describe goals in natural language |
|
||||
| Manual graph definition | Auto-generated agent graphs |
|
||||
| Reactive error handling | Proactive self-evolution |
|
||||
| Static tool configurations | Dynamic SDK-wrapped nodes |
|
||||
| Separate monitoring setup | Built-in real-time observability |
|
||||
| DIY budget management | Integrated cost controls & degradation |
|
||||
| Traditional Frameworks | Aden |
|
||||
| -------------------------- | -------------------------------------- |
|
||||
| Hardcode agent workflows | Describe goals in natural language |
|
||||
| Manual graph definition | Auto-generated agent graphs |
|
||||
| Reactive error handling | Proactive self-evolution |
|
||||
| Static tool configurations | Dynamic SDK-wrapped nodes |
|
||||
| Separate monitoring setup | Built-in real-time observability |
|
||||
| DIY budget management | Integrated cost controls & degradation |
|
||||
|
||||
### How It Works
|
||||
|
||||
@@ -150,20 +186,21 @@ Aden takes a fundamentally different approach to agent development. While most f
|
||||
|
||||
### Comparison Table
|
||||
|
||||
| Framework | Category | Approach | Aden Difference |
|
||||
|-----------|----------|----------|-----------------|
|
||||
| **LangChain, LlamaIndex, Haystack** | Component Libraries | Predefined components for RAG/LLM apps; manual connection logic | Generates entire graph and connection code upfront |
|
||||
| **CrewAI, AutoGen, Swarm** | Multi-Agent Orchestration | Role-based agents with predefined collaboration patterns | Dynamically creates agents/connections; adapts on failure |
|
||||
| **PydanticAI, Mastra, Agno** | Type-Safe Frameworks | Structured outputs and validation for known workflows | Evolving workflows; structure emerges through iteration |
|
||||
| **Agent Zero, Letta** | Personal AI Assistants | Memory and learning; OS-as-tool or stateful memory focus | Production multi-agent systems with self-healing |
|
||||
| **CAMEL** | Research Framework | Emergent behavior in large-scale simulations (up to 1M agents) | Production-oriented with reliable execution and recovery |
|
||||
| **TEN Framework, Genkit** | Infrastructure Frameworks | Real-time multimodal (TEN) or full-stack AI (Genkit) | Higher abstraction—generates and evolves agent logic |
|
||||
| **GPT Engineer, Motia** | Code Generation | Code from specs (GPT Engineer) or "Step" primitive (Motia) | Self-adapting graphs with automatic failure recovery |
|
||||
| **Trading Agents** | Domain-Specific | Hardcoded trading firm roles on LangGraph | Domain-agnostic; generates structures for any use case |
|
||||
| Framework | Category | Approach | Aden Difference |
|
||||
| ----------------------------------- | ------------------------- | --------------------------------------------------------------- | --------------------------------------------------------- |
|
||||
| **LangChain, LlamaIndex, Haystack** | Component Libraries | Predefined components for RAG/LLM apps; manual connection logic | Generates entire graph and connection code upfront |
|
||||
| **CrewAI, AutoGen, Swarm** | Multi-Agent Orchestration | Role-based agents with predefined collaboration patterns | Dynamically creates agents/connections; adapts on failure |
|
||||
| **PydanticAI, Mastra, Agno** | Type-Safe Frameworks | Structured outputs and validation for known workflows | Evolving workflows; structure emerges through iteration |
|
||||
| **Agent Zero, Letta** | Personal AI Assistants | Memory and learning; OS-as-tool or stateful memory focus | Production multi-agent systems with self-healing |
|
||||
| **CAMEL** | Research Framework | Emergent behavior in large-scale simulations (up to 1M agents) | Production-oriented with reliable execution and recovery |
|
||||
| **TEN Framework, Genkit** | Infrastructure Frameworks | Real-time multimodal (TEN) or full-stack AI (Genkit) | Higher abstraction—generates and evolves agent logic |
|
||||
| **GPT Engineer, Motia** | Code Generation | Code from specs (GPT Engineer) or "Step" primitive (Motia) | Self-adapting graphs with automatic failure recovery |
|
||||
| **Trading Agents** | Domain-Specific | Hardcoded trading firm roles on LangGraph | Domain-agnostic; generates structures for any use case |
|
||||
|
||||
### When to Choose Aden
|
||||
|
||||
Choose Aden when you need:
|
||||
|
||||
- Agents that **self-improve from failures** without manual intervention
|
||||
- **Goal-driven development** where you describe outcomes, not workflows
|
||||
- **Production reliability** with automatic recovery and redeployment
|
||||
@@ -171,6 +208,7 @@ Choose Aden when you need:
|
||||
- **Full observability** with real-time monitoring and human oversight
|
||||
|
||||
Choose other frameworks when you need:
|
||||
|
||||
- **Type-safe, predictable workflows** (PydanticAI, Mastra)
|
||||
- **RAG and document processing** (LlamaIndex, Haystack)
|
||||
- **Research on agent emergence** (CAMEL)
|
||||
@@ -181,13 +219,13 @@ Choose other frameworks when you need:
|
||||
|
||||
```
|
||||
hive/
|
||||
├── honeycomb/ # Frontend Dashboard
|
||||
├── hive/ # Backend API Server
|
||||
├── aden-tools/ # MCP Tools Package - 19 tools for agent capabilities
|
||||
├── core/ # Core framework - Agent runtime, graph executor, protocols
|
||||
├── tools/ # MCP Tools Package - 19 tools for agent capabilities
|
||||
├── exports/ # Agent packages - Pre-built agents and examples
|
||||
├── docs/ # Documentation and guides
|
||||
├── scripts/ # Build and utility scripts
|
||||
├── config.yaml.example # Configuration template
|
||||
├── docker-compose.yml # Container orchestration
|
||||
├── .claude/ # Claude Code skills for building agents
|
||||
├── ENVIRONMENT_SETUP.md # Python setup guide for agent development
|
||||
├── DEVELOPER.md # Developer guide
|
||||
├── CONTRIBUTING.md # Contribution guidelines
|
||||
└── ROADMAP.md # Product roadmap
|
||||
@@ -195,31 +233,30 @@ hive/
|
||||
|
||||
## Development
|
||||
|
||||
### Local Development with Hot Reload
|
||||
### Python Agent Development
|
||||
|
||||
For building and running goal-driven agents with the framework:
|
||||
|
||||
```bash
|
||||
# Copy development overrides
|
||||
cp docker-compose.override.yml.example docker-compose.override.yml
|
||||
# One-time setup
|
||||
./scripts/setup-python.sh
|
||||
|
||||
# Start with hot reload enabled
|
||||
docker compose up
|
||||
# This installs:
|
||||
# - framework package (core runtime)
|
||||
# - aden_tools package (19 MCP tools)
|
||||
# - All dependencies
|
||||
|
||||
# Build new agents using Claude Code skills
|
||||
claude> /building-agents
|
||||
|
||||
# Test agents
|
||||
claude> /testing-agent
|
||||
|
||||
# Run agents
|
||||
PYTHONPATH=core:exports python -m agent_name run --input '{...}'
|
||||
```
|
||||
|
||||
### Running Without Docker
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# Generate environment files
|
||||
npm run generate:env
|
||||
|
||||
# Start frontend (in honeycomb/)
|
||||
cd honeycomb && npm run dev
|
||||
|
||||
# Start backend (in hive/)
|
||||
cd hive && npm run dev
|
||||
```
|
||||
See [ENVIRONMENT_SETUP.md](ENVIRONMENT_SETUP.md) for complete setup instructions.
|
||||
|
||||
## Documentation
|
||||
|
||||
@@ -290,11 +327,11 @@ No. Aden is built from the ground up with no dependencies on LangChain, CrewAI,
|
||||
|
||||
**Q: What LLM providers does Aden support?**
|
||||
|
||||
Aden supports OpenAI (GPT-4, GPT-4o), Anthropic (Claude models), and Google Gemini out of the box. The architecture is provider-agnostic through SDK abstraction, with LiteLLM integration on the roadmap for expanded model support.
|
||||
Aden supports 100+ LLM providers through LiteLLM integration, including OpenAI (GPT-4, GPT-4o), Anthropic (Claude models), Google Gemini, Mistral, Groq, and many more. Simply set the appropriate API key environment variable and specify the model name.
|
||||
|
||||
**Q: Can I use Aden with local AI models like Ollama?**
|
||||
|
||||
Local model support through LiteLLM integration is on our roadmap. The SDK's provider-agnostic design means adding local model support will be straightforward once implemented.
|
||||
Yes! Aden supports local models through LiteLLM. Simply use the model name format `ollama/model-name` (e.g., `ollama/llama3`, `ollama/mistral`) and ensure Ollama is running locally.
|
||||
|
||||
**Q: What makes Aden different from other agent frameworks?**
|
||||
|
||||
|
||||
+339
@@ -0,0 +1,339 @@
|
||||
<p align="center">
|
||||
<img width="100%" alt="Hive Banner" src="https://storage.googleapis.com/aden-prod-assets/website/aden-title-card.png" />
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="README.md">English</a> |
|
||||
<a href="README.zh-CN.md">简体中文</a> |
|
||||
<a href="README.es.md">Español</a> |
|
||||
<a href="README.pt.md">Português</a> |
|
||||
<a href="README.ja.md">日本語</a> |
|
||||
<a href="README.ru.md">Русский</a>
|
||||
</p>
|
||||
|
||||
[](https://github.com/adenhq/hive/blob/main/LICENSE)
|
||||
[](https://www.ycombinator.com/companies/aden)
|
||||
[](https://hub.docker.com/u/adenhq)
|
||||
[](https://discord.com/invite/MXE49hrKDk)
|
||||
[](https://x.com/aden_hq)
|
||||
[](https://www.linkedin.com/company/teamaden/)
|
||||
|
||||
<p align="center">
|
||||
<img src="https://img.shields.io/badge/AI_Agents-Self--Improving-brightgreen?style=flat-square" alt="AI Agents" />
|
||||
<img src="https://img.shields.io/badge/Multi--Agent-Systems-blue?style=flat-square" alt="Multi-Agent" />
|
||||
<img src="https://img.shields.io/badge/Goal--Driven-Development-purple?style=flat-square" alt="Goal-Driven" />
|
||||
<img src="https://img.shields.io/badge/Human--in--the--Loop-orange?style=flat-square" alt="HITL" />
|
||||
<img src="https://img.shields.io/badge/Production--Ready-red?style=flat-square" alt="Production" />
|
||||
</p>
|
||||
<p align="center">
|
||||
<img src="https://img.shields.io/badge/OpenAI-supported-412991?style=flat-square&logo=openai" alt="OpenAI" />
|
||||
<img src="https://img.shields.io/badge/Anthropic-supported-d4a574?style=flat-square" alt="Anthropic" />
|
||||
<img src="https://img.shields.io/badge/Google_Gemini-supported-4285F4?style=flat-square&logo=google" alt="Gemini" />
|
||||
<img src="https://img.shields.io/badge/MCP-19_Tools-00ADD8?style=flat-square" alt="MCP" />
|
||||
</p>
|
||||
|
||||
## Visão Geral
|
||||
|
||||
Construa agentes de IA confiáveis e auto-aperfeiçoáveis sem codificar fluxos de trabalho. Defina seu objetivo através de uma conversa com um agente de codificação, e o framework gera um grafo de nós com código de conexão criado dinamicamente. Quando algo quebra, o framework captura dados de falha, evolui o agente através do agente de codificação e reimplanta. Nós de intervenção humana integrados, gerenciamento de credenciais e monitoramento em tempo real dão a você controle sem sacrificar a adaptabilidade.
|
||||
|
||||
Visite [adenhq.com](https://adenhq.com) para documentação completa, exemplos e guias.
|
||||
|
||||
## O que é Aden
|
||||
|
||||
<p align="center">
|
||||
<img width="100%" alt="Aden Architecture" src="docs/assets/aden-architecture-diagram.jpg" />
|
||||
</p>
|
||||
|
||||
Aden é uma plataforma para construir, implantar, operar e adaptar agentes de IA:
|
||||
|
||||
- **Construir** - Um Agente de Codificação gera Agentes de Trabalho especializados (Vendas, Marketing, Operações) a partir de objetivos em linguagem natural
|
||||
- **Implantar** - Implantação headless com integração CI/CD e gerenciamento completo do ciclo de vida de API
|
||||
- **Operar** - Monitoramento em tempo real, observabilidade e guardrails de runtime mantêm os agentes confiáveis
|
||||
- **Adaptar** - Avaliação contínua, supervisão e adaptação garantem que os agentes melhorem ao longo do tempo
|
||||
- **Infraestrutura** - Memória compartilhada, integrações LLM, ferramentas e habilidades alimentam cada agente
|
||||
|
||||
## Links Rápidos
|
||||
|
||||
- **[Documentação](https://docs.adenhq.com/)** - Guias completos e referência de API
|
||||
- **[Guia de Auto-Hospedagem](https://docs.adenhq.com/getting-started/quickstart)** - Implante o Hive em sua infraestrutura
|
||||
- **[Changelog](https://github.com/adenhq/hive/releases)** - Últimas atualizações e versões
|
||||
<!-- - **[Roadmap](https://adenhq.com/roadmap)** - Funcionalidades e planos futuros -->
|
||||
- **[Reportar Problemas](https://github.com/adenhq/hive/issues)** - Relatórios de bugs e solicitações de funcionalidades
|
||||
|
||||
## Início Rápido
|
||||
|
||||
### Pré-requisitos
|
||||
|
||||
- [Python 3.11+](https://www.python.org/downloads/) - Para desenvolvimento de agentes
|
||||
- [Docker](https://docs.docker.com/get-docker/) (v20.10+) - Opcional, para ferramentas containerizadas
|
||||
|
||||
### Instalação
|
||||
|
||||
```bash
|
||||
# Clonar o repositório
|
||||
git clone https://github.com/adenhq/hive.git
|
||||
cd hive
|
||||
|
||||
# Executar configuração do ambiente Python
|
||||
./scripts/setup-python.sh
|
||||
```
|
||||
|
||||
Isto instala:
|
||||
- **framework** - Runtime do agente principal e executor de grafos
|
||||
- **aden_tools** - 19 ferramentas MCP para capacidades de agentes
|
||||
- Todas as dependências necessárias
|
||||
|
||||
### Construa Seu Primeiro Agente
|
||||
|
||||
```bash
|
||||
# Instalar habilidades do Claude Code (uma vez)
|
||||
./quickstart.sh
|
||||
|
||||
# Construir um agente usando Claude Code
|
||||
claude> /building-agents
|
||||
|
||||
# Testar seu agente
|
||||
claude> /testing-agent
|
||||
|
||||
# Executar seu agente
|
||||
PYTHONPATH=core:exports python -m your_agent_name run --input '{...}'
|
||||
```
|
||||
|
||||
**[📖 Guia Completo de Configuração](ENVIRONMENT_SETUP.md)** - Instruções detalhadas para desenvolvimento de agentes
|
||||
|
||||
## Funcionalidades
|
||||
|
||||
- **Desenvolvimento Orientado a Objetivos** - Defina objetivos em linguagem natural; o agente de codificação gera o grafo de agentes e código de conexão para alcançá-los
|
||||
- **Agentes Auto-Adaptáveis** - Framework captura falhas, atualiza objetivos e atualiza o grafo de agentes
|
||||
- **Conexões de Nós Dinâmicas** - Sem arestas predefinidas; código de conexão é gerado por qualquer LLM capaz baseado em seus objetivos
|
||||
- **Nós Envolvidos em SDK** - Cada nó recebe memória compartilhada, memória RLM local, monitoramento, ferramentas e acesso LLM prontos para uso
|
||||
- **Humano no Loop** - Nós de intervenção que pausam a execução para entrada humana com timeouts e escalonamento configuráveis
|
||||
- **Observabilidade em Tempo Real** - Streaming WebSocket para monitoramento ao vivo de execução de agentes, decisões e comunicação entre nós
|
||||
- **Controle de Custo e Orçamento** - Defina limites de gastos, throttles e políticas de degradação automática de modelo
|
||||
- **Pronto para Produção** - Auto-hospedável, construído para escala e confiabilidade
|
||||
|
||||
## Por que Aden
|
||||
|
||||
Frameworks de agentes tradicionais exigem que você projete manualmente fluxos de trabalho, defina interações de agentes e lide com falhas reativamente. Aden inverte esse paradigma—**você descreve resultados, e o sistema se constrói sozinho**.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph BUILD["🏗️ BUILD"]
|
||||
GOAL["Define Goal<br/>+ Success Criteria"] --> NODES["Add Nodes<br/>LLM/Router/Function"]
|
||||
NODES --> EDGES["Connect Edges<br/>on_success/failure/conditional"]
|
||||
EDGES --> TEST["Test & Validate"] --> APPROVE["Approve & Export"]
|
||||
end
|
||||
|
||||
subgraph EXPORT["📦 EXPORT"]
|
||||
direction TB
|
||||
JSON["agent.json<br/>(GraphSpec)"]
|
||||
TOOLS["tools.py<br/>(Functions)"]
|
||||
MCP["mcp_servers.json<br/>(Integrations)"]
|
||||
end
|
||||
|
||||
subgraph RUN["🚀 RUNTIME"]
|
||||
LOAD["AgentRunner<br/>Load + Parse"] --> SETUP["Setup Runtime<br/>+ ToolRegistry"]
|
||||
SETUP --> EXEC["GraphExecutor<br/>Execute Nodes"]
|
||||
|
||||
subgraph DECISION["Decision Recording"]
|
||||
DEC1["runtime.decide()<br/>intent → options → choice"]
|
||||
DEC2["runtime.record_outcome()<br/>success, result, metrics"]
|
||||
end
|
||||
end
|
||||
|
||||
subgraph INFRA["⚙️ INFRASTRUCTURE"]
|
||||
CTX["NodeContext<br/>memory • llm • tools"]
|
||||
STORE[("FileStorage<br/>Runs & Decisions")]
|
||||
end
|
||||
|
||||
APPROVE --> EXPORT
|
||||
EXPORT --> LOAD
|
||||
EXEC --> DECISION
|
||||
EXEC --> CTX
|
||||
DECISION --> STORE
|
||||
STORE -.->|"Analyze & Improve"| NODES
|
||||
|
||||
style BUILD fill:#ffbe42,stroke:#cc5d00,stroke-width:3px,color:#333
|
||||
style EXPORT fill:#fff59d,stroke:#ed8c00,stroke-width:2px,color:#333
|
||||
style RUN fill:#ffb100,stroke:#cc5d00,stroke-width:3px,color:#333
|
||||
style DECISION fill:#ffcc80,stroke:#ed8c00,stroke-width:2px,color:#333
|
||||
style INFRA fill:#e8763d,stroke:#cc5d00,stroke-width:3px,color:#fff
|
||||
style STORE fill:#ed8c00,stroke:#cc5d00,stroke-width:2px,color:#fff
|
||||
```
|
||||
|
||||
### A Vantagem Aden
|
||||
|
||||
| Frameworks Tradicionais | Aden |
|
||||
|-------------------------|------|
|
||||
| Codificar fluxos de trabalho de agentes | Descrever objetivos em linguagem natural |
|
||||
| Definição manual de grafos | Grafos de agentes auto-gerados |
|
||||
| Tratamento reativo de erros | Auto-evolução proativa |
|
||||
| Configurações de ferramentas estáticas | Nós dinâmicos envolvidos em SDK |
|
||||
| Configuração de monitoramento separada | Observabilidade em tempo real integrada |
|
||||
| Gerenciamento de orçamento DIY | Controles de custo e degradação integrados |
|
||||
|
||||
### Como Funciona
|
||||
|
||||
1. **Defina Seu Objetivo** → Descreva o que você quer alcançar em linguagem simples
|
||||
2. **Agente de Codificação Gera** → Cria o grafo de agentes, código de conexão e casos de teste
|
||||
3. **Workers Executam** → Nós envolvidos em SDK executam com observabilidade completa e acesso a ferramentas
|
||||
4. **Plano de Controle Monitora** → Métricas em tempo real, aplicação de orçamento, gerenciamento de políticas
|
||||
5. **Auto-Aperfeiçoamento** → Em caso de falha, o sistema evolui o grafo e reimplanta automaticamente
|
||||
|
||||
## Como Aden se Compara
|
||||
|
||||
Aden adota uma abordagem fundamentalmente diferente para o desenvolvimento de agentes. Enquanto a maioria dos frameworks exige que você codifique fluxos de trabalho ou defina manualmente grafos de agentes, Aden usa um **agente de codificação para gerar todo o seu sistema de agentes** a partir de objetivos em linguagem natural. Quando os agentes falham, o framework não apenas registra erros—**ele evolui automaticamente o grafo de agentes** e reimplanta.
|
||||
|
||||
> **Nota:** Para a tabela de comparação detalhada de frameworks e perguntas frequentes, consulte o [README.md](README.md) em inglês.
|
||||
|
||||
### Quando Escolher Aden
|
||||
|
||||
Escolha Aden quando você precisar de:
|
||||
|
||||
- Agentes que **se auto-aperfeiçoam a partir de falhas** sem intervenção manual
|
||||
- **Desenvolvimento orientado a objetivos** onde você descreve resultados, não fluxos de trabalho
|
||||
- **Confiabilidade em produção** com recuperação e reimplantação automáticas
|
||||
- **Iteração rápida** em arquiteturas de agentes sem reescrever código
|
||||
- **Observabilidade completa** com monitoramento em tempo real e supervisão humana
|
||||
|
||||
Escolha outros frameworks quando você precisar de:
|
||||
|
||||
- **Fluxos de trabalho previsíveis e type-safe** (PydanticAI, Mastra)
|
||||
- **RAG e processamento de documentos** (LlamaIndex, Haystack)
|
||||
- **Pesquisa sobre emergência de agentes** (CAMEL)
|
||||
- **Voz/multimodal em tempo real** (TEN Framework)
|
||||
- **Encadeamento simples de componentes** (LangChain, Swarm)
|
||||
|
||||
## Estrutura do Projeto
|
||||
|
||||
```
|
||||
hive/
|
||||
├── core/ # Framework principal - Runtime de agentes, executor de grafos, protocolos
|
||||
├── tools/ # Pacote de Ferramentas MCP - 19 ferramentas para capacidades de agentes
|
||||
├── exports/ # Pacotes de Agentes - Agentes pré-construídos e exemplos
|
||||
├── docs/ # Documentação e guias
|
||||
├── scripts/ # Scripts de build e utilitários
|
||||
├── .claude/ # Habilidades Claude Code para construir agentes
|
||||
├── ENVIRONMENT_SETUP.md # Guia de configuração Python para desenvolvimento de agentes
|
||||
├── DEVELOPER.md # Guia do desenvolvedor
|
||||
├── CONTRIBUTING.md # Diretrizes de contribuição
|
||||
└── ROADMAP.md # Roadmap do produto
|
||||
```
|
||||
|
||||
## Desenvolvimento
|
||||
|
||||
### Desenvolvimento de Agentes Python
|
||||
|
||||
Para construir e executar agentes orientados a objetivos com o framework:
|
||||
|
||||
```bash
|
||||
# Configuração única
|
||||
./scripts/setup-python.sh
|
||||
|
||||
# Isto instala:
|
||||
# - pacote framework (runtime principal)
|
||||
# - pacote aden_tools (19 ferramentas MCP)
|
||||
# - Todas as dependências
|
||||
|
||||
# Construir novos agentes usando habilidades Claude Code
|
||||
claude> /building-agents
|
||||
|
||||
# Testar agentes
|
||||
claude> /testing-agent
|
||||
|
||||
# Executar agentes
|
||||
PYTHONPATH=core:exports python -m agent_name run --input '{...}'
|
||||
```
|
||||
|
||||
Consulte [ENVIRONMENT_SETUP.md](ENVIRONMENT_SETUP.md) para instruções completas de configuração.
|
||||
|
||||
## Documentação
|
||||
|
||||
- **[Guia do Desenvolvedor](DEVELOPER.md)** - Guia abrangente para desenvolvedores
|
||||
- [Começando](docs/getting-started.md) - Instruções de configuração rápida
|
||||
- [Guia de Configuração](docs/configuration.md) - Todas as opções de configuração
|
||||
- [Visão Geral da Arquitetura](docs/architecture.md) - Design e estrutura do sistema
|
||||
|
||||
## Roadmap
|
||||
|
||||
O Aden Agent Framework visa ajudar desenvolvedores a construir agentes auto-adaptativos orientados a resultados. Encontre nosso roadmap aqui
|
||||
|
||||
[ROADMAP.md](ROADMAP.md)
|
||||
|
||||
```mermaid
|
||||
timeline
|
||||
title Aden Agent Framework Roadmap
|
||||
section Foundation
|
||||
Architecture : Node-Based Architecture : Python SDK : LLM Integration (OpenAI, Anthropic, Google) : Communication Protocol
|
||||
Coding Agent : Goal Creation Session : Worker Agent Creation : MCP Tools Integration
|
||||
Worker Agent : Human-in-the-Loop : Callback Handlers : Intervention Points : Streaming Interface
|
||||
Tools : File Use : Memory (STM/LTM) : Web Search : Web Scraper : Audit Trail
|
||||
Core : Eval System : Pydantic Validation : Docker Deployment : Documentation : Sample Agents
|
||||
section Expansion
|
||||
Intelligence : Guardrails : Streaming Mode : Semantic Search
|
||||
Platform : JavaScript SDK : Custom Tool Integrator : Credential Store
|
||||
Deployment : Self-Hosted : Cloud Services : CI/CD Pipeline
|
||||
Templates : Sales Agent : Marketing Agent : Analytics Agent : Training Agent : Smart Form Agent
|
||||
```
|
||||
|
||||
## Comunidade e Suporte
|
||||
|
||||
Usamos [Discord](https://discord.com/invite/MXE49hrKDk) para suporte, solicitações de funcionalidades e discussões da comunidade.
|
||||
|
||||
- Discord - [Junte-se à nossa comunidade](https://discord.com/invite/MXE49hrKDk)
|
||||
- Twitter/X - [@adenhq](https://x.com/aden_hq)
|
||||
- LinkedIn - [Página da Empresa](https://www.linkedin.com/company/teamaden/)
|
||||
|
||||
## Contribuindo
|
||||
|
||||
Aceitamos contribuições! Por favor, consulte [CONTRIBUTING.md](CONTRIBUTING.md) para diretrizes.
|
||||
|
||||
1. Faça fork do repositório
|
||||
2. Crie sua branch de funcionalidade (`git checkout -b feature/amazing-feature`)
|
||||
3. Faça commit das suas alterações (`git commit -m 'Add amazing feature'`)
|
||||
4. Faça push para a branch (`git push origin feature/amazing-feature`)
|
||||
5. Abra um Pull Request
|
||||
|
||||
## Junte-se ao Nosso Time
|
||||
|
||||
**Estamos contratando!** Junte-se a nós em funções de engenharia, pesquisa e go-to-market.
|
||||
|
||||
[Ver Posições Abertas](https://jobs.adenhq.com/a8cec478-cdbc-473c-bbd4-f4b7027ec193/applicant)
|
||||
|
||||
## Segurança
|
||||
|
||||
Para questões de segurança, por favor consulte [SECURITY.md](SECURITY.md).
|
||||
|
||||
## Licença
|
||||
|
||||
Este projeto está licenciado sob a Licença Apache 2.0 - veja o arquivo [LICENSE](LICENSE) para detalhes.
|
||||
|
||||
## Perguntas Frequentes (FAQ)
|
||||
|
||||
> **Nota:** Para as perguntas frequentes completas, consulte o [README.md](README.md) em inglês.
|
||||
|
||||
**P: O Aden depende do LangChain ou outros frameworks de agentes?**
|
||||
|
||||
Não. O Aden é construído do zero sem dependências do LangChain, CrewAI ou outros frameworks de agentes. O framework é projetado para ser leve e flexível, gerando grafos de agentes dinamicamente em vez de depender de componentes predefinidos.
|
||||
|
||||
**P: Quais provedores de LLM o Aden suporta?**
|
||||
|
||||
O Aden suporta mais de 100 provedores de LLM através da integração LiteLLM, incluindo OpenAI (GPT-4, GPT-4o), Anthropic (modelos Claude), Google Gemini, Mistral, Groq e muitos mais. Simplesmente configure a variável de ambiente da chave API apropriada e especifique o nome do modelo.
|
||||
|
||||
**P: O Aden é open-source?**
|
||||
|
||||
Sim, o Aden é totalmente open-source sob a Licença Apache 2.0. Incentivamos ativamente contribuições e colaboração da comunidade.
|
||||
|
||||
**P: O que torna o Aden diferente de outros frameworks de agentes?**
|
||||
|
||||
O Aden gera todo o seu sistema de agentes a partir de objetivos em linguagem natural usando um agente de codificação—você não codifica fluxos de trabalho nem define grafos manualmente. Quando os agentes falham, o framework captura automaticamente os dados de falha, evolui o grafo de agentes e reimplanta. Este loop de auto-aperfeiçoamento é único do Aden.
|
||||
|
||||
**P: O Aden suporta fluxos de trabalho com humano no loop?**
|
||||
|
||||
Sim, o Aden suporta totalmente fluxos de trabalho com humano no loop através de nós de intervenção que pausam a execução para entrada humana. Estes incluem timeouts configuráveis e políticas de escalonamento, permitindo colaboração perfeita entre especialistas humanos e agentes de IA.
|
||||
|
||||
---
|
||||
|
||||
<p align="center">
|
||||
Feito com 🔥 Paixão em San Francisco
|
||||
</p>
|
||||
+339
@@ -0,0 +1,339 @@
|
||||
<p align="center">
|
||||
<img width="100%" alt="Hive Banner" src="https://storage.googleapis.com/aden-prod-assets/website/aden-title-card.png" />
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="README.md">English</a> |
|
||||
<a href="README.zh-CN.md">简体中文</a> |
|
||||
<a href="README.es.md">Español</a> |
|
||||
<a href="README.pt.md">Português</a> |
|
||||
<a href="README.ja.md">日本語</a> |
|
||||
<a href="README.ru.md">Русский</a>
|
||||
</p>
|
||||
|
||||
[](https://github.com/adenhq/hive/blob/main/LICENSE)
|
||||
[](https://www.ycombinator.com/companies/aden)
|
||||
[](https://hub.docker.com/u/adenhq)
|
||||
[](https://discord.com/invite/MXE49hrKDk)
|
||||
[](https://x.com/aden_hq)
|
||||
[](https://www.linkedin.com/company/teamaden/)
|
||||
|
||||
<p align="center">
|
||||
<img src="https://img.shields.io/badge/AI_Agents-Self--Improving-brightgreen?style=flat-square" alt="AI Agents" />
|
||||
<img src="https://img.shields.io/badge/Multi--Agent-Systems-blue?style=flat-square" alt="Multi-Agent" />
|
||||
<img src="https://img.shields.io/badge/Goal--Driven-Development-purple?style=flat-square" alt="Goal-Driven" />
|
||||
<img src="https://img.shields.io/badge/Human--in--the--Loop-orange?style=flat-square" alt="HITL" />
|
||||
<img src="https://img.shields.io/badge/Production--Ready-red?style=flat-square" alt="Production" />
|
||||
</p>
|
||||
<p align="center">
|
||||
<img src="https://img.shields.io/badge/OpenAI-supported-412991?style=flat-square&logo=openai" alt="OpenAI" />
|
||||
<img src="https://img.shields.io/badge/Anthropic-supported-d4a574?style=flat-square" alt="Anthropic" />
|
||||
<img src="https://img.shields.io/badge/Google_Gemini-supported-4285F4?style=flat-square&logo=google" alt="Gemini" />
|
||||
<img src="https://img.shields.io/badge/MCP-19_Tools-00ADD8?style=flat-square" alt="MCP" />
|
||||
</p>
|
||||
|
||||
## Обзор
|
||||
|
||||
Создавайте надёжных, самосовершенствующихся ИИ-агентов без жёсткого кодирования рабочих процессов. Определите свою цель через разговор с кодирующим агентом, и фреймворк сгенерирует граф узлов с динамически созданным кодом соединений. Когда что-то ломается, фреймворк захватывает данные об ошибке, эволюционирует агента через кодирующего агента и переразвёртывает. Встроенные узлы человеческого вмешательства, управление учётными данными и мониторинг в реальном времени дают вам контроль без ущерба для адаптивности.
|
||||
|
||||
Посетите [adenhq.com](https://adenhq.com) для полной документации, примеров и руководств.
|
||||
|
||||
## Что такое Aden
|
||||
|
||||
<p align="center">
|
||||
<img width="100%" alt="Aden Architecture" src="docs/assets/aden-architecture-diagram.jpg" />
|
||||
</p>
|
||||
|
||||
Aden — это платформа для создания, развёртывания, эксплуатации и адаптации ИИ-агентов:
|
||||
|
||||
- **Создание** - Кодирующий агент генерирует специализированных рабочих агентов (продажи, маркетинг, операции) из целей на естественном языке
|
||||
- **Развёртывание** - Headless-развёртывание с интеграцией CI/CD и полным управлением жизненным циклом API
|
||||
- **Эксплуатация** - Мониторинг в реальном времени, наблюдаемость и защитные барьеры времени выполнения обеспечивают надёжность агентов
|
||||
- **Адаптация** - Непрерывная оценка, контроль и адаптация гарантируют улучшение агентов со временем
|
||||
- **Инфраструктура** - Общая память, интеграции LLM, инструменты и навыки питают каждого агента
|
||||
|
||||
## Быстрые ссылки
|
||||
|
||||
- **[Документация](https://docs.adenhq.com/)** - Полные руководства и справочник API
|
||||
- **[Руководство по самостоятельному хостингу](https://docs.adenhq.com/getting-started/quickstart)** - Разверните Hive в своей инфраструктуре
|
||||
- **[История изменений](https://github.com/adenhq/hive/releases)** - Последние обновления и релизы
|
||||
<!-- - **[Дорожная карта](https://adenhq.com/roadmap)** - Предстоящие функции и планы -->
|
||||
- **[Сообщить о проблеме](https://github.com/adenhq/hive/issues)** - Отчёты об ошибках и запросы функций
|
||||
|
||||
## Быстрый старт
|
||||
|
||||
### Предварительные требования
|
||||
|
||||
- [Python 3.11+](https://www.python.org/downloads/) - Для разработки агентов
|
||||
- [Docker](https://docs.docker.com/get-docker/) (v20.10+) - Опционально, для контейнеризованных инструментов
|
||||
|
||||
### Установка
|
||||
|
||||
```bash
|
||||
# Клонировать репозиторий
|
||||
git clone https://github.com/adenhq/hive.git
|
||||
cd hive
|
||||
|
||||
# Запустить настройку окружения Python
|
||||
./scripts/setup-python.sh
|
||||
```
|
||||
|
||||
Это установит:
|
||||
- **framework** - Основная среда выполнения агентов и исполнитель графов
|
||||
- **aden_tools** - 19 инструментов MCP для возможностей агентов
|
||||
- Все необходимые зависимости
|
||||
|
||||
### Создайте своего первого агента
|
||||
|
||||
```bash
|
||||
# Установить навыки Claude Code (один раз)
|
||||
./quickstart.sh
|
||||
|
||||
# Создать агента с помощью Claude Code
|
||||
claude> /building-agents
|
||||
|
||||
# Протестировать агента
|
||||
claude> /testing-agent
|
||||
|
||||
# Запустить агента
|
||||
PYTHONPATH=core:exports python -m your_agent_name run --input '{...}'
|
||||
```
|
||||
|
||||
**[📖 Полное руководство по настройке](ENVIRONMENT_SETUP.md)** - Подробные инструкции для разработки агентов
|
||||
|
||||
## Функции
|
||||
|
||||
- **Целеориентированная разработка** - Определяйте цели на естественном языке; кодирующий агент генерирует граф агентов и код соединений для их достижения
|
||||
- **Самоадаптирующиеся агенты** - Фреймворк захватывает сбои, обновляет цели и обновляет граф агентов
|
||||
- **Динамические соединения узлов** - Без предопределённых рёбер; код соединений генерируется любым способным LLM на основе ваших целей
|
||||
- **Узлы, обёрнутые SDK** - Каждый узел получает общую память, локальную RLM-память, мониторинг, инструменты и доступ к LLM из коробки
|
||||
- **Человек в контуре** - Узлы вмешательства, которые приостанавливают выполнение для человеческого ввода с настраиваемыми таймаутами и эскалацией
|
||||
- **Наблюдаемость в реальном времени** - WebSocket-стриминг для живого мониторинга выполнения агентов, решений и межузловой коммуникации
|
||||
- **Контроль затрат и бюджета** - Устанавливайте лимиты расходов, ограничения и политики автоматической деградации модели
|
||||
- **Готовность к продакшену** - Возможность самостоятельного хостинга, создан для масштабирования и надёжности
|
||||
|
||||
## Почему Aden
|
||||
|
||||
Традиционные фреймворки агентов требуют ручного проектирования рабочих процессов, определения взаимодействий агентов и реактивной обработки сбоев. Aden переворачивает эту парадигму — **вы описываете результаты, и система строит себя сама**.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph BUILD["🏗️ BUILD"]
|
||||
GOAL["Define Goal<br/>+ Success Criteria"] --> NODES["Add Nodes<br/>LLM/Router/Function"]
|
||||
NODES --> EDGES["Connect Edges<br/>on_success/failure/conditional"]
|
||||
EDGES --> TEST["Test & Validate"] --> APPROVE["Approve & Export"]
|
||||
end
|
||||
|
||||
subgraph EXPORT["📦 EXPORT"]
|
||||
direction TB
|
||||
JSON["agent.json<br/>(GraphSpec)"]
|
||||
TOOLS["tools.py<br/>(Functions)"]
|
||||
MCP["mcp_servers.json<br/>(Integrations)"]
|
||||
end
|
||||
|
||||
subgraph RUN["🚀 RUNTIME"]
|
||||
LOAD["AgentRunner<br/>Load + Parse"] --> SETUP["Setup Runtime<br/>+ ToolRegistry"]
|
||||
SETUP --> EXEC["GraphExecutor<br/>Execute Nodes"]
|
||||
|
||||
subgraph DECISION["Decision Recording"]
|
||||
DEC1["runtime.decide()<br/>intent → options → choice"]
|
||||
DEC2["runtime.record_outcome()<br/>success, result, metrics"]
|
||||
end
|
||||
end
|
||||
|
||||
subgraph INFRA["⚙️ INFRASTRUCTURE"]
|
||||
CTX["NodeContext<br/>memory • llm • tools"]
|
||||
STORE[("FileStorage<br/>Runs & Decisions")]
|
||||
end
|
||||
|
||||
APPROVE --> EXPORT
|
||||
EXPORT --> LOAD
|
||||
EXEC --> DECISION
|
||||
EXEC --> CTX
|
||||
DECISION --> STORE
|
||||
STORE -.->|"Analyze & Improve"| NODES
|
||||
|
||||
style BUILD fill:#ffbe42,stroke:#cc5d00,stroke-width:3px,color:#333
|
||||
style EXPORT fill:#fff59d,stroke:#ed8c00,stroke-width:2px,color:#333
|
||||
style RUN fill:#ffb100,stroke:#cc5d00,stroke-width:3px,color:#333
|
||||
style DECISION fill:#ffcc80,stroke:#ed8c00,stroke-width:2px,color:#333
|
||||
style INFRA fill:#e8763d,stroke:#cc5d00,stroke-width:3px,color:#fff
|
||||
style STORE fill:#ed8c00,stroke:#cc5d00,stroke-width:2px,color:#fff
|
||||
```
|
||||
|
||||
### Преимущество Aden
|
||||
|
||||
| Традиционные фреймворки | Aden |
|
||||
|-------------------------|------|
|
||||
| Жёсткое кодирование рабочих процессов | Описание целей на естественном языке |
|
||||
| Ручное определение графов | Автоматически генерируемые графы агентов |
|
||||
| Реактивная обработка ошибок | Проактивная самоэволюция |
|
||||
| Статические конфигурации инструментов | Динамические узлы, обёрнутые SDK |
|
||||
| Отдельная настройка мониторинга | Встроенная наблюдаемость в реальном времени |
|
||||
| DIY управление бюджетом | Интегрированный контроль затрат и деградация |
|
||||
|
||||
### Как это работает
|
||||
|
||||
1. **Определите цель** → Опишите, чего хотите достичь, простым языком
|
||||
2. **Кодирующий агент генерирует** → Создаёт граф агентов, код соединений и тестовые случаи
|
||||
3. **Рабочие выполняют** → Узлы, обёрнутые SDK, работают с полной наблюдаемостью и доступом к инструментам
|
||||
4. **Плоскость управления мониторит** → Метрики в реальном времени, применение бюджета, управление политиками
|
||||
5. **Самосовершенствование** → При сбое система эволюционирует граф и автоматически переразвёртывает
|
||||
|
||||
## Сравнение Aden
|
||||
|
||||
Aden использует принципиально иной подход к разработке агентов. В то время как большинство фреймворков требуют жёсткого кодирования рабочих процессов или ручного определения графов агентов, Aden использует **кодирующего агента для генерации всей системы агентов** из целей на естественном языке. Когда агенты терпят неудачу, фреймворк не просто регистрирует ошибки — он **автоматически эволюционирует граф агентов** и переразвёртывает.
|
||||
|
||||
> **Примечание:** Для подробной таблицы сравнения фреймворков и часто задаваемых вопросов обратитесь к английской версии [README.md](README.md).
|
||||
|
||||
### Когда выбирать Aden
|
||||
|
||||
Выбирайте Aden, когда вам нужны:
|
||||
|
||||
- Агенты, которые **самосовершенствуются на основе сбоев** без ручного вмешательства
|
||||
- **Целеориентированная разработка**, где вы описываете результаты, а не рабочие процессы
|
||||
- **Надёжность продакшена** с автоматическим восстановлением и переразвёртыванием
|
||||
- **Быстрая итерация** архитектур агентов без переписывания кода
|
||||
- **Полная наблюдаемость** с мониторингом в реальном времени и человеческим надзором
|
||||
|
||||
Выбирайте другие фреймворки, когда вам нужны:
|
||||
|
||||
- **Предсказуемые, типобезопасные рабочие процессы** (PydanticAI, Mastra)
|
||||
- **RAG и обработка документов** (LlamaIndex, Haystack)
|
||||
- **Исследование эмерджентности агентов** (CAMEL)
|
||||
- **Голос/мультимодальность в реальном времени** (TEN Framework)
|
||||
- **Простое связывание компонентов** (LangChain, Swarm)
|
||||
|
||||
## Структура проекта
|
||||
|
||||
```
|
||||
hive/
|
||||
├── core/ # Основной фреймворк - Среда выполнения агентов, исполнитель графов, протоколы
|
||||
├── tools/ # Пакет инструментов MCP - 19 инструментов для возможностей агентов
|
||||
├── exports/ # Пакеты агентов - Предварительно созданные агенты и примеры
|
||||
├── docs/ # Документация и руководства
|
||||
├── scripts/ # Скрипты сборки и утилиты
|
||||
├── .claude/ # Навыки Claude Code для создания агентов
|
||||
├── ENVIRONMENT_SETUP.md # Руководство по настройке Python для разработки агентов
|
||||
├── DEVELOPER.md # Руководство разработчика
|
||||
├── CONTRIBUTING.md # Руководство по участию
|
||||
└── ROADMAP.md # Дорожная карта продукта
|
||||
```
|
||||
|
||||
## Разработка
|
||||
|
||||
### Разработка агентов на Python
|
||||
|
||||
Для создания и запуска целеориентированных агентов с помощью фреймворка:
|
||||
|
||||
```bash
|
||||
# Одноразовая настройка
|
||||
./scripts/setup-python.sh
|
||||
|
||||
# Это установит:
|
||||
# - пакет framework (основная среда выполнения)
|
||||
# - пакет aden_tools (19 инструментов MCP)
|
||||
# - Все зависимости
|
||||
|
||||
# Создать новых агентов с помощью навыков Claude Code
|
||||
claude> /building-agents
|
||||
|
||||
# Протестировать агентов
|
||||
claude> /testing-agent
|
||||
|
||||
# Запустить агентов
|
||||
PYTHONPATH=core:exports python -m agent_name run --input '{...}'
|
||||
```
|
||||
|
||||
Обратитесь к [ENVIRONMENT_SETUP.md](ENVIRONMENT_SETUP.md) для полных инструкций по настройке.
|
||||
|
||||
## Документация
|
||||
|
||||
- **[Руководство разработчика](DEVELOPER.md)** - Полное руководство для разработчиков
|
||||
- [Начало работы](docs/getting-started.md) - Инструкции по быстрой настройке
|
||||
- [Руководство по конфигурации](docs/configuration.md) - Все опции конфигурации
|
||||
- [Обзор архитектуры](docs/architecture.md) - Дизайн и структура системы
|
||||
|
||||
## Дорожная карта
|
||||
|
||||
Aden Agent Framework призван помочь разработчикам создавать самоадаптирующихся агентов, ориентированных на результат. Найдите нашу дорожную карту здесь
|
||||
|
||||
[ROADMAP.md](ROADMAP.md)
|
||||
|
||||
```mermaid
|
||||
timeline
|
||||
title Aden Agent Framework Roadmap
|
||||
section Foundation
|
||||
Architecture : Node-Based Architecture : Python SDK : LLM Integration (OpenAI, Anthropic, Google) : Communication Protocol
|
||||
Coding Agent : Goal Creation Session : Worker Agent Creation : MCP Tools Integration
|
||||
Worker Agent : Human-in-the-Loop : Callback Handlers : Intervention Points : Streaming Interface
|
||||
Tools : File Use : Memory (STM/LTM) : Web Search : Web Scraper : Audit Trail
|
||||
Core : Eval System : Pydantic Validation : Docker Deployment : Documentation : Sample Agents
|
||||
section Expansion
|
||||
Intelligence : Guardrails : Streaming Mode : Semantic Search
|
||||
Platform : JavaScript SDK : Custom Tool Integrator : Credential Store
|
||||
Deployment : Self-Hosted : Cloud Services : CI/CD Pipeline
|
||||
Templates : Sales Agent : Marketing Agent : Analytics Agent : Training Agent : Smart Form Agent
|
||||
```
|
||||
|
||||
## Сообщество и поддержка
|
||||
|
||||
Мы используем [Discord](https://discord.com/invite/MXE49hrKDk) для поддержки, запросов функций и обсуждений сообщества.
|
||||
|
||||
- Discord - [Присоединиться к сообществу](https://discord.com/invite/MXE49hrKDk)
|
||||
- Twitter/X - [@adenhq](https://x.com/aden_hq)
|
||||
- LinkedIn - [Страница компании](https://www.linkedin.com/company/teamaden/)
|
||||
|
||||
## Участие в разработке
|
||||
|
||||
Мы приветствуем вклад! Пожалуйста, ознакомьтесь с [CONTRIBUTING.md](CONTRIBUTING.md) для руководств.
|
||||
|
||||
1. Сделайте форк репозитория
|
||||
2. Создайте ветку функции (`git checkout -b feature/amazing-feature`)
|
||||
3. Зафиксируйте изменения (`git commit -m 'Add amazing feature'`)
|
||||
4. Отправьте в ветку (`git push origin feature/amazing-feature`)
|
||||
5. Откройте Pull Request
|
||||
|
||||
## Присоединяйтесь к команде
|
||||
|
||||
**Мы нанимаем!** Присоединяйтесь к нам на позициях в инженерии, исследованиях и выходе на рынок.
|
||||
|
||||
[Посмотреть открытые позиции](https://jobs.adenhq.com/a8cec478-cdbc-473c-bbd4-f4b7027ec193/applicant)
|
||||
|
||||
## Безопасность
|
||||
|
||||
По вопросам безопасности, пожалуйста, обратитесь к [SECURITY.md](SECURITY.md).
|
||||
|
||||
## Лицензия
|
||||
|
||||
Этот проект лицензирован под лицензией Apache 2.0 - см. файл [LICENSE](LICENSE) для деталей.
|
||||
|
||||
## Часто задаваемые вопросы (FAQ)
|
||||
|
||||
> **Примечание:** Для полных часто задаваемых вопросов обратитесь к английской версии [README.md](README.md).
|
||||
|
||||
**В: Зависит ли Aden от LangChain или других фреймворков агентов?**
|
||||
|
||||
Нет. Aden построен с нуля без зависимостей от LangChain, CrewAI или других фреймворков агентов. Фреймворк разработан лёгким и гибким, динамически генерируя графы агентов вместо того, чтобы полагаться на предопределённые компоненты.
|
||||
|
||||
**В: Каких провайдеров LLM поддерживает Aden?**
|
||||
|
||||
Aden поддерживает более 100 провайдеров LLM через интеграцию LiteLLM, включая OpenAI (GPT-4, GPT-4o), Anthropic (модели Claude), Google Gemini, Mistral, Groq и многих других. Просто настройте соответствующую переменную окружения API-ключа и укажите имя модели.
|
||||
|
||||
**В: Aden с открытым исходным кодом?**
|
||||
|
||||
Да, Aden полностью с открытым исходным кодом под лицензией Apache 2.0. Мы активно поощряем вклад и сотрудничество сообщества.
|
||||
|
||||
**В: Что делает Aden отличным от других фреймворков агентов?**
|
||||
|
||||
Aden генерирует всю систему агентов из целей на естественном языке, используя кодирующего агента — вы не кодируете рабочие процессы и не определяете графы вручную. Когда агенты терпят неудачу, фреймворк автоматически захватывает данные о сбое, эволюционирует граф агентов и переразвёртывает. Этот цикл самосовершенствования уникален для Aden.
|
||||
|
||||
**В: Поддерживает ли Aden рабочие процессы с человеком в контуре?**
|
||||
|
||||
Да, Aden полностью поддерживает рабочие процессы с человеком в контуре через узлы вмешательства, которые приостанавливают выполнение для человеческого ввода. Они включают настраиваемые таймауты и политики эскалации, обеспечивая бесшовное сотрудничество между экспертами-людьми и ИИ-агентами.
|
||||
|
||||
---
|
||||
|
||||
<p align="center">
|
||||
Сделано с 🔥 Страстью в Сан-Франциско
|
||||
</p>
|
||||
+339
@@ -0,0 +1,339 @@
|
||||
<p align="center">
|
||||
<img width="100%" alt="Hive Banner" src="https://storage.googleapis.com/aden-prod-assets/website/aden-title-card.png" />
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="README.md">English</a> |
|
||||
<a href="README.zh-CN.md">简体中文</a> |
|
||||
<a href="README.es.md">Español</a> |
|
||||
<a href="README.pt.md">Português</a> |
|
||||
<a href="README.ja.md">日本語</a> |
|
||||
<a href="README.ru.md">Русский</a>
|
||||
</p>
|
||||
|
||||
[](https://github.com/adenhq/hive/blob/main/LICENSE)
|
||||
[](https://www.ycombinator.com/companies/aden)
|
||||
[](https://hub.docker.com/u/adenhq)
|
||||
[](https://discord.com/invite/MXE49hrKDk)
|
||||
[](https://x.com/aden_hq)
|
||||
[](https://www.linkedin.com/company/teamaden/)
|
||||
|
||||
<p align="center">
|
||||
<img src="https://img.shields.io/badge/AI_Agents-Self--Improving-brightgreen?style=flat-square" alt="AI Agents" />
|
||||
<img src="https://img.shields.io/badge/Multi--Agent-Systems-blue?style=flat-square" alt="Multi-Agent" />
|
||||
<img src="https://img.shields.io/badge/Goal--Driven-Development-purple?style=flat-square" alt="Goal-Driven" />
|
||||
<img src="https://img.shields.io/badge/Human--in--the--Loop-orange?style=flat-square" alt="HITL" />
|
||||
<img src="https://img.shields.io/badge/Production--Ready-red?style=flat-square" alt="Production" />
|
||||
</p>
|
||||
<p align="center">
|
||||
<img src="https://img.shields.io/badge/OpenAI-supported-412991?style=flat-square&logo=openai" alt="OpenAI" />
|
||||
<img src="https://img.shields.io/badge/Anthropic-supported-d4a574?style=flat-square" alt="Anthropic" />
|
||||
<img src="https://img.shields.io/badge/Google_Gemini-supported-4285F4?style=flat-square&logo=google" alt="Gemini" />
|
||||
<img src="https://img.shields.io/badge/MCP-19_Tools-00ADD8?style=flat-square" alt="MCP" />
|
||||
</p>
|
||||
|
||||
## 概述
|
||||
|
||||
构建可靠的、自我改进的 AI 智能体,无需硬编码工作流。通过与编码智能体对话来定义目标,框架会生成带有动态创建连接代码的节点图。当出现问题时,框架会捕获故障数据,通过编码智能体进化智能体,并重新部署。内置的人机协作节点、凭证管理和实时监控让您在保持适应性的同时拥有完全控制权。
|
||||
|
||||
访问 [adenhq.com](https://adenhq.com) 获取完整文档、示例和指南。
|
||||
|
||||
## 什么是 Aden
|
||||
|
||||
<p align="center">
|
||||
<img width="100%" alt="Aden Architecture" src="docs/assets/aden-architecture-diagram.jpg" />
|
||||
</p>
|
||||
|
||||
Aden 是一个用于构建、部署、运营和适应 AI 智能体的平台:
|
||||
|
||||
- **构建** - 编码智能体根据自然语言目标生成专业的工作智能体(销售、营销、运营)
|
||||
- **部署** - 无头部署,支持 CI/CD 集成和完整的 API 生命周期管理
|
||||
- **运营** - 实时监控、可观测性和运行时护栏确保智能体可靠运行
|
||||
- **适应** - 持续评估、监督和适应确保智能体随时间改进
|
||||
- **基础设施** - 共享内存、LLM 集成、工具和技能为每个智能体提供支持
|
||||
|
||||
## 快速链接
|
||||
|
||||
- **[文档](https://docs.adenhq.com/)** - 完整指南和 API 参考
|
||||
- **[自托管指南](https://docs.adenhq.com/getting-started/quickstart)** - 在您的基础设施上部署 Hive
|
||||
- **[更新日志](https://github.com/adenhq/hive/releases)** - 最新更新和版本
|
||||
<!-- - **[路线图](https://adenhq.com/roadmap)** - 即将推出的功能和计划 -->
|
||||
- **[报告问题](https://github.com/adenhq/hive/issues)** - Bug 报告和功能请求
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 前置要求
|
||||
|
||||
- [Python 3.11+](https://www.python.org/downloads/) - 用于智能体开发
|
||||
- [Docker](https://docs.docker.com/get-docker/) (v20.10+) - 可选,用于容器化工具
|
||||
|
||||
### 安装
|
||||
|
||||
```bash
|
||||
# 克隆仓库
|
||||
git clone https://github.com/adenhq/hive.git
|
||||
cd hive
|
||||
|
||||
# 运行 Python 环境设置
|
||||
./scripts/setup-python.sh
|
||||
```
|
||||
|
||||
这将安装:
|
||||
- **framework** - 核心智能体运行时和图执行器
|
||||
- **aden_tools** - 19 个 MCP 工具提供智能体能力
|
||||
- 所有必需的依赖项
|
||||
|
||||
### 构建您的第一个智能体
|
||||
|
||||
```bash
|
||||
# 安装 Claude Code 技能(一次性)
|
||||
./quickstart.sh
|
||||
|
||||
# 使用 Claude Code 构建智能体
|
||||
claude> /building-agents
|
||||
|
||||
# 测试您的智能体
|
||||
claude> /testing-agent
|
||||
|
||||
# 运行您的智能体
|
||||
PYTHONPATH=core:exports python -m your_agent_name run --input '{...}'
|
||||
```
|
||||
|
||||
**[📖 完整设置指南](ENVIRONMENT_SETUP.md)** - 智能体开发的详细说明
|
||||
|
||||
## 功能特性
|
||||
|
||||
- **目标驱动开发** - 用自然语言定义目标;编码智能体生成智能体图和连接代码来实现它们
|
||||
- **自适应智能体** - 框架捕获故障,更新目标并更新智能体图
|
||||
- **动态节点连接** - 没有预定义边;连接代码由任何有能力的 LLM 根据您的目标生成
|
||||
- **SDK 封装节点** - 每个节点开箱即用地获得共享内存、本地 RLM 内存、监控、工具和 LLM 访问
|
||||
- **人机协作** - 干预节点暂停执行以等待人工输入,支持可配置的超时和升级
|
||||
- **实时可观测性** - WebSocket 流式传输用于实时监控智能体执行、决策和节点间通信
|
||||
- **成本与预算控制** - 设置支出限制、节流和自动模型降级策略
|
||||
- **生产就绪** - 可自托管,为规模和可靠性而构建
|
||||
|
||||
## 为什么选择 Aden
|
||||
|
||||
传统智能体框架要求您手动设计工作流、定义智能体交互并被动处理故障。Aden 颠覆了这一范式——**您描述结果,系统自动构建自己**。
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph BUILD["🏗️ BUILD"]
|
||||
GOAL["Define Goal<br/>+ Success Criteria"] --> NODES["Add Nodes<br/>LLM/Router/Function"]
|
||||
NODES --> EDGES["Connect Edges<br/>on_success/failure/conditional"]
|
||||
EDGES --> TEST["Test & Validate"] --> APPROVE["Approve & Export"]
|
||||
end
|
||||
|
||||
subgraph EXPORT["📦 EXPORT"]
|
||||
direction TB
|
||||
JSON["agent.json<br/>(GraphSpec)"]
|
||||
TOOLS["tools.py<br/>(Functions)"]
|
||||
MCP["mcp_servers.json<br/>(Integrations)"]
|
||||
end
|
||||
|
||||
subgraph RUN["🚀 RUNTIME"]
|
||||
LOAD["AgentRunner<br/>Load + Parse"] --> SETUP["Setup Runtime<br/>+ ToolRegistry"]
|
||||
SETUP --> EXEC["GraphExecutor<br/>Execute Nodes"]
|
||||
|
||||
subgraph DECISION["Decision Recording"]
|
||||
DEC1["runtime.decide()<br/>intent → options → choice"]
|
||||
DEC2["runtime.record_outcome()<br/>success, result, metrics"]
|
||||
end
|
||||
end
|
||||
|
||||
subgraph INFRA["⚙️ INFRASTRUCTURE"]
|
||||
CTX["NodeContext<br/>memory • llm • tools"]
|
||||
STORE[("FileStorage<br/>Runs & Decisions")]
|
||||
end
|
||||
|
||||
APPROVE --> EXPORT
|
||||
EXPORT --> LOAD
|
||||
EXEC --> DECISION
|
||||
EXEC --> CTX
|
||||
DECISION --> STORE
|
||||
STORE -.->|"Analyze & Improve"| NODES
|
||||
|
||||
style BUILD fill:#ffbe42,stroke:#cc5d00,stroke-width:3px,color:#333
|
||||
style EXPORT fill:#fff59d,stroke:#ed8c00,stroke-width:2px,color:#333
|
||||
style RUN fill:#ffb100,stroke:#cc5d00,stroke-width:3px,color:#333
|
||||
style DECISION fill:#ffcc80,stroke:#ed8c00,stroke-width:2px,color:#333
|
||||
style INFRA fill:#e8763d,stroke:#cc5d00,stroke-width:3px,color:#fff
|
||||
style STORE fill:#ed8c00,stroke:#cc5d00,stroke-width:2px,color:#fff
|
||||
```
|
||||
|
||||
### Aden 的优势
|
||||
|
||||
| 传统框架 | Aden |
|
||||
|----------|------|
|
||||
| 硬编码智能体工作流 | 用自然语言描述目标 |
|
||||
| 手动图定义 | 自动生成智能体图 |
|
||||
| 被动错误处理 | 主动自我进化 |
|
||||
| 静态工具配置 | 动态 SDK 封装节点 |
|
||||
| 单独设置监控 | 内置实时可观测性 |
|
||||
| DIY 预算管理 | 集成成本控制和降级 |
|
||||
|
||||
### 工作原理
|
||||
|
||||
1. **定义目标** → 用简单英语描述您想要实现的目标
|
||||
2. **编码智能体生成** → 创建智能体图、连接代码和测试用例
|
||||
3. **工作节点执行** → SDK 封装节点以完全可观测性和工具访问运行
|
||||
4. **控制平面监控** → 实时指标、预算执行、策略管理
|
||||
5. **自我改进** → 失败时,系统进化图并自动重新部署
|
||||
|
||||
## Aden 与其他框架的比较
|
||||
|
||||
Aden 在智能体开发方面采取了根本不同的方法。虽然大多数框架要求您硬编码工作流或手动定义智能体图,但 Aden 使用**编码智能体从自然语言目标生成整个智能体系统**。当智能体失败时,框架不仅记录错误——它会**自动进化智能体图**并重新部署。
|
||||
|
||||
> **注意:** 详细的框架比较表和常见问题解答,请参阅英文版 [README.md](README.md)。
|
||||
|
||||
### 何时选择 Aden
|
||||
|
||||
选择 Aden 当您需要:
|
||||
|
||||
- 智能体从失败中**自我改进**而无需人工干预
|
||||
- **目标驱动的开发**,您描述结果而非工作流
|
||||
- 具有自动恢复和重新部署的**生产可靠性**
|
||||
- 无需重写代码即可**快速迭代**智能体架构
|
||||
- 具有实时监控和人工监督的**完整可观测性**
|
||||
|
||||
选择其他框架当您需要:
|
||||
|
||||
- **类型安全、可预测的工作流**(PydanticAI、Mastra)
|
||||
- **RAG 和文档处理**(LlamaIndex、Haystack)
|
||||
- **智能体涌现的研究**(CAMEL)
|
||||
- **实时语音/多模态**(TEN Framework)
|
||||
- **简单的组件链接**(LangChain、Swarm)
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
hive/
|
||||
├── core/ # 核心框架 - 智能体运行时、图执行器、协议
|
||||
├── tools/ # MCP 工具包 - 19 个工具提供智能体能力
|
||||
├── exports/ # 智能体包 - 预构建的智能体和示例
|
||||
├── docs/ # 文档和指南
|
||||
├── scripts/ # 构建和实用脚本
|
||||
├── .claude/ # Claude Code 技能用于构建智能体
|
||||
├── ENVIRONMENT_SETUP.md # 智能体开发的 Python 设置指南
|
||||
├── DEVELOPER.md # 开发者指南
|
||||
├── CONTRIBUTING.md # 贡献指南
|
||||
└── ROADMAP.md # 产品路线图
|
||||
```
|
||||
|
||||
## 开发
|
||||
|
||||
### Python 智能体开发
|
||||
|
||||
使用框架构建和运行目标驱动的智能体:
|
||||
|
||||
```bash
|
||||
# 一次性设置
|
||||
./scripts/setup-python.sh
|
||||
|
||||
# 这将安装:
|
||||
# - framework 包(核心运行时)
|
||||
# - aden_tools 包(19 个 MCP 工具)
|
||||
# - 所有依赖项
|
||||
|
||||
# 使用 Claude Code 技能构建新智能体
|
||||
claude> /building-agents
|
||||
|
||||
# 测试智能体
|
||||
claude> /testing-agent
|
||||
|
||||
# 运行智能体
|
||||
PYTHONPATH=core:exports python -m agent_name run --input '{...}'
|
||||
```
|
||||
|
||||
完整设置说明请参阅 [ENVIRONMENT_SETUP.md](ENVIRONMENT_SETUP.md)。
|
||||
|
||||
## 文档
|
||||
|
||||
- **[开发者指南](DEVELOPER.md)** - 开发者综合指南
|
||||
- [入门指南](docs/getting-started.md) - 快速设置说明
|
||||
- [配置指南](docs/configuration.md) - 所有配置选项
|
||||
- [架构概述](docs/architecture.md) - 系统设计和结构
|
||||
|
||||
## 路线图
|
||||
|
||||
Aden 智能体框架旨在帮助开发者构建面向结果的、自适应的智能体。请在此查看我们的路线图
|
||||
|
||||
[ROADMAP.md](ROADMAP.md)
|
||||
|
||||
```mermaid
|
||||
timeline
|
||||
title Aden Agent Framework Roadmap
|
||||
section Foundation
|
||||
Architecture : Node-Based Architecture : Python SDK : LLM Integration (OpenAI, Anthropic, Google) : Communication Protocol
|
||||
Coding Agent : Goal Creation Session : Worker Agent Creation : MCP Tools Integration
|
||||
Worker Agent : Human-in-the-Loop : Callback Handlers : Intervention Points : Streaming Interface
|
||||
Tools : File Use : Memory (STM/LTM) : Web Search : Web Scraper : Audit Trail
|
||||
Core : Eval System : Pydantic Validation : Docker Deployment : Documentation : Sample Agents
|
||||
section Expansion
|
||||
Intelligence : Guardrails : Streaming Mode : Semantic Search
|
||||
Platform : JavaScript SDK : Custom Tool Integrator : Credential Store
|
||||
Deployment : Self-Hosted : Cloud Services : CI/CD Pipeline
|
||||
Templates : Sales Agent : Marketing Agent : Analytics Agent : Training Agent : Smart Form Agent
|
||||
```
|
||||
|
||||
## 社区与支持
|
||||
|
||||
我们使用 [Discord](https://discord.com/invite/MXE49hrKDk) 进行支持、功能请求和社区讨论。
|
||||
|
||||
- Discord - [加入我们的社区](https://discord.com/invite/MXE49hrKDk)
|
||||
- Twitter/X - [@adenhq](https://x.com/aden_hq)
|
||||
- LinkedIn - [公司主页](https://www.linkedin.com/company/teamaden/)
|
||||
|
||||
## 贡献
|
||||
|
||||
我们欢迎贡献!请参阅 [CONTRIBUTING.md](CONTRIBUTING.md) 了解指南。
|
||||
|
||||
1. Fork 仓库
|
||||
2. 创建功能分支 (`git checkout -b feature/amazing-feature`)
|
||||
3. 提交更改 (`git commit -m 'Add amazing feature'`)
|
||||
4. 推送到分支 (`git push origin feature/amazing-feature`)
|
||||
5. 创建 Pull Request
|
||||
|
||||
## 加入我们的团队
|
||||
|
||||
**我们正在招聘!** 加入我们的工程、研究和市场推广团队。
|
||||
|
||||
[查看开放职位](https://jobs.adenhq.com/a8cec478-cdbc-473c-bbd4-f4b7027ec193/applicant)
|
||||
|
||||
## 安全
|
||||
|
||||
有关安全问题,请参阅 [SECURITY.md](SECURITY.md)。
|
||||
|
||||
## 许可证
|
||||
|
||||
本项目采用 Apache License 2.0 许可证 - 详情请参阅 [LICENSE](LICENSE) 文件。
|
||||
|
||||
## 常见问题 (FAQ)
|
||||
|
||||
> **注意:** 完整的常见问题解答,请参阅英文版 [README.md](README.md)。
|
||||
|
||||
**问:Aden 是否依赖 LangChain 或其他智能体框架?**
|
||||
|
||||
不。Aden 从头开始构建,不依赖 LangChain、CrewAI 或其他智能体框架。该框架设计精简灵活,动态生成智能体图而非依赖预定义组件。
|
||||
|
||||
**问:Aden 支持哪些 LLM 提供商?**
|
||||
|
||||
Aden 通过 LiteLLM 集成支持 100 多个 LLM 提供商,包括 OpenAI(GPT-4、GPT-4o)、Anthropic(Claude 模型)、Google Gemini、Mistral、Groq 等。只需设置适当的 API 密钥环境变量并指定模型名称即可。
|
||||
|
||||
**问:Aden 是开源的吗?**
|
||||
|
||||
是的,Aden 在 Apache License 2.0 下完全开源。我们积极鼓励社区贡献和协作。
|
||||
|
||||
**问:Aden 与其他智能体框架有何不同?**
|
||||
|
||||
Aden 使用编码智能体从自然语言目标生成整个智能体系统——您无需硬编码工作流或手动定义图。当智能体失败时,框架会自动捕获故障数据、进化智能体图并重新部署。这种自我改进循环是 Aden 独有的。
|
||||
|
||||
**问:Aden 支持人机协作工作流吗?**
|
||||
|
||||
是的,Aden 通过干预节点完全支持人机协作工作流,这些节点会暂停执行以等待人工输入。包括可配置的超时和升级策略,实现人类专家与 AI 智能体的无缝协作。
|
||||
|
||||
---
|
||||
|
||||
<p align="center">
|
||||
用 🔥 热情打造于旧金山
|
||||
</p>
|
||||
+1
-1
@@ -126,7 +126,7 @@ timeline
|
||||
- [ ] Docker container standardization
|
||||
- [ ] Headless backend execution
|
||||
- [ ] Exposed API for frontend attachment
|
||||
- [ ] Local monitoring & observability (from hive repo)
|
||||
- [ ] Local monitoring & observability
|
||||
- [ ] Basic lifecycle APIs (Start, Stop, Pause, Resume)
|
||||
|
||||
### Deployment (Cloud)
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
# File Read Tool
|
||||
|
||||
Read contents of local files with encoding support.
|
||||
|
||||
## Description
|
||||
|
||||
Use for reading configs, data files, source code, logs, or any text file. Returns file content along with path, name, size, and encoding metadata.
|
||||
|
||||
## Arguments
|
||||
|
||||
| Argument | Type | Required | Default | Description |
|
||||
|----------|------|----------|---------|-------------|
|
||||
| `file_path` | str | Yes | - | Path to the file to read (absolute or relative) |
|
||||
| `encoding` | str | No | `utf-8` | File encoding (utf-8, latin-1, etc.) |
|
||||
| `max_size` | int | No | `10000000` | Maximum file size to read in bytes (default 10MB) |
|
||||
|
||||
## Environment Variables
|
||||
|
||||
This tool does not require any environment variables.
|
||||
|
||||
## Error Handling
|
||||
|
||||
Returns error dicts for common issues:
|
||||
- `File not found: <path>` - File does not exist
|
||||
- `Not a file: <path>` - Path points to a directory
|
||||
- `File too large: <size> bytes (max: <max_size>)` - File exceeds max_size limit
|
||||
- `Failed to decode file with encoding '<encoding>'` - Wrong encoding specified
|
||||
- `Permission denied: <path>` - No read access to file
|
||||
@@ -1,4 +0,0 @@
|
||||
"""File Read Tool - Read contents of local files."""
|
||||
from .file_read_tool import register_tools
|
||||
|
||||
__all__ = ["register_tools"]
|
||||
@@ -1,75 +0,0 @@
|
||||
"""
|
||||
File Read Tool - Read contents of local files.
|
||||
|
||||
Supports reading text files with various encodings.
|
||||
Returns file content along with metadata.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from fastmcp import FastMCP
|
||||
|
||||
|
||||
def register_tools(mcp: FastMCP) -> None:
|
||||
"""Register file read tools with the MCP server."""
|
||||
|
||||
@mcp.tool()
|
||||
def file_read(
|
||||
file_path: str,
|
||||
encoding: str = "utf-8",
|
||||
max_size: int = 10_000_000,
|
||||
) -> dict:
|
||||
"""
|
||||
Read the contents of a local file.
|
||||
|
||||
Use for reading configs, data files, source code, logs, or any text file.
|
||||
Returns file content along with path, name, size, and encoding.
|
||||
|
||||
Args:
|
||||
file_path: Path to the file to read (absolute or relative)
|
||||
encoding: File encoding (utf-8, latin-1, etc.)
|
||||
max_size: Maximum file size to read in bytes (default 10MB)
|
||||
|
||||
Returns:
|
||||
Dict with file content and metadata, or error dict
|
||||
"""
|
||||
try:
|
||||
path = Path(file_path).resolve()
|
||||
|
||||
# Check if file exists
|
||||
if not path.exists():
|
||||
return {"error": f"File not found: {file_path}"}
|
||||
|
||||
# Check if it's a file (not directory)
|
||||
if not path.is_file():
|
||||
return {"error": f"Not a file: {file_path}"}
|
||||
|
||||
# Check file size
|
||||
file_size = path.stat().st_size
|
||||
if max_size > 0 and file_size > max_size:
|
||||
return {
|
||||
"error": f"File too large: {file_size} bytes (max: {max_size})",
|
||||
"file_size": file_size,
|
||||
}
|
||||
|
||||
# Read the file
|
||||
content = path.read_text(encoding=encoding)
|
||||
|
||||
return {
|
||||
"path": str(path),
|
||||
"name": path.name,
|
||||
"content": content,
|
||||
"size": len(content),
|
||||
"encoding": encoding,
|
||||
}
|
||||
|
||||
except UnicodeDecodeError as e:
|
||||
return {
|
||||
"error": f"Failed to decode file with encoding '{encoding}': {str(e)}",
|
||||
"suggestion": "Try a different encoding like 'latin-1' or 'cp1252'",
|
||||
}
|
||||
except PermissionError:
|
||||
return {"error": f"Permission denied: {file_path}"}
|
||||
except Exception as e:
|
||||
return {"error": f"Failed to read file: {str(e)}"}
|
||||
@@ -1,29 +0,0 @@
|
||||
# File Write Tool
|
||||
|
||||
Write content to local files with encoding support.
|
||||
|
||||
## Description
|
||||
|
||||
Can create new files or overwrite/append to existing ones. Use for saving data, creating configs, writing reports, or exporting results. Optionally creates parent directories if they don't exist.
|
||||
|
||||
## Arguments
|
||||
|
||||
| Argument | Type | Required | Default | Description |
|
||||
|----------|------|----------|---------|-------------|
|
||||
| `file_path` | str | Yes | - | Path to the file to write (absolute or relative) |
|
||||
| `content` | str | Yes | - | Content to write to the file |
|
||||
| `encoding` | str | No | `utf-8` | File encoding (utf-8, latin-1, etc.) |
|
||||
| `mode` | str | No | `write` | Write mode - 'write' (overwrite) or 'append' |
|
||||
| `create_dirs` | bool | No | `True` | Create parent directories if they don't exist |
|
||||
|
||||
## Environment Variables
|
||||
|
||||
This tool does not require any environment variables.
|
||||
|
||||
## Error Handling
|
||||
|
||||
Returns error dicts for common issues:
|
||||
- `Parent directory does not exist: <path>` - Parent dir missing and create_dirs=False
|
||||
- `Invalid mode: <mode>. Use 'write' or 'append'.` - Invalid mode specified
|
||||
- `Permission denied: <path>` - No write access to file/directory
|
||||
- `OS error writing file: <error>` - Filesystem error
|
||||
@@ -1,4 +0,0 @@
|
||||
"""File Write Tool - Create or update local files."""
|
||||
from .file_write_tool import register_tools
|
||||
|
||||
__all__ = ["register_tools"]
|
||||
@@ -1,83 +0,0 @@
|
||||
"""
|
||||
File Write Tool - Create or update local files.
|
||||
|
||||
Supports writing text files with various encodings.
|
||||
Can create directories if they don't exist.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from fastmcp import FastMCP
|
||||
|
||||
|
||||
def register_tools(mcp: FastMCP) -> None:
|
||||
"""Register file write tools with the MCP server."""
|
||||
|
||||
@mcp.tool()
|
||||
def file_write(
|
||||
file_path: str,
|
||||
content: str,
|
||||
encoding: str = "utf-8",
|
||||
mode: str = "write",
|
||||
create_dirs: bool = True,
|
||||
) -> dict:
|
||||
"""
|
||||
Write content to a local file.
|
||||
|
||||
Can create new files or overwrite/append to existing ones.
|
||||
Use for saving data, creating configs, writing reports, or exporting results.
|
||||
|
||||
Args:
|
||||
file_path: Path to the file to write (absolute or relative)
|
||||
content: Content to write to the file
|
||||
encoding: File encoding (utf-8, latin-1, etc.)
|
||||
mode: Write mode - 'write' (overwrite) or 'append'
|
||||
create_dirs: Create parent directories if they don't exist
|
||||
|
||||
Returns:
|
||||
Dict with write result or error dict
|
||||
"""
|
||||
try:
|
||||
path = Path(file_path).resolve()
|
||||
|
||||
# Create parent directories if requested
|
||||
if create_dirs:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
elif not path.parent.exists():
|
||||
return {"error": f"Parent directory does not exist: {path.parent}"}
|
||||
|
||||
# Determine write mode
|
||||
if mode == "append":
|
||||
write_mode = "a"
|
||||
elif mode == "write":
|
||||
write_mode = "w"
|
||||
else:
|
||||
return {"error": f"Invalid mode: {mode}. Use 'write' or 'append'."}
|
||||
|
||||
# Check if we're overwriting
|
||||
existed = path.exists()
|
||||
previous_size = path.stat().st_size if existed else 0
|
||||
|
||||
# Write the file
|
||||
with open(path, write_mode, encoding=encoding) as f:
|
||||
f.write(content)
|
||||
|
||||
new_size = path.stat().st_size
|
||||
|
||||
return {
|
||||
"path": str(path),
|
||||
"name": path.name,
|
||||
"bytes_written": len(content.encode(encoding)),
|
||||
"total_size": new_size,
|
||||
"mode": mode,
|
||||
"created": not existed,
|
||||
"previous_size": previous_size if existed else None,
|
||||
}
|
||||
|
||||
except PermissionError:
|
||||
return {"error": f"Permission denied: {file_path}"}
|
||||
except OSError as e:
|
||||
return {"error": f"OS error writing file: {str(e)}"}
|
||||
except Exception as e:
|
||||
return {"error": f"Failed to write file: {str(e)}"}
|
||||
@@ -1,96 +0,0 @@
|
||||
"""Tests for file_read tool (FastMCP)."""
|
||||
import pytest
|
||||
from pathlib import Path
|
||||
|
||||
from fastmcp import FastMCP
|
||||
from aden_tools.tools.file_read_tool import register_tools
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def file_read_fn(mcp: FastMCP):
|
||||
"""Register and return the file_read tool function."""
|
||||
register_tools(mcp)
|
||||
# Access the registered tool's function directly
|
||||
return mcp._tool_manager._tools["file_read"].fn
|
||||
|
||||
|
||||
class TestFileReadTool:
|
||||
"""Tests for file_read tool."""
|
||||
|
||||
def test_read_existing_file(self, file_read_fn, sample_text_file: Path):
|
||||
"""Reading an existing file returns content and metadata."""
|
||||
result = file_read_fn(file_path=str(sample_text_file))
|
||||
|
||||
assert "error" not in result
|
||||
assert result["content"] == "Hello, World!\nLine 2\nLine 3"
|
||||
assert result["name"] == "test.txt"
|
||||
assert result["encoding"] == "utf-8"
|
||||
assert "size" in result
|
||||
|
||||
def test_read_file_not_found(self, file_read_fn, tmp_path: Path):
|
||||
"""Reading a non-existent file returns an error dict."""
|
||||
missing_file = tmp_path / "does_not_exist.txt"
|
||||
|
||||
result = file_read_fn(file_path=str(missing_file))
|
||||
|
||||
assert "error" in result
|
||||
assert "not found" in result["error"].lower()
|
||||
|
||||
def test_read_directory_returns_error(self, file_read_fn, tmp_path: Path):
|
||||
"""Reading a directory (not a file) returns an error."""
|
||||
result = file_read_fn(file_path=str(tmp_path))
|
||||
|
||||
assert "error" in result
|
||||
assert "not a file" in result["error"].lower()
|
||||
|
||||
def test_read_file_too_large(self, file_read_fn, tmp_path: Path):
|
||||
"""Reading a file exceeding max_size returns an error."""
|
||||
large_file = tmp_path / "large.txt"
|
||||
large_file.write_text("x" * 1000)
|
||||
|
||||
result = file_read_fn(file_path=str(large_file), max_size=100)
|
||||
|
||||
assert "error" in result
|
||||
assert "too large" in result["error"].lower()
|
||||
assert "file_size" in result
|
||||
|
||||
def test_read_with_no_size_limit(self, file_read_fn, tmp_path: Path):
|
||||
"""Reading with max_size=0 allows any file size."""
|
||||
large_file = tmp_path / "large.txt"
|
||||
content = "x" * 100_000
|
||||
large_file.write_text(content)
|
||||
|
||||
# max_size=0 means no limit in the implementation
|
||||
result = file_read_fn(file_path=str(large_file), max_size=0)
|
||||
|
||||
assert "error" not in result
|
||||
assert result["content"] == content
|
||||
|
||||
def test_read_with_different_encoding(self, file_read_fn, tmp_path: Path):
|
||||
"""Reading with a specific encoding works."""
|
||||
latin_file = tmp_path / "latin.txt"
|
||||
# Write bytes directly with latin-1 encoding
|
||||
latin_file.write_bytes("café".encode("latin-1"))
|
||||
|
||||
result = file_read_fn(file_path=str(latin_file), encoding="latin-1")
|
||||
|
||||
assert "error" not in result
|
||||
assert result["content"] == "café"
|
||||
assert result["encoding"] == "latin-1"
|
||||
|
||||
def test_read_with_wrong_encoding_returns_error(self, file_read_fn, tmp_path: Path):
|
||||
"""Reading with wrong encoding returns helpful error."""
|
||||
# Create a file with bytes that aren't valid UTF-8
|
||||
binary_file = tmp_path / "binary.txt"
|
||||
binary_file.write_bytes(b"\xff\xfe")
|
||||
|
||||
result = file_read_fn(file_path=str(binary_file), encoding="utf-8")
|
||||
|
||||
assert "error" in result
|
||||
assert "suggestion" in result
|
||||
|
||||
def test_returns_absolute_path(self, file_read_fn, sample_text_file: Path):
|
||||
"""Result includes the absolute path."""
|
||||
result = file_read_fn(file_path=str(sample_text_file))
|
||||
|
||||
assert result["path"] == str(sample_text_file.resolve())
|
||||
@@ -1,99 +0,0 @@
|
||||
"""Tests for file_write tool (FastMCP)."""
|
||||
import pytest
|
||||
from pathlib import Path
|
||||
|
||||
from fastmcp import FastMCP
|
||||
from aden_tools.tools.file_write_tool import register_tools
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def file_write_fn(mcp: FastMCP):
|
||||
"""Register and return the file_write tool function."""
|
||||
register_tools(mcp)
|
||||
return mcp._tool_manager._tools["file_write"].fn
|
||||
|
||||
|
||||
class TestFileWriteTool:
|
||||
"""Tests for file_write tool."""
|
||||
|
||||
def test_write_creates_new_file(self, file_write_fn, tmp_path: Path):
|
||||
"""Writing to a new file creates it with content."""
|
||||
new_file = tmp_path / "new.txt"
|
||||
|
||||
result = file_write_fn(file_path=str(new_file), content="Hello, World!")
|
||||
|
||||
assert "error" not in result
|
||||
assert result["created"] is True
|
||||
assert result["name"] == "new.txt"
|
||||
assert new_file.read_text() == "Hello, World!"
|
||||
|
||||
def test_write_overwrites_existing(self, file_write_fn, tmp_path: Path):
|
||||
"""Writing to existing file overwrites by default."""
|
||||
existing = tmp_path / "existing.txt"
|
||||
existing.write_text("old content")
|
||||
|
||||
result = file_write_fn(file_path=str(existing), content="new content")
|
||||
|
||||
assert "error" not in result
|
||||
assert result["created"] is False
|
||||
assert result["previous_size"] is not None
|
||||
assert existing.read_text() == "new content"
|
||||
|
||||
def test_write_appends_to_existing(self, file_write_fn, tmp_path: Path):
|
||||
"""Writing with mode='append' adds to existing content."""
|
||||
existing = tmp_path / "existing.txt"
|
||||
existing.write_text("line1\n")
|
||||
|
||||
result = file_write_fn(file_path=str(existing), content="line2\n", mode="append")
|
||||
|
||||
assert "error" not in result
|
||||
assert result["mode"] == "append"
|
||||
assert existing.read_text() == "line1\nline2\n"
|
||||
|
||||
def test_write_creates_parent_dirs(self, file_write_fn, tmp_path: Path):
|
||||
"""Writing with create_dirs=True creates missing directories."""
|
||||
deep_path = tmp_path / "nested" / "dirs" / "file.txt"
|
||||
|
||||
result = file_write_fn(file_path=str(deep_path), content="content", create_dirs=True)
|
||||
|
||||
assert "error" not in result
|
||||
assert deep_path.exists()
|
||||
assert deep_path.read_text() == "content"
|
||||
|
||||
def test_write_fails_without_parent_dir(self, file_write_fn, tmp_path: Path):
|
||||
"""Writing with create_dirs=False fails if parent doesn't exist."""
|
||||
missing_dir = tmp_path / "missing" / "file.txt"
|
||||
|
||||
result = file_write_fn(file_path=str(missing_dir), content="content", create_dirs=False)
|
||||
|
||||
assert "error" in result
|
||||
assert "parent directory" in result["error"].lower()
|
||||
|
||||
def test_write_invalid_mode(self, file_write_fn, tmp_path: Path):
|
||||
"""Writing with invalid mode returns error."""
|
||||
result = file_write_fn(
|
||||
file_path=str(tmp_path / "test.txt"),
|
||||
content="content",
|
||||
mode="invalid"
|
||||
)
|
||||
|
||||
assert "error" in result
|
||||
assert "invalid mode" in result["error"].lower()
|
||||
|
||||
def test_write_returns_bytes_written(self, file_write_fn, tmp_path: Path):
|
||||
"""Result includes accurate bytes_written count."""
|
||||
content = "Hello, World!"
|
||||
|
||||
result = file_write_fn(file_path=str(tmp_path / "test.txt"), content=content)
|
||||
|
||||
assert result["bytes_written"] == len(content.encode("utf-8"))
|
||||
|
||||
def test_write_with_encoding(self, file_write_fn, tmp_path: Path):
|
||||
"""Writing with specific encoding works."""
|
||||
file_path = tmp_path / "latin.txt"
|
||||
|
||||
result = file_write_fn(file_path=str(file_path), content="café", encoding="latin-1")
|
||||
|
||||
assert "error" not in result
|
||||
# Verify it was written with latin-1 encoding
|
||||
assert file_path.read_bytes() == "café".encode("latin-1")
|
||||
@@ -1,118 +0,0 @@
|
||||
# Hive Configuration
|
||||
# ======================
|
||||
# Copy this file to config.yaml and customize for your environment.
|
||||
# Run `npm run setup` to generate .env files from this configuration.
|
||||
#
|
||||
# For detailed documentation, see: docs/configuration.md
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Application Settings
|
||||
# -----------------------------------------------------------------------------
|
||||
app:
|
||||
# Application name (displayed in UI and logs)
|
||||
name: Hive
|
||||
|
||||
# Environment: development, production, or test
|
||||
environment: development
|
||||
|
||||
# Log level: debug, info, warn, error
|
||||
log_level: info
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Server Configuration
|
||||
# -----------------------------------------------------------------------------
|
||||
server:
|
||||
# Frontend settings
|
||||
frontend:
|
||||
# Port for the frontend application
|
||||
port: 3000
|
||||
|
||||
# Backend (Hive) settings
|
||||
backend:
|
||||
# Port for the backend API
|
||||
port: 4000
|
||||
|
||||
# Host to bind to (0.0.0.0 for all interfaces)
|
||||
host: 0.0.0.0
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# TimescaleDB Configuration (Time-series metrics storage)
|
||||
# -----------------------------------------------------------------------------
|
||||
timescaledb:
|
||||
# Connection URL for TimescaleDB
|
||||
# Format: postgresql://user:password@host:port/database
|
||||
url: postgresql://postgres:postgres@localhost:5432/aden_tsdb
|
||||
|
||||
# External port mapping (for docker-compose)
|
||||
port: 5432
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# MongoDB Configuration (Policies, pricing, control config)
|
||||
# -----------------------------------------------------------------------------
|
||||
mongodb:
|
||||
# Connection URL for MongoDB
|
||||
url: mongodb://localhost:27017
|
||||
|
||||
# Database name for main data
|
||||
database: aden
|
||||
|
||||
# Database name for ERP data
|
||||
erp_database: erp
|
||||
|
||||
# External port mapping (for docker-compose)
|
||||
port: 27017
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Redis Configuration (Caching and Socket.IO)
|
||||
# -----------------------------------------------------------------------------
|
||||
redis:
|
||||
# Connection URL for Redis
|
||||
url: redis://localhost:6379
|
||||
|
||||
# External port mapping (for docker-compose)
|
||||
port: 6379
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Authentication & Security
|
||||
# -----------------------------------------------------------------------------
|
||||
auth:
|
||||
# JWT secret key - CHANGE THIS IN PRODUCTION!
|
||||
# Generate with: openssl rand -base64 32
|
||||
jwt_secret: change-this-to-a-secure-random-string-min-32-chars
|
||||
|
||||
# JWT token expiration (e.g., 1h, 7d, 30d)
|
||||
jwt_expires_in: 7d
|
||||
|
||||
# Passphrase for additional encryption - CHANGE THIS IN PRODUCTION!
|
||||
passphrase: change-this-to-a-secure-passphrase
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# NPM Configuration
|
||||
# -----------------------------------------------------------------------------
|
||||
npm:
|
||||
# NPM token for private package access (if needed)
|
||||
token: ""
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# CORS Configuration
|
||||
# -----------------------------------------------------------------------------
|
||||
cors:
|
||||
# Allowed origin for CORS requests
|
||||
# In production, set this to your frontend URL
|
||||
origin: http://localhost:3000
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Feature Flags
|
||||
# -----------------------------------------------------------------------------
|
||||
features:
|
||||
# Enable user registration
|
||||
registration: true
|
||||
|
||||
# Enable API rate limiting
|
||||
rate_limiting: false
|
||||
|
||||
# Enable request logging
|
||||
request_logging: true
|
||||
|
||||
# Enable MCP (Model Context Protocol) server
|
||||
mcp_server: true
|
||||
+2
-2
@@ -5,10 +5,10 @@
|
||||
"args": ["-m", "framework.mcp.agent_builder_server"],
|
||||
"cwd": "/home/timothy/oss/hive/core"
|
||||
},
|
||||
"aden-tools": {
|
||||
"tools": {
|
||||
"command": "python",
|
||||
"args": ["-m", "aden_tools.mcp_server", "--stdio"],
|
||||
"cwd": "/home/timothy/oss/hive/aden-tools"
|
||||
"cwd": "/home/timothy/oss/hive/tools"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ This guide explains how to use the new MCP integration tools in the agent builde
|
||||
|
||||
The agent builder now supports registering external MCP servers as tool sources. This allows you to:
|
||||
|
||||
1. Register MCP servers (like aden-tools) during agent building
|
||||
1. Register MCP servers (like tools) during agent building
|
||||
2. Discover available tools from those servers
|
||||
3. Use those tools in your agent nodes
|
||||
4. Automatically generate `mcp_servers.json` configuration on export
|
||||
@@ -18,6 +18,7 @@ The agent builder now supports registering external MCP servers as tool sources.
|
||||
Register an MCP server as a tool source for your agent.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- `name` (string, required): Unique name for the MCP server
|
||||
- `transport` (string, required): Transport type - "stdio" or "http"
|
||||
- `command` (string): Command to run (for stdio transport)
|
||||
@@ -29,21 +30,23 @@ Register an MCP server as a tool source for your agent.
|
||||
- `description` (string): Description of the MCP server
|
||||
|
||||
**Example - STDIO:**
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "add_mcp_server",
|
||||
"arguments": {
|
||||
"name": "aden-tools",
|
||||
"name": "tools",
|
||||
"transport": "stdio",
|
||||
"command": "python",
|
||||
"args": "[\"mcp_server.py\", \"--stdio\"]",
|
||||
"cwd": "../aden-tools",
|
||||
"cwd": "../tools",
|
||||
"description": "Aden tools for web search and file operations"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Example - HTTP:**
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "add_mcp_server",
|
||||
@@ -57,15 +60,16 @@ Register an MCP server as a tool source for your agent.
|
||||
```
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"server": {
|
||||
"name": "aden-tools",
|
||||
"name": "tools",
|
||||
"transport": "stdio",
|
||||
"command": "python",
|
||||
"args": ["mcp_server.py", "--stdio"],
|
||||
"cwd": "../aden-tools",
|
||||
"cwd": "../tools",
|
||||
"description": "Aden tools..."
|
||||
},
|
||||
"tools_discovered": 6,
|
||||
@@ -78,7 +82,7 @@ Register an MCP server as a tool source for your agent.
|
||||
"example_tool"
|
||||
],
|
||||
"total_mcp_servers": 1,
|
||||
"note": "MCP server 'aden-tools' registered with 6 tools. These tools can now be used in llm_tool_use nodes."
|
||||
"note": "MCP server 'tools' registered with 6 tools. These tools can now be used in llm_tool_use nodes."
|
||||
}
|
||||
```
|
||||
|
||||
@@ -89,15 +93,16 @@ List all registered MCP servers.
|
||||
**Parameters:** None
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"mcp_servers": [
|
||||
{
|
||||
"name": "aden-tools",
|
||||
"name": "tools",
|
||||
"transport": "stdio",
|
||||
"command": "python",
|
||||
"args": ["mcp_server.py", "--stdio"],
|
||||
"cwd": "../aden-tools",
|
||||
"cwd": "../tools",
|
||||
"description": "Aden tools..."
|
||||
}
|
||||
],
|
||||
@@ -110,24 +115,27 @@ List all registered MCP servers.
|
||||
List tools available from registered MCP servers.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- `server_name` (string, optional): Name of specific server to list tools from. If omitted, lists tools from all servers.
|
||||
|
||||
**Example:**
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "list_mcp_tools",
|
||||
"arguments": {
|
||||
"server_name": "aden-tools"
|
||||
"server_name": "tools"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"tools_by_server": {
|
||||
"aden-tools": [
|
||||
"tools": [
|
||||
{
|
||||
"name": "web_search",
|
||||
"description": "Search the web for information using Brave Search API...",
|
||||
@@ -150,23 +158,26 @@ List tools available from registered MCP servers.
|
||||
Remove a registered MCP server.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- `name` (string, required): Name of the MCP server to remove
|
||||
|
||||
**Example:**
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "remove_mcp_server",
|
||||
"arguments": {
|
||||
"name": "aden-tools"
|
||||
"name": "tools"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"removed": "aden-tools",
|
||||
"removed": "tools",
|
||||
"remaining_servers": 0
|
||||
}
|
||||
```
|
||||
@@ -176,6 +187,7 @@ Remove a registered MCP server.
|
||||
Here's a complete workflow for building an agent with MCP tools:
|
||||
|
||||
### 1. Create Session
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "create_session",
|
||||
@@ -186,30 +198,33 @@ Here's a complete workflow for building an agent with MCP tools:
|
||||
```
|
||||
|
||||
### 2. Register MCP Server
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "add_mcp_server",
|
||||
"arguments": {
|
||||
"name": "aden-tools",
|
||||
"name": "tools",
|
||||
"transport": "stdio",
|
||||
"command": "python",
|
||||
"args": "[\"mcp_server.py\", \"--stdio\"]",
|
||||
"cwd": "../aden-tools"
|
||||
"cwd": "../tools"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. List Available Tools
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "list_mcp_tools",
|
||||
"arguments": {
|
||||
"server_name": "aden-tools"
|
||||
"server_name": "tools"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Set Goal
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "set_goal",
|
||||
@@ -223,6 +238,7 @@ Here's a complete workflow for building an agent with MCP tools:
|
||||
```
|
||||
|
||||
### 5. Add Node with MCP Tool
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "add_node",
|
||||
@@ -239,9 +255,10 @@ Here's a complete workflow for building an agent with MCP tools:
|
||||
}
|
||||
```
|
||||
|
||||
Note: `web_search` is now available because we registered the aden-tools MCP server!
|
||||
Note: `web_search` is now available because we registered the tools MCP server!
|
||||
|
||||
### 6. Export Agent
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "export_graph",
|
||||
@@ -250,6 +267,7 @@ Note: `web_search` is now available because we registered the aden-tools MCP ser
|
||||
```
|
||||
|
||||
The export will create:
|
||||
|
||||
- `exports/web-research-agent/agent.json` - Agent specification
|
||||
- `exports/web-research-agent/README.md` - Documentation
|
||||
- `exports/web-research-agent/mcp_servers.json` - **MCP server configuration** ✨
|
||||
@@ -262,11 +280,11 @@ When you export an agent with registered MCP servers, an `mcp_servers.json` file
|
||||
{
|
||||
"servers": [
|
||||
{
|
||||
"name": "aden-tools",
|
||||
"name": "tools",
|
||||
"transport": "stdio",
|
||||
"command": "python",
|
||||
"args": ["mcp_server.py", "--stdio"],
|
||||
"cwd": "../aden-tools",
|
||||
"cwd": "../tools",
|
||||
"description": "Aden tools for web search and file operations"
|
||||
}
|
||||
]
|
||||
@@ -288,7 +306,7 @@ runner = AgentRunner.load("exports/web-research-agent")
|
||||
# Run with input
|
||||
result = await runner.run({"query": "latest AI breakthroughs"})
|
||||
|
||||
# The web_search tool from aden-tools is automatically available!
|
||||
# The web_search tool from tools is automatically available!
|
||||
```
|
||||
|
||||
## Benefits
|
||||
@@ -301,14 +319,17 @@ result = await runner.run({"query": "latest AI breakthroughs"})
|
||||
|
||||
## Common MCP Servers
|
||||
|
||||
### aden-tools
|
||||
### tools
|
||||
|
||||
Provides:
|
||||
|
||||
- `web_search` - Brave Search API integration
|
||||
- `web_scrape` - Web page content extraction
|
||||
- `file_read` / `file_write` - File operations
|
||||
- `pdf_read` - PDF text extraction
|
||||
|
||||
### Custom MCP Servers
|
||||
|
||||
You can register any MCP server that follows the Model Context Protocol specification.
|
||||
|
||||
## Troubleshooting
|
||||
@@ -372,19 +393,21 @@ If credentials are missing, you'll receive a response like:
|
||||
|
||||
1. Get the required API key from the URL in `help_url`
|
||||
2. Add it to your environment:
|
||||
|
||||
```bash
|
||||
# Option 1: Export directly
|
||||
export BRAVE_SEARCH_API_KEY=your-key-here
|
||||
|
||||
# Option 2: Add to aden-tools/.env
|
||||
echo "BRAVE_SEARCH_API_KEY=your-key-here" >> aden-tools/.env
|
||||
# Option 2: Add to tools/.env
|
||||
echo "BRAVE_SEARCH_API_KEY=your-key-here" >> tools/.env
|
||||
```
|
||||
|
||||
3. Retry the `add_node` command
|
||||
|
||||
### Required Credentials by Tool
|
||||
|
||||
| Tool | Credential | Get Key |
|
||||
|------|------------|---------|
|
||||
| Tool | Credential | Get Key |
|
||||
| ------------ | ---------------------- | ----------------------------------------------------- |
|
||||
| `web_search` | `BRAVE_SEARCH_API_KEY` | [brave.com/search/api](https://brave.com/search/api/) |
|
||||
|
||||
Note: The MCP server itself requires `ANTHROPIC_API_KEY` at startup for LLM operations.
|
||||
|
||||
@@ -21,13 +21,13 @@ from framework.runner.runner import AgentRunner
|
||||
# Load your agent
|
||||
runner = AgentRunner.load("exports/my-agent")
|
||||
|
||||
# Register aden-tools MCP server
|
||||
# Register tools MCP server
|
||||
runner.register_mcp_server(
|
||||
name="aden-tools",
|
||||
name="tools",
|
||||
transport="stdio",
|
||||
command="python",
|
||||
args=["-m", "aden_tools.mcp_server", "--stdio"],
|
||||
cwd="/path/to/aden-tools"
|
||||
cwd="/path/to/tools"
|
||||
)
|
||||
|
||||
# Tools are now available to your agent
|
||||
@@ -42,11 +42,11 @@ Create `mcp_servers.json` in your agent folder:
|
||||
{
|
||||
"servers": [
|
||||
{
|
||||
"name": "aden-tools",
|
||||
"name": "tools",
|
||||
"transport": "stdio",
|
||||
"command": "python",
|
||||
"args": ["-m", "aden_tools.mcp_server", "--stdio"],
|
||||
"cwd": "../aden-tools"
|
||||
"cwd": "../tools"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -78,6 +78,7 @@ runner.register_mcp_server(
|
||||
```
|
||||
|
||||
**Configuration:**
|
||||
|
||||
- `command`: Executable to run (e.g., "python", "node")
|
||||
- `args`: List of command-line arguments
|
||||
- `cwd`: Working directory for the process
|
||||
@@ -99,6 +100,7 @@ runner.register_mcp_server(
|
||||
```
|
||||
|
||||
**Configuration:**
|
||||
|
||||
- `url`: Base URL of the MCP server
|
||||
- `headers`: HTTP headers to include (optional)
|
||||
|
||||
@@ -119,7 +121,7 @@ builder.add_node(
|
||||
name="Web Researcher",
|
||||
node_type="llm_tool_use",
|
||||
system_prompt="Research the topic using web_search",
|
||||
tools=["web_search"], # Tool from aden-tools MCP server
|
||||
tools=["web_search"], # Tool from tools MCP server
|
||||
input_keys=["topic"],
|
||||
output_keys=["findings"]
|
||||
)
|
||||
@@ -145,9 +147,9 @@ Tools from MCP servers can be referenced in your agent.json just like built-in t
|
||||
}
|
||||
```
|
||||
|
||||
## Available Tools from aden-tools
|
||||
## Available Tools from tools
|
||||
|
||||
When you register the `aden-tools` MCP server, the following tools become available:
|
||||
When you register the `tools` MCP server, the following tools become available:
|
||||
|
||||
- **web_search**: Search the web using Brave Search API
|
||||
- **web_scrape**: Scrape content from a URL
|
||||
@@ -163,11 +165,11 @@ Some MCP tools require environment variables. You can pass them in the configura
|
||||
|
||||
```python
|
||||
runner.register_mcp_server(
|
||||
name="aden-tools",
|
||||
name="tools",
|
||||
transport="stdio",
|
||||
command="python",
|
||||
args=["-m", "aden_tools.mcp_server", "--stdio"],
|
||||
cwd="../aden-tools",
|
||||
cwd="../tools",
|
||||
env={
|
||||
"BRAVE_SEARCH_API_KEY": os.environ["BRAVE_SEARCH_API_KEY"]
|
||||
}
|
||||
@@ -180,11 +182,11 @@ runner.register_mcp_server(
|
||||
{
|
||||
"servers": [
|
||||
{
|
||||
"name": "aden-tools",
|
||||
"name": "tools",
|
||||
"transport": "stdio",
|
||||
"command": "python",
|
||||
"args": ["-m", "aden_tools.mcp_server", "--stdio"],
|
||||
"cwd": "../aden-tools",
|
||||
"cwd": "../tools",
|
||||
"env": {
|
||||
"BRAVE_SEARCH_API_KEY": "${BRAVE_SEARCH_API_KEY}"
|
||||
}
|
||||
@@ -203,11 +205,11 @@ You can register multiple MCP servers to access different sets of tools:
|
||||
{
|
||||
"servers": [
|
||||
{
|
||||
"name": "aden-tools",
|
||||
"name": "tools",
|
||||
"transport": "stdio",
|
||||
"command": "python",
|
||||
"args": ["-m", "aden_tools.mcp_server", "--stdio"],
|
||||
"cwd": "../aden-tools"
|
||||
"cwd": "../tools"
|
||||
},
|
||||
{
|
||||
"name": "database-tools",
|
||||
@@ -243,6 +245,7 @@ runner.register_mcp_server(
|
||||
### 2. Use HTTP for Production
|
||||
|
||||
HTTP transport is better for:
|
||||
|
||||
- Containerized deployments
|
||||
- Shared tools across multiple agents
|
||||
- Remote tool execution
|
||||
@@ -330,11 +333,11 @@ async def main():
|
||||
|
||||
# Register MCP server
|
||||
runner.register_mcp_server(
|
||||
name="aden-tools",
|
||||
name="tools",
|
||||
transport="stdio",
|
||||
command="python",
|
||||
args=["-m", "aden_tools.mcp_server", "--stdio"],
|
||||
cwd="../aden-tools",
|
||||
cwd="../tools",
|
||||
env={
|
||||
"BRAVE_SEARCH_API_KEY": "your-api-key"
|
||||
}
|
||||
|
||||
@@ -21,16 +21,16 @@ async def example_1_programmatic_registration():
|
||||
# Load an existing agent
|
||||
runner = AgentRunner.load("exports/task-planner")
|
||||
|
||||
# Register aden-tools MCP server via STDIO
|
||||
# Register tools MCP server via STDIO
|
||||
num_tools = runner.register_mcp_server(
|
||||
name="aden-tools",
|
||||
name="tools",
|
||||
transport="stdio",
|
||||
command="python",
|
||||
args=["-m", "aden_tools.mcp_server", "--stdio"],
|
||||
cwd="../aden-tools",
|
||||
cwd="../tools",
|
||||
)
|
||||
|
||||
print(f"Registered {num_tools} tools from aden-tools MCP server")
|
||||
print(f"Registered {num_tools} tools from tools MCP server")
|
||||
|
||||
# List all available tools
|
||||
tools = runner._tool_registry.get_tools()
|
||||
@@ -51,14 +51,14 @@ async def example_2_http_transport():
|
||||
"""Example 2: Connect to MCP server via HTTP"""
|
||||
print("\n=== Example 2: HTTP MCP Server Connection ===\n")
|
||||
|
||||
# First, start the aden-tools MCP server in HTTP mode:
|
||||
# cd aden-tools && python mcp_server.py --port 4001
|
||||
# First, start the tools MCP server in HTTP mode:
|
||||
# cd tools && python mcp_server.py --port 4001
|
||||
|
||||
runner = AgentRunner.load("exports/task-planner")
|
||||
|
||||
# Register aden-tools via HTTP
|
||||
# Register tools via HTTP
|
||||
num_tools = runner.register_mcp_server(
|
||||
name="aden-tools-http",
|
||||
name="tools-http",
|
||||
transport="http",
|
||||
url="http://localhost:4001",
|
||||
)
|
||||
@@ -130,7 +130,7 @@ async def example_4_custom_agent_with_mcp_tools():
|
||||
description="Search the web for information",
|
||||
node_type="llm_tool_use",
|
||||
system_prompt="Search for {query} and return the top results. Use the web_search tool.",
|
||||
tools=["web_search"], # This tool comes from aden-tools MCP server
|
||||
tools=["web_search"], # This tool comes from tools MCP server
|
||||
input_keys=["query"],
|
||||
output_keys=["search_results"],
|
||||
)
|
||||
@@ -160,11 +160,11 @@ async def example_4_custom_agent_with_mcp_tools():
|
||||
# Load and register MCP server
|
||||
runner = AgentRunner.load(export_path)
|
||||
runner.register_mcp_server(
|
||||
name="aden-tools",
|
||||
name="tools",
|
||||
transport="stdio",
|
||||
command="python",
|
||||
args=["-m", "aden_tools.mcp_server", "--stdio"],
|
||||
cwd="../aden-tools",
|
||||
cwd="../tools",
|
||||
)
|
||||
|
||||
# Run the agent
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
{
|
||||
"servers": [
|
||||
{
|
||||
"name": "aden-tools",
|
||||
"name": "tools",
|
||||
"description": "Aden tools including web search, file operations, and PDF reading",
|
||||
"transport": "stdio",
|
||||
"command": "python",
|
||||
"args": ["mcp_server.py", "--stdio"],
|
||||
"cwd": "../aden-tools",
|
||||
"cwd": "../tools",
|
||||
"env": {
|
||||
"BRAVE_SEARCH_API_KEY": "${BRAVE_SEARCH_API_KEY}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "aden-tools-http",
|
||||
"name": "tools-http",
|
||||
"description": "Aden tools via HTTP (for Docker deployments)",
|
||||
"transport": "http",
|
||||
"url": "http://localhost:4001",
|
||||
|
||||
@@ -2,5 +2,6 @@
|
||||
|
||||
from framework.llm.provider import LLMProvider, LLMResponse
|
||||
from framework.llm.anthropic import AnthropicProvider
|
||||
from framework.llm.litellm import LiteLLMProvider
|
||||
|
||||
__all__ = ["LLMProvider", "LLMResponse", "AnthropicProvider"]
|
||||
__all__ = ["LLMProvider", "LLMResponse", "AnthropicProvider", "LiteLLMProvider"]
|
||||
|
||||
@@ -1,11 +1,27 @@
|
||||
"""Anthropic Claude LLM provider."""
|
||||
"""Anthropic Claude LLM provider - backward compatible wrapper around LiteLLM."""
|
||||
|
||||
import os
|
||||
from typing import Any
|
||||
|
||||
import anthropic
|
||||
from framework.llm.provider import LLMProvider, LLMResponse, Tool
|
||||
from framework.llm.litellm import LiteLLMProvider
|
||||
|
||||
from framework.llm.provider import LLMProvider, LLMResponse, Tool, ToolUse, ToolResult
|
||||
|
||||
def _get_api_key_from_credential_manager() -> str | None:
|
||||
"""Get API key from CredentialManager or environment.
|
||||
|
||||
Priority:
|
||||
1. CredentialManager (supports .env hot-reload)
|
||||
2. os.environ fallback
|
||||
"""
|
||||
try:
|
||||
from aden_tools.credentials import CredentialManager
|
||||
|
||||
creds = CredentialManager()
|
||||
if creds.is_available("anthropic"):
|
||||
return creds.get("anthropic")
|
||||
except ImportError:
|
||||
pass
|
||||
return os.environ.get("ANTHROPIC_API_KEY")
|
||||
|
||||
|
||||
def _get_api_key_from_credential_manager() -> str | None:
|
||||
@@ -30,7 +46,9 @@ class AnthropicProvider(LLMProvider):
|
||||
"""
|
||||
Anthropic Claude LLM provider.
|
||||
|
||||
Uses the Anthropic API to interact with Claude models.
|
||||
This is a backward-compatible wrapper that internally uses LiteLLMProvider.
|
||||
Existing code using AnthropicProvider will continue to work unchanged,
|
||||
while benefiting from LiteLLM's unified interface and features.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
@@ -46,6 +64,7 @@ class AnthropicProvider(LLMProvider):
|
||||
or ANTHROPIC_API_KEY env var.
|
||||
model: Model to use (default: claude-haiku-4-5-20251001)
|
||||
"""
|
||||
# Delegate to LiteLLMProvider internally.
|
||||
self.api_key = api_key or _get_api_key_from_credential_manager()
|
||||
if not self.api_key:
|
||||
raise ValueError(
|
||||
@@ -53,7 +72,17 @@ class AnthropicProvider(LLMProvider):
|
||||
)
|
||||
|
||||
self.model = model
|
||||
self.client = anthropic.Anthropic(api_key=self.api_key)
|
||||
|
||||
self._provider = LiteLLMProvider(
|
||||
model=model,
|
||||
api_key=self.api_key,
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
||||
self.model = model
|
||||
self.api_key = api_key
|
||||
|
||||
def complete(
|
||||
self,
|
||||
@@ -62,34 +91,12 @@ class AnthropicProvider(LLMProvider):
|
||||
tools: list[Tool] | None = None,
|
||||
max_tokens: int = 1024,
|
||||
) -> LLMResponse:
|
||||
"""Generate a completion from Claude."""
|
||||
kwargs: dict[str, Any] = {
|
||||
"model": self.model,
|
||||
"max_tokens": max_tokens,
|
||||
"messages": messages,
|
||||
}
|
||||
|
||||
if system:
|
||||
kwargs["system"] = system
|
||||
|
||||
if tools:
|
||||
kwargs["tools"] = [self._tool_to_dict(t) for t in tools]
|
||||
|
||||
response = self.client.messages.create(**kwargs)
|
||||
|
||||
# Extract text content
|
||||
content = ""
|
||||
for block in response.content:
|
||||
if block.type == "text":
|
||||
content += block.text
|
||||
|
||||
return LLMResponse(
|
||||
content=content,
|
||||
model=response.model,
|
||||
input_tokens=response.usage.input_tokens,
|
||||
output_tokens=response.usage.output_tokens,
|
||||
stop_reason=response.stop_reason,
|
||||
raw_response=response,
|
||||
"""Generate a completion from Claude (via LiteLLM)."""
|
||||
return self._provider.complete(
|
||||
messages=messages,
|
||||
system=system,
|
||||
tools=tools,
|
||||
max_tokens=max_tokens,
|
||||
)
|
||||
|
||||
def complete_with_tools(
|
||||
@@ -186,15 +193,3 @@ class AnthropicProvider(LLMProvider):
|
||||
stop_reason="max_iterations",
|
||||
raw_response=None,
|
||||
)
|
||||
|
||||
def _tool_to_dict(self, tool: Tool) -> dict[str, Any]:
|
||||
"""Convert Tool to Anthropic API format."""
|
||||
return {
|
||||
"name": tool.name,
|
||||
"description": tool.description,
|
||||
"input_schema": {
|
||||
"type": "object",
|
||||
"properties": tool.parameters.get("properties", {}),
|
||||
"required": tool.parameters.get("required", []),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -0,0 +1,248 @@
|
||||
"""LiteLLM provider for pluggable multi-provider LLM support.
|
||||
|
||||
LiteLLM provides a unified, OpenAI-compatible interface that supports
|
||||
multiple LLM providers including OpenAI, Anthropic, Gemini, Mistral,
|
||||
Groq, and local models.
|
||||
|
||||
See: https://docs.litellm.ai/docs/providers
|
||||
"""
|
||||
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
import litellm
|
||||
|
||||
from framework.llm.provider import LLMProvider, LLMResponse, Tool, ToolUse, ToolResult
|
||||
|
||||
|
||||
class LiteLLMProvider(LLMProvider):
|
||||
"""
|
||||
LiteLLM-based LLM provider for multi-provider support.
|
||||
|
||||
Supports any model that LiteLLM supports, including:
|
||||
- OpenAI: gpt-4o, gpt-4o-mini, gpt-4-turbo, gpt-3.5-turbo
|
||||
- Anthropic: claude-3-opus, claude-3-sonnet, claude-3-haiku
|
||||
- Google: gemini-pro, gemini-1.5-pro, gemini-1.5-flash
|
||||
- Mistral: mistral-large, mistral-medium, mistral-small
|
||||
- Groq: llama3-70b, mixtral-8x7b
|
||||
- Local: ollama/llama3, ollama/mistral
|
||||
- And many more...
|
||||
|
||||
Usage:
|
||||
# OpenAI
|
||||
provider = LiteLLMProvider(model="gpt-4o-mini")
|
||||
|
||||
# Anthropic
|
||||
provider = LiteLLMProvider(model="claude-3-haiku-20240307")
|
||||
|
||||
# Google Gemini
|
||||
provider = LiteLLMProvider(model="gemini/gemini-1.5-flash")
|
||||
|
||||
# Local Ollama
|
||||
provider = LiteLLMProvider(model="ollama/llama3")
|
||||
|
||||
# With custom API base
|
||||
provider = LiteLLMProvider(
|
||||
model="gpt-4o-mini",
|
||||
api_base="https://my-proxy.com/v1"
|
||||
)
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
model: str = "gpt-4o-mini",
|
||||
api_key: str | None = None,
|
||||
api_base: str | None = None,
|
||||
**kwargs: Any,
|
||||
):
|
||||
"""
|
||||
Initialize the LiteLLM provider.
|
||||
|
||||
Args:
|
||||
model: Model identifier (e.g., "gpt-4o-mini", "claude-3-haiku-20240307")
|
||||
LiteLLM auto-detects the provider from the model name.
|
||||
api_key: API key for the provider. If not provided, LiteLLM will
|
||||
look for the appropriate env var (OPENAI_API_KEY,
|
||||
ANTHROPIC_API_KEY, etc.)
|
||||
api_base: Custom API base URL (for proxies or local deployments)
|
||||
**kwargs: Additional arguments passed to litellm.completion()
|
||||
"""
|
||||
self.model = model
|
||||
self.api_key = api_key
|
||||
self.api_base = api_base
|
||||
self.extra_kwargs = kwargs
|
||||
|
||||
def complete(
|
||||
self,
|
||||
messages: list[dict[str, Any]],
|
||||
system: str = "",
|
||||
tools: list[Tool] | None = None,
|
||||
max_tokens: int = 1024,
|
||||
) -> LLMResponse:
|
||||
"""Generate a completion using LiteLLM."""
|
||||
# Prepare messages with system prompt
|
||||
full_messages = []
|
||||
if system:
|
||||
full_messages.append({"role": "system", "content": system})
|
||||
full_messages.extend(messages)
|
||||
|
||||
# Build kwargs
|
||||
kwargs: dict[str, Any] = {
|
||||
"model": self.model,
|
||||
"messages": full_messages,
|
||||
"max_tokens": max_tokens,
|
||||
**self.extra_kwargs,
|
||||
}
|
||||
|
||||
if self.api_key:
|
||||
kwargs["api_key"] = self.api_key
|
||||
if self.api_base:
|
||||
kwargs["api_base"] = self.api_base
|
||||
|
||||
# Add tools if provided
|
||||
if tools:
|
||||
kwargs["tools"] = [self._tool_to_openai_format(t) for t in tools]
|
||||
|
||||
# Make the call
|
||||
response = litellm.completion(**kwargs)
|
||||
|
||||
# Extract content
|
||||
content = response.choices[0].message.content or ""
|
||||
|
||||
# Get usage info
|
||||
usage = response.usage
|
||||
input_tokens = usage.prompt_tokens if usage else 0
|
||||
output_tokens = usage.completion_tokens if usage else 0
|
||||
|
||||
return LLMResponse(
|
||||
content=content,
|
||||
model=response.model or self.model,
|
||||
input_tokens=input_tokens,
|
||||
output_tokens=output_tokens,
|
||||
stop_reason=response.choices[0].finish_reason or "",
|
||||
raw_response=response,
|
||||
)
|
||||
|
||||
def complete_with_tools(
|
||||
self,
|
||||
messages: list[dict[str, Any]],
|
||||
system: str,
|
||||
tools: list[Tool],
|
||||
tool_executor: callable,
|
||||
max_iterations: int = 10,
|
||||
) -> LLMResponse:
|
||||
"""Run a tool-use loop until the LLM produces a final response."""
|
||||
# Prepare messages with system prompt
|
||||
current_messages = []
|
||||
if system:
|
||||
current_messages.append({"role": "system", "content": system})
|
||||
current_messages.extend(messages)
|
||||
|
||||
total_input_tokens = 0
|
||||
total_output_tokens = 0
|
||||
|
||||
# Convert tools to OpenAI format
|
||||
openai_tools = [self._tool_to_openai_format(t) for t in tools]
|
||||
|
||||
for _ in range(max_iterations):
|
||||
# Build kwargs
|
||||
kwargs: dict[str, Any] = {
|
||||
"model": self.model,
|
||||
"messages": current_messages,
|
||||
"max_tokens": 1024,
|
||||
"tools": openai_tools,
|
||||
**self.extra_kwargs,
|
||||
}
|
||||
|
||||
if self.api_key:
|
||||
kwargs["api_key"] = self.api_key
|
||||
if self.api_base:
|
||||
kwargs["api_base"] = self.api_base
|
||||
|
||||
response = litellm.completion(**kwargs)
|
||||
|
||||
# Track tokens
|
||||
usage = response.usage
|
||||
if usage:
|
||||
total_input_tokens += usage.prompt_tokens
|
||||
total_output_tokens += usage.completion_tokens
|
||||
|
||||
choice = response.choices[0]
|
||||
message = choice.message
|
||||
|
||||
# Check if we're done (no tool calls)
|
||||
if choice.finish_reason == "stop" or not message.tool_calls:
|
||||
return LLMResponse(
|
||||
content=message.content or "",
|
||||
model=response.model or self.model,
|
||||
input_tokens=total_input_tokens,
|
||||
output_tokens=total_output_tokens,
|
||||
stop_reason=choice.finish_reason or "stop",
|
||||
raw_response=response,
|
||||
)
|
||||
|
||||
# Process tool calls.
|
||||
# Add assistant message with tool calls.
|
||||
current_messages.append({
|
||||
"role": "assistant",
|
||||
"content": message.content,
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": tc.id,
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": tc.function.name,
|
||||
"arguments": tc.function.arguments,
|
||||
},
|
||||
}
|
||||
for tc in message.tool_calls
|
||||
],
|
||||
})
|
||||
|
||||
# Execute tools and add results.
|
||||
for tool_call in message.tool_calls:
|
||||
# Parse arguments
|
||||
try:
|
||||
args = json.loads(tool_call.function.arguments)
|
||||
except json.JSONDecodeError:
|
||||
args = {}
|
||||
|
||||
tool_use = ToolUse(
|
||||
id=tool_call.id,
|
||||
name=tool_call.function.name,
|
||||
input=args,
|
||||
)
|
||||
|
||||
result = tool_executor(tool_use)
|
||||
|
||||
# Add tool result message
|
||||
current_messages.append({
|
||||
"role": "tool",
|
||||
"tool_call_id": result.tool_use_id,
|
||||
"content": result.content,
|
||||
})
|
||||
|
||||
# Max iterations reached
|
||||
return LLMResponse(
|
||||
content="Max tool iterations reached",
|
||||
model=self.model,
|
||||
input_tokens=total_input_tokens,
|
||||
output_tokens=total_output_tokens,
|
||||
stop_reason="max_iterations",
|
||||
raw_response=None,
|
||||
)
|
||||
|
||||
def _tool_to_openai_format(self, tool: Tool) -> dict[str, Any]:
|
||||
"""Convert Tool to OpenAI function calling format."""
|
||||
return {
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": tool.name,
|
||||
"description": tool.description,
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": tool.parameters.get("properties", {}),
|
||||
"required": tool.parameters.get("required", []),
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -446,7 +446,7 @@ def _validate_tool_credentials(tools_list: list[str]) -> dict | None:
|
||||
"warnings": [
|
||||
f"⚠️ Credential validation SKIPPED: aden_tools not available ({e}). "
|
||||
"Tools may fail at runtime if credentials are missing. "
|
||||
"Add aden-tools/src to PYTHONPATH to enable validation."
|
||||
"Add tools/src to PYTHONPATH to enable validation."
|
||||
],
|
||||
}
|
||||
|
||||
@@ -1435,11 +1435,11 @@ def add_mcp_server(
|
||||
|
||||
Example for stdio:
|
||||
add_mcp_server(
|
||||
name="aden-tools",
|
||||
name="tools",
|
||||
transport="stdio",
|
||||
command="python",
|
||||
args='["mcp_server.py", "--stdio"]',
|
||||
cwd="../aden-tools"
|
||||
cwd="../tools"
|
||||
)
|
||||
|
||||
Example for http:
|
||||
|
||||
@@ -4,7 +4,6 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
@@ -71,10 +70,10 @@ class AgentOrchestrator:
|
||||
self._model = model
|
||||
self._message_log: list[AgentMessage] = []
|
||||
|
||||
# Auto-create LLM if API key available
|
||||
if self._llm is None and os.environ.get("ANTHROPIC_API_KEY"):
|
||||
from framework.llm.anthropic import AnthropicProvider
|
||||
self._llm = AnthropicProvider(model=model)
|
||||
# Auto-create LLM - LiteLLM auto-detects provider and API key from model name
|
||||
if self._llm is None:
|
||||
from framework.llm.litellm import LiteLLMProvider
|
||||
self._llm = LiteLLMProvider(model=self._model)
|
||||
|
||||
def register(
|
||||
self,
|
||||
|
||||
@@ -12,6 +12,7 @@ from framework.graph.edge import GraphSpec, EdgeSpec, EdgeCondition
|
||||
from framework.graph.node import NodeSpec
|
||||
from framework.graph.executor import GraphExecutor, ExecutionResult
|
||||
from framework.llm.provider import LLMProvider, Tool, ToolResult, ToolUse
|
||||
from framework.llm.litellm import LiteLLMProvider
|
||||
from framework.runner.tool_registry import ToolRegistry
|
||||
from framework.runtime.core import Runtime
|
||||
|
||||
@@ -183,7 +184,8 @@ class AgentRunner:
|
||||
goal: Loaded Goal object
|
||||
mock_mode: If True, use mock LLM responses
|
||||
storage_path: Path for runtime storage (defaults to temp)
|
||||
model: Anthropic model to use
|
||||
model: Model to use - any LiteLLM-compatible model name
|
||||
(e.g., "claude-sonnet-4-20250514", "gpt-4o-mini", "gemini/gemini-pro")
|
||||
"""
|
||||
self.agent_path = agent_path
|
||||
self.graph = graph
|
||||
@@ -313,16 +315,16 @@ class AgentRunner:
|
||||
Example:
|
||||
# Register STDIO MCP server
|
||||
runner.register_mcp_server(
|
||||
name="aden-tools",
|
||||
name="tools",
|
||||
transport="stdio",
|
||||
command="python",
|
||||
args=["-m", "aden_tools.mcp_server", "--stdio"],
|
||||
cwd="/path/to/aden-tools"
|
||||
cwd="/path/to/tools"
|
||||
)
|
||||
|
||||
# Register HTTP MCP server
|
||||
runner.register_mcp_server(
|
||||
name="aden-tools",
|
||||
name="tools",
|
||||
transport="http",
|
||||
url="http://localhost:4001"
|
||||
)
|
||||
|
||||
@@ -10,6 +10,7 @@ dependencies = [
|
||||
"pytest>=8.0",
|
||||
"pytest-asyncio>=0.23",
|
||||
"pytest-xdist>=3.0",
|
||||
"litellm>=1.81.0",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
pydantic>=2.0
|
||||
anthropic>=0.40.0
|
||||
httpx>=0.27.0
|
||||
litellm>=1.81.0
|
||||
|
||||
# MCP server dependencies
|
||||
mcp
|
||||
|
||||
@@ -0,0 +1,332 @@
|
||||
"""Tests for LiteLLM provider.
|
||||
|
||||
Run with:
|
||||
cd core
|
||||
pip install litellm pytest
|
||||
pytest tests/test_litellm_provider.py -v
|
||||
|
||||
For live tests (requires API keys):
|
||||
OPENAI_API_KEY=sk-... pytest tests/test_litellm_provider.py -v -m live
|
||||
"""
|
||||
|
||||
import os
|
||||
import pytest
|
||||
from unittest.mock import Mock, patch, MagicMock
|
||||
|
||||
from framework.llm.litellm import LiteLLMProvider
|
||||
from framework.llm.anthropic import AnthropicProvider
|
||||
from framework.llm.provider import LLMProvider, Tool, ToolUse, ToolResult
|
||||
|
||||
|
||||
class TestLiteLLMProviderInit:
|
||||
"""Test LiteLLMProvider initialization."""
|
||||
|
||||
def test_init_with_defaults(self):
|
||||
"""Test initialization with default parameters."""
|
||||
with patch.dict(os.environ, {"OPENAI_API_KEY": "test-key"}):
|
||||
provider = LiteLLMProvider()
|
||||
assert provider.model == "gpt-4o-mini"
|
||||
assert provider.api_key is None
|
||||
assert provider.api_base is None
|
||||
|
||||
def test_init_with_custom_model(self):
|
||||
"""Test initialization with custom model."""
|
||||
with patch.dict(os.environ, {"ANTHROPIC_API_KEY": "test-key"}):
|
||||
provider = LiteLLMProvider(model="claude-3-haiku-20240307")
|
||||
assert provider.model == "claude-3-haiku-20240307"
|
||||
|
||||
def test_init_with_api_key(self):
|
||||
"""Test initialization with explicit API key."""
|
||||
provider = LiteLLMProvider(model="gpt-4o-mini", api_key="my-api-key")
|
||||
assert provider.api_key == "my-api-key"
|
||||
|
||||
def test_init_with_api_base(self):
|
||||
"""Test initialization with custom API base."""
|
||||
provider = LiteLLMProvider(
|
||||
model="gpt-4o-mini",
|
||||
api_key="my-key",
|
||||
api_base="https://my-proxy.com/v1"
|
||||
)
|
||||
assert provider.api_base == "https://my-proxy.com/v1"
|
||||
|
||||
def test_init_ollama_no_key_needed(self):
|
||||
"""Test that Ollama models don't require API key."""
|
||||
with patch.dict(os.environ, {}, clear=True):
|
||||
# Should not raise.
|
||||
provider = LiteLLMProvider(model="ollama/llama3")
|
||||
assert provider.model == "ollama/llama3"
|
||||
|
||||
|
||||
class TestLiteLLMProviderComplete:
|
||||
"""Test LiteLLMProvider.complete() method."""
|
||||
|
||||
@patch("litellm.completion")
|
||||
def test_complete_basic(self, mock_completion):
|
||||
"""Test basic completion call."""
|
||||
# Mock response
|
||||
mock_response = MagicMock()
|
||||
mock_response.choices = [MagicMock()]
|
||||
mock_response.choices[0].message.content = "Hello! I'm an AI assistant."
|
||||
mock_response.choices[0].finish_reason = "stop"
|
||||
mock_response.model = "gpt-4o-mini"
|
||||
mock_response.usage.prompt_tokens = 10
|
||||
mock_response.usage.completion_tokens = 20
|
||||
mock_completion.return_value = mock_response
|
||||
|
||||
provider = LiteLLMProvider(model="gpt-4o-mini", api_key="test-key")
|
||||
result = provider.complete(
|
||||
messages=[{"role": "user", "content": "Hello"}]
|
||||
)
|
||||
|
||||
assert result.content == "Hello! I'm an AI assistant."
|
||||
assert result.model == "gpt-4o-mini"
|
||||
assert result.input_tokens == 10
|
||||
assert result.output_tokens == 20
|
||||
assert result.stop_reason == "stop"
|
||||
|
||||
# Verify litellm.completion was called correctly
|
||||
mock_completion.assert_called_once()
|
||||
call_kwargs = mock_completion.call_args[1]
|
||||
assert call_kwargs["model"] == "gpt-4o-mini"
|
||||
assert call_kwargs["api_key"] == "test-key"
|
||||
|
||||
@patch("litellm.completion")
|
||||
def test_complete_with_system_prompt(self, mock_completion):
|
||||
"""Test completion with system prompt."""
|
||||
mock_response = MagicMock()
|
||||
mock_response.choices = [MagicMock()]
|
||||
mock_response.choices[0].message.content = "Response"
|
||||
mock_response.choices[0].finish_reason = "stop"
|
||||
mock_response.model = "gpt-4o-mini"
|
||||
mock_response.usage.prompt_tokens = 15
|
||||
mock_response.usage.completion_tokens = 5
|
||||
mock_completion.return_value = mock_response
|
||||
|
||||
provider = LiteLLMProvider(model="gpt-4o-mini", api_key="test-key")
|
||||
provider.complete(
|
||||
messages=[{"role": "user", "content": "Hello"}],
|
||||
system="You are a helpful assistant."
|
||||
)
|
||||
|
||||
call_kwargs = mock_completion.call_args[1]
|
||||
messages = call_kwargs["messages"]
|
||||
assert messages[0]["role"] == "system"
|
||||
assert messages[0]["content"] == "You are a helpful assistant."
|
||||
|
||||
@patch("litellm.completion")
|
||||
def test_complete_with_tools(self, mock_completion):
|
||||
"""Test completion with tools."""
|
||||
mock_response = MagicMock()
|
||||
mock_response.choices = [MagicMock()]
|
||||
mock_response.choices[0].message.content = "Response"
|
||||
mock_response.choices[0].finish_reason = "stop"
|
||||
mock_response.model = "gpt-4o-mini"
|
||||
mock_response.usage.prompt_tokens = 20
|
||||
mock_response.usage.completion_tokens = 10
|
||||
mock_completion.return_value = mock_response
|
||||
|
||||
provider = LiteLLMProvider(model="gpt-4o-mini", api_key="test-key")
|
||||
|
||||
tools = [
|
||||
Tool(
|
||||
name="get_weather",
|
||||
description="Get the weather for a location",
|
||||
parameters={
|
||||
"properties": {
|
||||
"location": {"type": "string", "description": "City name"}
|
||||
},
|
||||
"required": ["location"]
|
||||
}
|
||||
)
|
||||
]
|
||||
|
||||
provider.complete(
|
||||
messages=[{"role": "user", "content": "What's the weather?"}],
|
||||
tools=tools
|
||||
)
|
||||
|
||||
call_kwargs = mock_completion.call_args[1]
|
||||
assert "tools" in call_kwargs
|
||||
assert call_kwargs["tools"][0]["type"] == "function"
|
||||
assert call_kwargs["tools"][0]["function"]["name"] == "get_weather"
|
||||
|
||||
|
||||
class TestLiteLLMProviderToolUse:
|
||||
"""Test LiteLLMProvider.complete_with_tools() method."""
|
||||
|
||||
@patch("litellm.completion")
|
||||
def test_complete_with_tools_single_iteration(self, mock_completion):
|
||||
"""Test tool use with single iteration."""
|
||||
# First response: tool call
|
||||
tool_call_response = MagicMock()
|
||||
tool_call_response.choices = [MagicMock()]
|
||||
tool_call_response.choices[0].message.content = None
|
||||
tool_call_response.choices[0].message.tool_calls = [MagicMock()]
|
||||
tool_call_response.choices[0].message.tool_calls[0].id = "call_123"
|
||||
tool_call_response.choices[0].message.tool_calls[0].function.name = "get_weather"
|
||||
tool_call_response.choices[0].message.tool_calls[0].function.arguments = '{"location": "London"}'
|
||||
tool_call_response.choices[0].finish_reason = "tool_calls"
|
||||
tool_call_response.model = "gpt-4o-mini"
|
||||
tool_call_response.usage.prompt_tokens = 20
|
||||
tool_call_response.usage.completion_tokens = 15
|
||||
|
||||
# Second response: final answer
|
||||
final_response = MagicMock()
|
||||
final_response.choices = [MagicMock()]
|
||||
final_response.choices[0].message.content = "The weather in London is sunny."
|
||||
final_response.choices[0].message.tool_calls = None
|
||||
final_response.choices[0].finish_reason = "stop"
|
||||
final_response.model = "gpt-4o-mini"
|
||||
final_response.usage.prompt_tokens = 30
|
||||
final_response.usage.completion_tokens = 10
|
||||
|
||||
mock_completion.side_effect = [tool_call_response, final_response]
|
||||
|
||||
provider = LiteLLMProvider(model="gpt-4o-mini", api_key="test-key")
|
||||
|
||||
tools = [
|
||||
Tool(
|
||||
name="get_weather",
|
||||
description="Get the weather",
|
||||
parameters={"properties": {"location": {"type": "string"}}, "required": ["location"]}
|
||||
)
|
||||
]
|
||||
|
||||
def tool_executor(tool_use: ToolUse) -> ToolResult:
|
||||
return ToolResult(
|
||||
tool_use_id=tool_use.id,
|
||||
content="Sunny, 22C",
|
||||
is_error=False
|
||||
)
|
||||
|
||||
result = provider.complete_with_tools(
|
||||
messages=[{"role": "user", "content": "What's the weather in London?"}],
|
||||
system="You are a weather assistant.",
|
||||
tools=tools,
|
||||
tool_executor=tool_executor
|
||||
)
|
||||
|
||||
assert result.content == "The weather in London is sunny."
|
||||
assert result.input_tokens == 50 # 20 + 30
|
||||
assert result.output_tokens == 25 # 15 + 10
|
||||
assert mock_completion.call_count == 2
|
||||
|
||||
|
||||
class TestToolConversion:
|
||||
"""Test tool format conversion."""
|
||||
|
||||
def test_tool_to_openai_format(self):
|
||||
"""Test converting Tool to OpenAI format."""
|
||||
provider = LiteLLMProvider(model="gpt-4o-mini", api_key="test-key")
|
||||
|
||||
tool = Tool(
|
||||
name="search",
|
||||
description="Search the web",
|
||||
parameters={
|
||||
"properties": {
|
||||
"query": {"type": "string", "description": "Search query"}
|
||||
},
|
||||
"required": ["query"]
|
||||
}
|
||||
)
|
||||
|
||||
result = provider._tool_to_openai_format(tool)
|
||||
|
||||
assert result["type"] == "function"
|
||||
assert result["function"]["name"] == "search"
|
||||
assert result["function"]["description"] == "Search the web"
|
||||
assert result["function"]["parameters"]["properties"]["query"]["type"] == "string"
|
||||
assert result["function"]["parameters"]["required"] == ["query"]
|
||||
|
||||
|
||||
class TestAnthropicProviderBackwardCompatibility:
|
||||
"""Test AnthropicProvider backward compatibility with LiteLLM backend."""
|
||||
|
||||
def test_anthropic_provider_is_llm_provider(self):
|
||||
"""Test that AnthropicProvider implements LLMProvider interface."""
|
||||
provider = AnthropicProvider(api_key="test-key")
|
||||
assert isinstance(provider, LLMProvider)
|
||||
|
||||
def test_anthropic_provider_init_defaults(self):
|
||||
"""Test AnthropicProvider initialization with defaults."""
|
||||
provider = AnthropicProvider(api_key="test-key")
|
||||
assert provider.model == "claude-sonnet-4-20250514"
|
||||
assert provider.api_key == "test-key"
|
||||
|
||||
def test_anthropic_provider_init_custom_model(self):
|
||||
"""Test AnthropicProvider initialization with custom model."""
|
||||
provider = AnthropicProvider(api_key="test-key", model="claude-3-haiku-20240307")
|
||||
assert provider.model == "claude-3-haiku-20240307"
|
||||
|
||||
def test_anthropic_provider_uses_litellm_internally(self):
|
||||
"""Test that AnthropicProvider delegates to LiteLLMProvider."""
|
||||
provider = AnthropicProvider(api_key="test-key", model="claude-3-haiku-20240307")
|
||||
assert isinstance(provider._provider, LiteLLMProvider)
|
||||
assert provider._provider.model == "claude-3-haiku-20240307"
|
||||
assert provider._provider.api_key == "test-key"
|
||||
|
||||
@patch("litellm.completion")
|
||||
def test_anthropic_provider_complete(self, mock_completion):
|
||||
"""Test AnthropicProvider.complete() delegates to LiteLLM."""
|
||||
mock_response = MagicMock()
|
||||
mock_response.choices = [MagicMock()]
|
||||
mock_response.choices[0].message.content = "Hello from Claude!"
|
||||
mock_response.choices[0].finish_reason = "stop"
|
||||
mock_response.model = "claude-3-haiku-20240307"
|
||||
mock_response.usage.prompt_tokens = 10
|
||||
mock_response.usage.completion_tokens = 5
|
||||
mock_completion.return_value = mock_response
|
||||
|
||||
provider = AnthropicProvider(api_key="test-key", model="claude-3-haiku-20240307")
|
||||
result = provider.complete(
|
||||
messages=[{"role": "user", "content": "Hello"}],
|
||||
system="You are helpful.",
|
||||
max_tokens=100
|
||||
)
|
||||
|
||||
assert result.content == "Hello from Claude!"
|
||||
assert result.model == "claude-3-haiku-20240307"
|
||||
assert result.input_tokens == 10
|
||||
assert result.output_tokens == 5
|
||||
|
||||
mock_completion.assert_called_once()
|
||||
call_kwargs = mock_completion.call_args[1]
|
||||
assert call_kwargs["model"] == "claude-3-haiku-20240307"
|
||||
assert call_kwargs["api_key"] == "test-key"
|
||||
|
||||
@patch("litellm.completion")
|
||||
def test_anthropic_provider_complete_with_tools(self, mock_completion):
|
||||
"""Test AnthropicProvider.complete_with_tools() delegates to LiteLLM."""
|
||||
# Mock a simple response (no tool calls)
|
||||
mock_response = MagicMock()
|
||||
mock_response.choices = [MagicMock()]
|
||||
mock_response.choices[0].message.content = "The time is 3:00 PM."
|
||||
mock_response.choices[0].message.tool_calls = None
|
||||
mock_response.choices[0].finish_reason = "stop"
|
||||
mock_response.model = "claude-3-haiku-20240307"
|
||||
mock_response.usage.prompt_tokens = 20
|
||||
mock_response.usage.completion_tokens = 10
|
||||
mock_completion.return_value = mock_response
|
||||
|
||||
provider = AnthropicProvider(api_key="test-key", model="claude-3-haiku-20240307")
|
||||
|
||||
tools = [
|
||||
Tool(
|
||||
name="get_time",
|
||||
description="Get current time",
|
||||
parameters={"properties": {}, "required": []}
|
||||
)
|
||||
]
|
||||
|
||||
def tool_executor(tool_use: ToolUse) -> ToolResult:
|
||||
return ToolResult(tool_use_id=tool_use.id, content="3:00 PM", is_error=False)
|
||||
|
||||
result = provider.complete_with_tools(
|
||||
messages=[{"role": "user", "content": "What time is it?"}],
|
||||
system="You are a time assistant.",
|
||||
tools=tools,
|
||||
tool_executor=tool_executor
|
||||
)
|
||||
|
||||
assert result.content == "The time is 3:00 PM."
|
||||
mock_completion.assert_called_once()
|
||||
@@ -0,0 +1,82 @@
|
||||
"""Tests for AgentOrchestrator LiteLLM integration.
|
||||
|
||||
Run with:
|
||||
cd core
|
||||
pytest tests/test_orchestrator.py -v
|
||||
"""
|
||||
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
from framework.llm.provider import LLMProvider
|
||||
from framework.llm.litellm import LiteLLMProvider
|
||||
from framework.runner.orchestrator import AgentOrchestrator
|
||||
|
||||
|
||||
class TestOrchestratorLLMInitialization:
|
||||
"""Test AgentOrchestrator LLM provider initialization."""
|
||||
|
||||
def test_auto_creates_litellm_provider_when_no_llm_passed(self):
|
||||
"""Test that LiteLLMProvider is auto-created when no llm is passed."""
|
||||
with patch.object(LiteLLMProvider, '__init__', return_value=None) as mock_init:
|
||||
orchestrator = AgentOrchestrator()
|
||||
|
||||
mock_init.assert_called_once_with(model="claude-sonnet-4-20250514")
|
||||
assert orchestrator._llm is not None
|
||||
|
||||
def test_uses_custom_model_parameter(self):
|
||||
"""Test that custom model parameter is passed to LiteLLMProvider."""
|
||||
with patch.object(LiteLLMProvider, '__init__', return_value=None) as mock_init:
|
||||
orchestrator = AgentOrchestrator(model="gpt-4o")
|
||||
|
||||
mock_init.assert_called_once_with(model="gpt-4o")
|
||||
|
||||
def test_supports_openai_model_names(self):
|
||||
"""Test that OpenAI model names are supported."""
|
||||
with patch.object(LiteLLMProvider, '__init__', return_value=None) as mock_init:
|
||||
orchestrator = AgentOrchestrator(model="gpt-4o-mini")
|
||||
|
||||
mock_init.assert_called_once_with(model="gpt-4o-mini")
|
||||
assert orchestrator._model == "gpt-4o-mini"
|
||||
|
||||
def test_supports_anthropic_model_names(self):
|
||||
"""Test that Anthropic model names are supported."""
|
||||
with patch.object(LiteLLMProvider, '__init__', return_value=None) as mock_init:
|
||||
orchestrator = AgentOrchestrator(model="claude-3-haiku-20240307")
|
||||
|
||||
mock_init.assert_called_once_with(model="claude-3-haiku-20240307")
|
||||
assert orchestrator._model == "claude-3-haiku-20240307"
|
||||
|
||||
def test_skips_auto_creation_when_llm_passed(self):
|
||||
"""Test that auto-creation is skipped when llm is explicitly passed."""
|
||||
mock_llm = Mock(spec=LLMProvider)
|
||||
|
||||
with patch.object(LiteLLMProvider, '__init__', return_value=None) as mock_init:
|
||||
orchestrator = AgentOrchestrator(llm=mock_llm)
|
||||
|
||||
mock_init.assert_not_called()
|
||||
assert orchestrator._llm is mock_llm
|
||||
|
||||
def test_model_attribute_stored_correctly(self):
|
||||
"""Test that _model attribute is stored correctly."""
|
||||
with patch.object(LiteLLMProvider, '__init__', return_value=None):
|
||||
orchestrator = AgentOrchestrator(model="gemini/gemini-1.5-flash")
|
||||
|
||||
assert orchestrator._model == "gemini/gemini-1.5-flash"
|
||||
|
||||
|
||||
class TestOrchestratorLLMProviderType:
|
||||
"""Test that orchestrator uses correct LLM provider type."""
|
||||
|
||||
def test_llm_is_litellm_provider_instance(self):
|
||||
"""Test that auto-created _llm is a LiteLLMProvider instance."""
|
||||
orchestrator = AgentOrchestrator()
|
||||
|
||||
assert isinstance(orchestrator._llm, LiteLLMProvider)
|
||||
|
||||
def test_llm_implements_llm_provider_interface(self):
|
||||
"""Test that _llm implements LLMProvider interface."""
|
||||
orchestrator = AgentOrchestrator()
|
||||
|
||||
assert isinstance(orchestrator._llm, LLMProvider)
|
||||
assert hasattr(orchestrator._llm, 'complete')
|
||||
assert hasattr(orchestrator._llm, 'complete_with_tools')
|
||||
@@ -1,37 +0,0 @@
|
||||
# Development overrides
|
||||
# Copy this file to docker-compose.override.yml for local development
|
||||
#
|
||||
# Usage:
|
||||
# cp docker-compose.override.yml.example docker-compose.override.yml
|
||||
# docker compose up
|
||||
#
|
||||
# This enables:
|
||||
# - Hot reload for both frontend and backend
|
||||
# - Source code mounted as volumes
|
||||
# - Debug ports exposed
|
||||
# - Development environment settings
|
||||
|
||||
services:
|
||||
honeycomb:
|
||||
build:
|
||||
context: ./honeycomb
|
||||
dockerfile: Dockerfile.dev
|
||||
volumes:
|
||||
- ./honeycomb/src:/app/src:ro
|
||||
- ./honeycomb/public:/app/public:ro
|
||||
- ./honeycomb/index.html:/app/index.html:ro
|
||||
environment:
|
||||
- VITE_API_URL=http://localhost:4000
|
||||
|
||||
hive:
|
||||
build:
|
||||
context: ./hive
|
||||
dockerfile: Dockerfile.dev
|
||||
volumes:
|
||||
- ./hive/src:/app/src:ro
|
||||
environment:
|
||||
- NODE_ENV=development
|
||||
- LOG_LEVEL=debug
|
||||
# Uncomment to enable Node.js debugging
|
||||
# ports:
|
||||
# - "9229:9229"
|
||||
@@ -1,168 +0,0 @@
|
||||
services:
|
||||
# Frontend - React application
|
||||
honeycomb:
|
||||
build:
|
||||
context: ./honeycomb
|
||||
target: production
|
||||
args:
|
||||
VITE_API_URL: ${VITE_API_URL:-http://localhost:4000}
|
||||
container_name: honeycomb-frontend
|
||||
ports:
|
||||
- "${FRONTEND_PORT:-3000}:3000"
|
||||
depends_on:
|
||||
hive:
|
||||
condition: service_healthy
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- honeycomb-network
|
||||
|
||||
# Backend - Hive API (LLM observability & control plane)
|
||||
hive:
|
||||
build:
|
||||
context: ./hive
|
||||
target: production
|
||||
args:
|
||||
NPM_TOKEN: ${NPM_TOKEN:-}
|
||||
container_name: honeycomb-backend
|
||||
ports:
|
||||
- "${BACKEND_PORT:-4000}:4000"
|
||||
environment:
|
||||
- NODE_ENV=${NODE_ENV:-production}
|
||||
- PORT=4000
|
||||
- LOG_LEVEL=${LOG_LEVEL:-info}
|
||||
# PostgreSQL (TimescaleDB)
|
||||
- TSDB_PG_URL=postgresql://postgres:postgres@timescaledb:5432/aden_tsdb
|
||||
# MongoDB
|
||||
- MONGODB_URL=mongodb://mongodb:27017
|
||||
- MONGODB_DBNAME=${MONGODB_DBNAME:-aden}
|
||||
- MONGODB_ERP_DBNAME=${MONGODB_ERP_DBNAME:-erp}
|
||||
# Redis
|
||||
- REDIS_URL=redis://redis:6379
|
||||
# Authentication
|
||||
- JWT_SECRET=${JWT_SECRET:-change-me-in-production-use-min-32-chars}
|
||||
- PASSPHRASE=${PASSPHRASE:-change-me-in-production}
|
||||
# Hive backend URL for SDK quickstart documents
|
||||
- HIVE_HOST=${HIVE_HOST:-http://localhost:4000}
|
||||
depends_on:
|
||||
timescaledb:
|
||||
condition: service_healthy
|
||||
mongodb:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
"CMD",
|
||||
"node",
|
||||
"-e",
|
||||
"fetch('http://localhost:4000/health').then(r => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))",
|
||||
]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 15s
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- honeycomb-network
|
||||
|
||||
# TimescaleDB - Time series database for LLM metrics
|
||||
timescaledb:
|
||||
image: timescale/timescaledb:latest-pg16
|
||||
container_name: honeycomb-timescaledb
|
||||
ports:
|
||||
- "${TSDB_PORT:-5432}:5432"
|
||||
environment:
|
||||
- POSTGRES_USER=postgres
|
||||
- POSTGRES_PASSWORD=postgres
|
||||
- POSTGRES_DB=aden_tsdb
|
||||
command: ["postgres", "-c", "log_min_messages=warning", "-c", "log_statement=none"]
|
||||
volumes:
|
||||
- timescaledb_data:/var/lib/postgresql/data
|
||||
# Auto-run schema files on first startup (alphabetical order)
|
||||
- ./hive/src/services/tsdb/00-init-timescaledb.sql:/docker-entrypoint-initdb.d/00-init-timescaledb.sql:ro
|
||||
- ./hive/src/services/tsdb/schema.sql:/docker-entrypoint-initdb.d/01-schema.sql:ro
|
||||
- ./hive/src/services/tsdb/users_schema.sql:/docker-entrypoint-initdb.d/02-users.sql:ro
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U postgres -d aden_tsdb"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 10s
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- honeycomb-network
|
||||
|
||||
# MongoDB - Policies, pricing, and control configuration
|
||||
mongodb:
|
||||
image: mongo:7
|
||||
container_name: honeycomb-mongodb
|
||||
ports:
|
||||
- "${MONGODB_PORT:-27017}:27017"
|
||||
command: ["mongod", "--quiet", "--logpath", "/dev/null"]
|
||||
volumes:
|
||||
- mongodb_data:/data/db
|
||||
healthcheck:
|
||||
test: ["CMD", "mongosh", "--quiet", "--eval", "db.adminCommand('ping')"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 10s
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- honeycomb-network
|
||||
|
||||
# Redis - Caching and Socket.IO adapter
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
container_name: honeycomb-redis
|
||||
ports:
|
||||
- "${REDIS_PORT:-6379}:6379"
|
||||
command: ["redis-server", "--loglevel", "warning"]
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 5s
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- honeycomb-network
|
||||
|
||||
# Aden Tools MCP Server - Python tools via Model Context Protocol
|
||||
aden-tools-mcp:
|
||||
build:
|
||||
context: ./aden-tools
|
||||
container_name: honeycomb-aden-tools-mcp
|
||||
ports:
|
||||
- "${ADEN_TOOLS_MCP_PORT:-4001}:4001"
|
||||
environment:
|
||||
- MCP_PORT=4001
|
||||
# Pass through tool-specific env vars
|
||||
- BRAVE_SEARCH_API_KEY=${BRAVE_SEARCH_API_KEY:-}
|
||||
volumes:
|
||||
- .:/workspace:rw # Mount project root for file access
|
||||
- aden_tools_workspaces:/app/workdir/workspaces # Persist file system tool workspaces
|
||||
working_dir: /workspace # Set working directory so relative paths work
|
||||
command: ["python", "/app/mcp_server.py"] # Use absolute path since working_dir changed
|
||||
healthcheck:
|
||||
test: ["CMD", "python", "-c", "import httpx; httpx.get('http://localhost:4001/health').raise_for_status()"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 10s
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- honeycomb-network
|
||||
|
||||
networks:
|
||||
honeycomb-network:
|
||||
driver: bridge
|
||||
|
||||
volumes:
|
||||
timescaledb_data:
|
||||
mongodb_data:
|
||||
redis_data:
|
||||
aden_tools_workspaces:
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 253 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 253 KiB |
+129
-101
@@ -1,163 +1,191 @@
|
||||
# Getting Started
|
||||
|
||||
This guide will help you get Hive running on your local machine.
|
||||
This guide will help you set up the Aden Agent Framework and build your first agent.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **Docker** (v20.10+) and **Docker Compose** (v2.0+) - for containerized deployment
|
||||
- **Node.js** (v20+) - for local development without Docker
|
||||
- **Python 3.11+** ([Download](https://www.python.org/downloads/)) - Python 3.12 or 3.13 recommended
|
||||
- **pip** - Package installer for Python (comes with Python)
|
||||
- **git** - Version control
|
||||
- **Claude Code** ([Install](https://docs.anthropic.com/claude/docs/claude-code)) - Optional, for using building skills
|
||||
|
||||
## Quick Start with Docker
|
||||
## Quick Start
|
||||
|
||||
The fastest way to get started is using Docker Compose:
|
||||
The fastest way to get started:
|
||||
|
||||
```bash
|
||||
# 1. Clone the repository
|
||||
git clone https://github.com/adenhq/hive.git
|
||||
cd hive
|
||||
|
||||
# 2. Copy and configure
|
||||
cp config.yaml.example config.yaml
|
||||
# 2. Run automated Python setup
|
||||
./scripts/setup-python.sh
|
||||
|
||||
# 3. Run setup
|
||||
npm run setup
|
||||
|
||||
# 4. Start services
|
||||
docker compose up
|
||||
# 3. Verify installation
|
||||
python -c "import framework; import aden_tools; print('✓ Setup complete')"
|
||||
```
|
||||
|
||||
The application will be available at:
|
||||
- **Frontend**: http://localhost:3000
|
||||
- **Backend API**: http://localhost:4000
|
||||
- **Health Check**: http://localhost:4000/health
|
||||
## Building Your First Agent
|
||||
|
||||
## Development Setup
|
||||
|
||||
For local development with hot reload:
|
||||
### Option 1: Using Claude Code Skills (Recommended)
|
||||
|
||||
```bash
|
||||
# 1. Clone and configure (same as above)
|
||||
git clone https://github.com/adenhq/hive.git
|
||||
cd hive
|
||||
cp config.yaml.example config.yaml
|
||||
# Install Claude Code skills (one-time)
|
||||
./quickstart.sh
|
||||
|
||||
# 2. Install dependencies
|
||||
npm install
|
||||
|
||||
# 3. Generate environment files
|
||||
npm run generate:env
|
||||
|
||||
# 4. Start frontend (terminal 1)
|
||||
cd honeycomb
|
||||
npm run dev
|
||||
|
||||
# 5. Start backend (terminal 2)
|
||||
cd hive
|
||||
npm run dev
|
||||
# Start Claude Code and build an agent
|
||||
claude> /building-agents
|
||||
```
|
||||
|
||||
### Using Docker for Development
|
||||
Follow the interactive prompts to:
|
||||
1. Define your agent's goal
|
||||
2. Design the workflow (nodes and edges)
|
||||
3. Generate the agent package
|
||||
4. Test the agent
|
||||
|
||||
You can also use Docker with hot reload enabled:
|
||||
### Option 2: From an Example
|
||||
|
||||
```bash
|
||||
# Copy development overrides
|
||||
cp docker-compose.override.yml.example docker-compose.override.yml
|
||||
# Copy an example agent
|
||||
cp -r exports/support_ticket_agent exports/my_agent
|
||||
|
||||
# Start with hot reload
|
||||
docker compose up
|
||||
# Customize the agent
|
||||
cd exports/my_agent
|
||||
# Edit agent.json, tools.py, README.md
|
||||
|
||||
# Validate the agent
|
||||
PYTHONPATH=core:exports python -m my_agent validate
|
||||
```
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
hive/
|
||||
├── honeycomb/ # Frontend (React + TypeScript + Vite)
|
||||
│ ├── src/
|
||||
│ │ ├── components/ # Reusable UI components
|
||||
│ │ ├── pages/ # Page components
|
||||
│ │ ├── hooks/ # Custom React hooks
|
||||
│ │ ├── services/ # API client and services
|
||||
│ │ ├── types/ # TypeScript type definitions
|
||||
│ │ └── utils/ # Utility functions
|
||||
│ └── public/ # Static assets
|
||||
├── core/ # Core Framework
|
||||
│ ├── framework/ # Agent runtime, graph executor
|
||||
│ │ ├── runner/ # AgentRunner - loads and runs agents
|
||||
│ │ ├── executor/ # GraphExecutor - executes node graphs
|
||||
│ │ ├── protocols/ # Standard protocols (hooks, tracing)
|
||||
│ │ ├── llm/ # LLM provider integrations
|
||||
│ │ └── memory/ # Memory systems (STM, LTM/RLM)
|
||||
│ └── pyproject.toml # Package metadata
|
||||
│
|
||||
├── hive/ # Backend (Node.js + TypeScript + Express)
|
||||
│ └── src/
|
||||
│ ├── controllers/ # Request handlers
|
||||
│ ├── middleware/ # Express middleware
|
||||
│ ├── models/ # Data models
|
||||
│ ├── routes/ # API routes
|
||||
│ ├── services/ # Business logic
|
||||
│ ├── types/ # TypeScript types
|
||||
│ └── utils/ # Utility functions
|
||||
├── tools/ # MCP Tools Package
|
||||
│ └── src/aden_tools/ # 19 tools for agent capabilities
|
||||
│ ├── tools/ # Individual tool implementations
|
||||
│ │ ├── web_search_tool/
|
||||
│ │ ├── web_scrape_tool/
|
||||
│ │ └── file_system_toolkits/
|
||||
│ └── mcp_server.py # HTTP MCP server
|
||||
│
|
||||
├── docs/ # Documentation
|
||||
├── scripts/ # Build and utility scripts
|
||||
└── config.yaml # Application configuration
|
||||
├── exports/ # Agent Packages
|
||||
│ ├── support_ticket_agent/
|
||||
│ ├── market_research_agent/
|
||||
│ └── ... # Your agents go here
|
||||
│
|
||||
├── .claude/ # Claude Code Skills
|
||||
│ └── skills/
|
||||
│ ├── building-agents/
|
||||
│ └── testing-agent/
|
||||
│
|
||||
└── docs/ # Documentation
|
||||
```
|
||||
|
||||
## AI Agent Tools Setup (Optional)
|
||||
|
||||
If you're using the AI agent framework with aden-tools:
|
||||
## Running an Agent
|
||||
|
||||
```bash
|
||||
# 1. Navigate to aden-tools
|
||||
cd aden-tools
|
||||
# Validate agent structure
|
||||
PYTHONPATH=core:exports python -m my_agent validate
|
||||
|
||||
# 2. Copy environment template
|
||||
cp .env.example .env
|
||||
# Show agent information
|
||||
PYTHONPATH=core:exports python -m my_agent info
|
||||
|
||||
# 3. Add your API keys to .env
|
||||
# - ANTHROPIC_API_KEY: Required for LLM operations
|
||||
# - BRAVE_SEARCH_API_KEY: Required for web search tool
|
||||
# Run agent with input
|
||||
PYTHONPATH=core:exports python -m my_agent run --input '{
|
||||
"task": "Your input here"
|
||||
}'
|
||||
|
||||
# Run in mock mode (no LLM calls)
|
||||
PYTHONPATH=core:exports python -m my_agent run --mock --input '{...}'
|
||||
```
|
||||
|
||||
## API Keys Setup
|
||||
|
||||
For running agents with real LLMs:
|
||||
|
||||
```bash
|
||||
# Add to your shell profile (~/.bashrc, ~/.zshrc, etc.)
|
||||
export ANTHROPIC_API_KEY="your-key-here"
|
||||
export OPENAI_API_KEY="your-key-here" # Optional
|
||||
export BRAVE_SEARCH_API_KEY="your-key-here" # Optional, for web search
|
||||
```
|
||||
|
||||
Get your API keys:
|
||||
- **Anthropic**: [console.anthropic.com](https://console.anthropic.com/)
|
||||
- **OpenAI**: [platform.openai.com](https://platform.openai.com/)
|
||||
- **Brave Search**: [brave.com/search/api](https://brave.com/search/api/)
|
||||
|
||||
## Testing Your Agent
|
||||
|
||||
```bash
|
||||
# Using Claude Code
|
||||
claude> /testing-agent
|
||||
|
||||
# Or manually
|
||||
PYTHONPATH=core:exports python -m my_agent test
|
||||
|
||||
# Run with specific test type
|
||||
PYTHONPATH=core:exports python -m my_agent test --type constraint
|
||||
PYTHONPATH=core:exports python -m my_agent test --type success
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. **Configure the Application**: See [Configuration Guide](configuration.md)
|
||||
2. **Understand the Architecture**: See [Architecture Overview](architecture.md)
|
||||
3. **Start Building**: Add your own components and API endpoints
|
||||
1. **Detailed Setup**: See [ENVIRONMENT_SETUP.md](../ENVIRONMENT_SETUP.md)
|
||||
2. **Developer Guide**: See [DEVELOPER.md](../DEVELOPER.md)
|
||||
3. **Agent Patterns**: Explore examples in `/exports`
|
||||
4. **Custom Tools**: Learn to integrate MCP servers
|
||||
5. **Join Community**: [Discord](https://discord.com/invite/MXE49hrKDk)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Port Already in Use
|
||||
|
||||
If ports 3000 or 4000 are in use, update `config.yaml`:
|
||||
|
||||
```yaml
|
||||
server:
|
||||
frontend:
|
||||
port: 3001 # Change to available port
|
||||
backend:
|
||||
port: 4001
|
||||
```
|
||||
|
||||
Then regenerate environment files:
|
||||
### ModuleNotFoundError: No module named 'framework'
|
||||
|
||||
```bash
|
||||
npm run generate:env
|
||||
# Reinstall framework package
|
||||
cd core
|
||||
pip install -e .
|
||||
```
|
||||
|
||||
### Docker Build Fails
|
||||
|
||||
Clear Docker cache and rebuild:
|
||||
### ModuleNotFoundError: No module named 'aden_tools'
|
||||
|
||||
```bash
|
||||
docker compose down
|
||||
docker compose build --no-cache
|
||||
docker compose up
|
||||
# Reinstall tools package
|
||||
cd tools
|
||||
pip install -e .
|
||||
```
|
||||
|
||||
### Dependencies Issues
|
||||
|
||||
Clear node_modules and reinstall:
|
||||
### LLM API Errors
|
||||
|
||||
```bash
|
||||
npm run clean
|
||||
npm install
|
||||
# Verify API key is set
|
||||
echo $ANTHROPIC_API_KEY
|
||||
|
||||
# Run in mock mode to test without API
|
||||
PYTHONPATH=core:exports python -m my_agent run --mock --input '{...}'
|
||||
```
|
||||
|
||||
### Package Installation Issues
|
||||
|
||||
```bash
|
||||
# Remove and reinstall
|
||||
pip uninstall -y framework aden-tools
|
||||
./scripts/setup-python.sh
|
||||
```
|
||||
|
||||
## Getting Help
|
||||
|
||||
- **Documentation**: Check the `/docs` folder
|
||||
- **Issues**: [github.com/adenhq/hive/issues](https://github.com/adenhq/hive/issues)
|
||||
- **Discord**: [discord.com/invite/MXE49hrKDk](https://discord.com/invite/MXE49hrKDk)
|
||||
- **Examples**: Explore `/exports` for working agents
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
# 🚀 Software Development Engineer
|
||||
|
||||
**Location:** San Francisco, CA (Hybrid) or Remote
|
||||
**Type:** Full-time
|
||||
**Team:** Engineering
|
||||
|
||||
---
|
||||
|
||||
## About Aden
|
||||
|
||||
We're building the future of AI agents. Aden is an open-source framework for creating self-improving, production-ready AI agents with built-in cost controls, human-in-the-loop capabilities, and comprehensive observability.
|
||||
|
||||
Our mission: Make AI agents reliable enough for real-world production use.
|
||||
|
||||
---
|
||||
|
||||
## The Role
|
||||
|
||||
We're looking for a Software Development Engineer to help build and scale our AI agent platform. You'll work across the full stack, from our React dashboard to our Node.js backend, contributing to core infrastructure that powers autonomous AI systems.
|
||||
|
||||
This is an opportunity to work on cutting-edge AI infrastructure alongside a small, experienced team passionate about shipping great software.
|
||||
|
||||
---
|
||||
|
||||
## What You'll Do
|
||||
|
||||
- Build and maintain features across our full-stack TypeScript codebase
|
||||
- Design and implement APIs for agent management, monitoring, and control
|
||||
- Work with real-time systems (WebSockets, event streaming)
|
||||
- Optimize database performance (TimescaleDB, MongoDB, Redis)
|
||||
- Contribute to our Model Context Protocol (MCP) server and tooling
|
||||
- Collaborate on architecture decisions for scalability and reliability
|
||||
- Write clean, tested, well-documented code
|
||||
- Participate in code reviews and help maintain code quality
|
||||
|
||||
---
|
||||
|
||||
## Tech Stack
|
||||
|
||||
**Frontend (Honeycomb Dashboard)**
|
||||
- React 18 + TypeScript
|
||||
- Vite
|
||||
- Tailwind CSS + Radix UI
|
||||
- Zustand (state management)
|
||||
- TanStack Query
|
||||
- Recharts + Vega (data visualization)
|
||||
- Socket.io (real-time updates)
|
||||
|
||||
**Backend (Hive)**
|
||||
- Node.js + Express + TypeScript
|
||||
- Socket.io (WebSocket)
|
||||
- Model Context Protocol (MCP)
|
||||
- Zod (validation)
|
||||
- Passport + JWT (authentication)
|
||||
|
||||
**Data Layer**
|
||||
- TimescaleDB (time-series metrics)
|
||||
- MongoDB (policies, configuration)
|
||||
- Redis (caching, pub/sub)
|
||||
|
||||
**Infrastructure**
|
||||
- Docker + Docker Compose
|
||||
- Kubernetes + Kustomize
|
||||
- GitHub Actions (CI/CD)
|
||||
- Nginx
|
||||
|
||||
---
|
||||
|
||||
## What We're Looking For
|
||||
|
||||
**Required:**
|
||||
- 2+ years of professional software development experience
|
||||
- Strong proficiency in TypeScript and Node.js
|
||||
- Experience with React and modern frontend development
|
||||
- Familiarity with SQL and NoSQL databases
|
||||
- Understanding of RESTful APIs and WebSocket communication
|
||||
- Comfortable with Git and collaborative development workflows
|
||||
- Strong problem-solving skills and attention to detail
|
||||
|
||||
**Nice to Have:**
|
||||
- Experience with AI/LLM applications or agent frameworks
|
||||
- Knowledge of time-series databases (TimescaleDB, InfluxDB)
|
||||
- Kubernetes and container orchestration experience
|
||||
- Experience with real-time systems at scale
|
||||
- Contributions to open-source projects
|
||||
- Familiarity with Model Context Protocol (MCP)
|
||||
|
||||
---
|
||||
|
||||
## What We Offer
|
||||
|
||||
- Competitive salary + equity
|
||||
- Health, dental, and vision insurance
|
||||
- Flexible work arrangements (hybrid/remote)
|
||||
- Learning & development budget
|
||||
- Home office setup stipend
|
||||
- Opportunity to work on open-source AI infrastructure
|
||||
- Small team, big impact
|
||||
|
||||
---
|
||||
|
||||
## How to Apply
|
||||
|
||||
**Show us what you can do by contributing to our open-source project:**
|
||||
|
||||
1. **Solve an existing issue**
|
||||
- Browse our [GitHub Issues](https://github.com/adenhq/hive/issues)
|
||||
- Look for issues labeled `good first issue` or `help wanted`
|
||||
- Comment on the issue to claim it
|
||||
- Submit a Pull Request with your solution
|
||||
|
||||
2. **Create new issues**
|
||||
- Found a bug? Report it with clear reproduction steps
|
||||
- Have an idea? Open a feature request with your proposal
|
||||
- Spotted documentation gaps? Suggest improvements
|
||||
- Quality issues that show you understand the codebase stand out
|
||||
|
||||
3. **Submit Pull Requests**
|
||||
- Fix bugs, add features, or improve documentation
|
||||
- Follow our contribution guidelines
|
||||
- Write clear PR descriptions explaining your changes
|
||||
- Respond to code review feedback
|
||||
|
||||
4. **Submit your application:**
|
||||
- Email: `contact@adenhq.com`
|
||||
- Subject: `[SDE] Your Name`
|
||||
- Include:
|
||||
- Resume/CV
|
||||
- GitHub profile
|
||||
- Links to your Issues and/or PRs on our repo
|
||||
- Brief intro about yourself
|
||||
|
||||
5. **What happens next:**
|
||||
- We review your contributions (1-2 weeks)
|
||||
- Technical interview (60 min)
|
||||
- Team interview (45 min)
|
||||
- Offer 🎉
|
||||
|
||||
---
|
||||
|
||||
## Why Join Us?
|
||||
|
||||
- **Impact:** Your code will power AI agents used by developers worldwide
|
||||
- **Open Source:** Everything we build is open source
|
||||
- **Learning:** Work with cutting-edge AI and distributed systems
|
||||
- **Culture:** Small team, low ego, high trust, ship fast
|
||||
- **Growth:** Early-stage company with room to grow
|
||||
|
||||
---
|
||||
|
||||
*Aden is an equal opportunity employer. We celebrate diversity and are committed to creating an inclusive environment for all employees.*
|
||||
|
||||
---
|
||||
|
||||
**Questions?** Email us at `contact@adenhq.com` or open an issue on [GitHub](https://github.com/adenhq/hive).
|
||||
|
||||
Made with 🔥 Passion in San Francisco
|
||||
@@ -2,6 +2,14 @@
|
||||
|
||||
Welcome to the Aden Engineering Challenges! These quizzes are designed for students and applicants who want to join the Aden team or contribute to our open-source projects.
|
||||
|
||||
---
|
||||
|
||||
## 💼 We're Hiring!
|
||||
|
||||
**[Software Development Engineer](./00-job-post.md)** - Full-stack TypeScript, React, Node.js, AI agents
|
||||
|
||||
---
|
||||
|
||||
## How It Works
|
||||
|
||||
1. **Choose your track** based on your interests and skill level
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
# Server Configuration
|
||||
PORT=4000
|
||||
NODE_ENV=development
|
||||
|
||||
# TSDB PostgreSQL (TimescaleDB)
|
||||
TSDB_PG_URL=postgresql://user:password@localhost:5432/aden_tsdb
|
||||
|
||||
# User Database (MySQL - read-only access)
|
||||
MYSQL_HOST=localhost
|
||||
MYSQL_PORT=3306
|
||||
MYSQL_USER=aden_reader
|
||||
MYSQL_PASSWORD=
|
||||
MYSQL_DATABASE=aden
|
||||
|
||||
# MongoDB (policies and pricing data)
|
||||
MONGODB_URL=mongodb://localhost:27017
|
||||
MONGODB_DBNAME=aden
|
||||
MONGODB_ERP_DBNAME=erp
|
||||
|
||||
# Redis (caching and socket.io adapter)
|
||||
REDIS_URL=redis://localhost:6379
|
||||
|
||||
# JWT Authentication
|
||||
JWT_SECRET=your-jwt-secret
|
||||
PASSPHRASE=your-passphrase
|
||||
|
||||
# Logging
|
||||
LOG_LEVEL=info
|
||||
@@ -1,24 +0,0 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: { node: true, es2020: true },
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
],
|
||||
ignorePatterns: ['dist', '.eslintrc.cjs'],
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module',
|
||||
},
|
||||
rules: {
|
||||
// Allow unused vars that start with underscore
|
||||
'@typescript-eslint/no-unused-vars': ['error', {
|
||||
argsIgnorePattern: '^_',
|
||||
varsIgnorePattern: '^_',
|
||||
destructuredArrayIgnorePattern: '^_',
|
||||
}],
|
||||
// Allow any types (common in API/external data handling)
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
},
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
# Build stage
|
||||
FROM node:20-alpine AS builder
|
||||
|
||||
ARG NPM_TOKEN
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Configure npm for private packages (@acho-inc/administration)
|
||||
RUN echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > .npmrc
|
||||
|
||||
# Copy package files
|
||||
COPY package*.json ./
|
||||
COPY tsconfig.json ./
|
||||
|
||||
# Install all dependencies (including dev for TypeScript build)
|
||||
RUN npm install
|
||||
|
||||
# Copy source code
|
||||
COPY src ./src
|
||||
|
||||
# Copy docs for quickstart templates
|
||||
COPY docs ./docs
|
||||
|
||||
# Build TypeScript
|
||||
RUN npm run build
|
||||
|
||||
# Remove npmrc after build
|
||||
RUN rm -f .npmrc
|
||||
|
||||
# Production stage
|
||||
FROM node:20-alpine AS production
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Create non-root user
|
||||
RUN addgroup -g 1001 -S nodejs && \
|
||||
adduser -S nodejs -u 1001
|
||||
|
||||
# Copy package files for production deps
|
||||
COPY package*.json ./
|
||||
|
||||
# Configure npm for private packages (needed for production install)
|
||||
ARG NPM_TOKEN
|
||||
RUN echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > .npmrc && \
|
||||
npm install --omit=dev && \
|
||||
rm -f .npmrc && \
|
||||
npm cache clean --force
|
||||
|
||||
# Copy compiled JavaScript from builder
|
||||
COPY --from=builder --chown=nodejs:nodejs /app/dist ./dist
|
||||
|
||||
# Copy docs directory for quickstart templates
|
||||
COPY --from=builder --chown=nodejs:nodejs /app/docs ./docs
|
||||
|
||||
USER nodejs
|
||||
|
||||
# Default port (can be overridden via PORT env var)
|
||||
EXPOSE 4000
|
||||
|
||||
ENV NODE_ENV=production
|
||||
|
||||
# Health check
|
||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
|
||||
CMD node -e "fetch('http://localhost:' + (process.env.PORT || 4000) + '/health').then(r => r.ok ? process.exit(0) : process.exit(1)).catch(() => process.exit(1))"
|
||||
|
||||
CMD ["node", "dist/index.js"]
|
||||
@@ -1,24 +0,0 @@
|
||||
# Development Dockerfile with hot reload
|
||||
FROM node:20-alpine
|
||||
|
||||
ARG NPM_TOKEN
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Configure npm for private packages (@acho-inc/administration)
|
||||
RUN echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > .npmrc
|
||||
|
||||
# Copy package files
|
||||
COPY package*.json ./
|
||||
|
||||
# Install dependencies
|
||||
RUN npm install && rm -f .npmrc
|
||||
|
||||
# Copy source code
|
||||
COPY . .
|
||||
|
||||
# Expose ports (app + debug)
|
||||
EXPOSE 4000 9229
|
||||
|
||||
# Start development server with hot reload
|
||||
CMD ["npm", "run", "dev"]
|
||||
@@ -1,24 +0,0 @@
|
||||
{
|
||||
"generic": {
|
||||
"name": "Generic",
|
||||
"description": "Generic agent integration",
|
||||
"pythonSupport": true,
|
||||
"typescriptSupport": true,
|
||||
"templateFile": "generic"
|
||||
},
|
||||
"langgraph": {
|
||||
"name": "LangGraph",
|
||||
"description": "LangGraph agent integration",
|
||||
"pythonSupport": true,
|
||||
"typescriptSupport": true,
|
||||
"templateFile": "langgraph"
|
||||
},
|
||||
"livekit": {
|
||||
"name": "LiveKit",
|
||||
"description": "LiveKit voice agent integration",
|
||||
"pythonSupport": true,
|
||||
"typescriptSupport": false,
|
||||
"adenPythonExtra": "livekit",
|
||||
"templateFile": "livekit"
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"openai": {
|
||||
"name": "OpenAI",
|
||||
"envVarComment": "# or ANTHROPIC_API_KEY, GOOGLE_API_KEY"
|
||||
},
|
||||
"anthropic": {
|
||||
"name": "Anthropic",
|
||||
"envVarComment": "# or OPENAI_API_KEY, GOOGLE_API_KEY"
|
||||
},
|
||||
"google": {
|
||||
"name": "Google",
|
||||
"envVarComment": "# or OPENAI_API_KEY, ANTHROPIC_API_KEY"
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"python": {
|
||||
"name": "Python",
|
||||
"adenPackage": "aden-py"
|
||||
},
|
||||
"javascript": {
|
||||
"name": "JavaScript/TypeScript",
|
||||
"adenPackage": "aden-ts"
|
||||
}
|
||||
}
|
||||
@@ -1,191 +0,0 @@
|
||||
Quick reference for integrating Aden LLM observability & cost control into LangFlow applications.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
`.env` file should contain:
|
||||
|
||||
```
|
||||
OPENAI_API_KEY=sk-xxx # or ANTHROPIC_API_KEY, GOOGLE_API_KEY
|
||||
ADEN_API_URL=https://hive.adenhq.com
|
||||
ADEN_API_KEY=your-aden-api-key
|
||||
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
pip install aden-py langflow python-dotenv
|
||||
|
||||
```
|
||||
|
||||
## Basic Setup (3 Steps)
|
||||
|
||||
### 1. Import and Load Environment
|
||||
|
||||
```python
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv()
|
||||
|
||||
from aden import (
|
||||
instrument,
|
||||
uninstrument,
|
||||
MeterOptions,
|
||||
create_console_emitter,
|
||||
BeforeRequestResult,
|
||||
RequestCancelledError,
|
||||
)
|
||||
|
||||
```
|
||||
|
||||
### 2. Define Budget Check Callback
|
||||
|
||||
```python
|
||||
def budget_check(params, context):
|
||||
"""Enforce budget limits before each LLM request."""
|
||||
budget_info = getattr(context, 'budget', None)
|
||||
|
||||
if budget_info and budget_info.get('exhausted', False):
|
||||
return BeforeRequestResult.cancel("Budget exhausted")
|
||||
|
||||
if budget_info and budget_info.get('percent_used', 0) >= 95:
|
||||
return BeforeRequestResult.throttle(delay_ms=2000)
|
||||
|
||||
if budget_info and budget_info.get('percent_used', 0) >= 80:
|
||||
return BeforeRequestResult.degrade(to_model="gpt-4o-mini", reason="Approaching limit")
|
||||
|
||||
return BeforeRequestResult.proceed()
|
||||
|
||||
```
|
||||
|
||||
### 3. Initialize Aden (at startup)
|
||||
|
||||
```python
|
||||
instrument(MeterOptions(
|
||||
api_key=os.environ.get("ADEN_API_KEY"),
|
||||
server_url=os.environ.get("ADEN_API_URL"),
|
||||
emit_metric=create_console_emitter(pretty=True),
|
||||
on_alert=lambda alert: print(f"[Aden {alert.level}] {alert.message}"),
|
||||
before_request=budget_check,
|
||||
))
|
||||
|
||||
```
|
||||
|
||||
### 4. Use LangFlow Components
|
||||
|
||||
```python
|
||||
from langflow.components.models import LanguageModelComponent
|
||||
|
||||
comp = LanguageModelComponent()
|
||||
comp.set_attributes({
|
||||
"provider": "Google", # or "OpenAI"
|
||||
"model_name": "gemini-2.0-flash",
|
||||
"api_key": os.getenv("GOOGLE_API_KEY"),
|
||||
"stream": False,
|
||||
})
|
||||
|
||||
model = comp.build_model()
|
||||
|
||||
try:
|
||||
response = model.invoke("Hello!")
|
||||
print(response.content)
|
||||
except RequestCancelledError as e:
|
||||
print(f"Budget exceeded: {e}")
|
||||
|
||||
```
|
||||
|
||||
### 5. Cleanup (on exit)
|
||||
|
||||
```python
|
||||
uninstrument()
|
||||
|
||||
```
|
||||
|
||||
## Complete Template
|
||||
|
||||
```python
|
||||
"""LangFlow with Aden instrumentation"""
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv()
|
||||
|
||||
from aden import (
|
||||
instrument, uninstrument, MeterOptions,
|
||||
create_console_emitter, BeforeRequestResult, RequestCancelledError,
|
||||
)
|
||||
|
||||
# Budget enforcement callback
|
||||
def budget_check(params, context):
|
||||
budget_info = getattr(context, 'budget', None)
|
||||
if budget_info and budget_info.get('exhausted', False):
|
||||
return BeforeRequestResult.cancel("Budget exhausted")
|
||||
if budget_info and budget_info.get('percent_used', 0) >= 95:
|
||||
return BeforeRequestResult.throttle(delay_ms=2000)
|
||||
if budget_info and budget_info.get('percent_used', 0) >= 80:
|
||||
return BeforeRequestResult.degrade(to_model="gpt-4o-mini", reason="Approaching limit")
|
||||
return BeforeRequestResult.proceed()
|
||||
|
||||
# Initialize Aden
|
||||
instrument(MeterOptions(
|
||||
api_key=os.environ.get("ADEN_API_KEY"),
|
||||
server_url=os.environ.get("ADEN_API_URL"),
|
||||
emit_metric=create_console_emitter(pretty=True),
|
||||
on_alert=lambda alert: print(f"[Aden {alert.level}] {alert.message}"),
|
||||
before_request=budget_check,
|
||||
))
|
||||
|
||||
# === YOUR LANGFLOW CODE HERE ===
|
||||
|
||||
from langflow.components.models import LanguageModelComponent
|
||||
|
||||
def run_model(user_input: str):
|
||||
try:
|
||||
comp = LanguageModelComponent()
|
||||
comp.set_attributes({
|
||||
"provider": "Google",
|
||||
"model_name": "gemini-2.0-flash",
|
||||
"api_key": os.getenv("GOOGLE_API_KEY"),
|
||||
"stream": False,
|
||||
})
|
||||
model = comp.build_model()
|
||||
return model.invoke(user_input).content
|
||||
except RequestCancelledError as e:
|
||||
return f"Sorry, you have used up your allowance. {e}"
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
print(run_model("Say hello!"))
|
||||
finally:
|
||||
uninstrument()
|
||||
|
||||
```
|
||||
|
||||
## Supported Providers
|
||||
|
||||
| Provider | Model Example | Notes |
|
||||
| --------- | ------------------- | -------------------------------- |
|
||||
| OpenAI | gpt-4o, gpt-4o-mini | Direct SDK instrumentation |
|
||||
| Google | gemini-2.0-flash | Uses gRPC client instrumentation |
|
||||
| Anthropic | claude-3-opus | Direct SDK instrumentation |
|
||||
|
||||
## Budget Actions Reference
|
||||
|
||||
| Action | When | Behavior |
|
||||
| ----------------------------------------------- | ----------------- | ------------------------------ |
|
||||
| `BeforeRequestResult.proceed()` | Within budget | Request continues normally |
|
||||
| `BeforeRequestResult.cancel(msg)` | Budget exhausted | Raises `RequestCancelledError` |
|
||||
| `BeforeRequestResult.throttle(delay_ms=N)` | Near limit | Delays request by N ms |
|
||||
| `BeforeRequestResult.degrade(to_model, reason)` | Approaching limit | Switches to cheaper model |
|
||||
|
||||
## Key Points
|
||||
|
||||
- `emit_metric` is **required** - use `create_console_emitter(pretty=True)` for dev
|
||||
- `before_request` callback enables budget enforcement
|
||||
- Always wrap model calls in `try/except RequestCancelledError`
|
||||
- Call `uninstrument()` on exit to flush remaining metrics
|
||||
- Control agent connects automatically when `api_key` + `server_url` are provided
|
||||
- Google Gemini support works automatically via gRPC client instrumentation
|
||||
|
||||
## Documentation
|
||||
|
||||
Full docs: [https://pypi.org/project/aden-py](https://pypi.org/project/aden-py/)
|
||||
@@ -1,164 +0,0 @@
|
||||
Quick reference for integrating Aden LLM observability & cost control into Python agents.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
`.env` file should contain:
|
||||
|
||||
```
|
||||
OPENAI_API_KEY=sk-xxx # or ANTHROPIC_API_KEY, GOOGLE_API_KEY
|
||||
ADEN_API_URL=https://hive.adenhq.com
|
||||
ADEN_API_KEY=your-aden-api-key
|
||||
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
pip install aden-py python-dotenv
|
||||
|
||||
```
|
||||
|
||||
## Basic Setup (3 Steps)
|
||||
|
||||
### 1. Import and Load Environment
|
||||
|
||||
```python
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv()
|
||||
|
||||
from aden import (
|
||||
instrument,
|
||||
uninstrument,
|
||||
MeterOptions,
|
||||
create_console_emitter,
|
||||
BeforeRequestResult,
|
||||
RequestCancelledError,
|
||||
)
|
||||
|
||||
```
|
||||
|
||||
### 2. Define Budget Check Callback
|
||||
|
||||
```python
|
||||
def budget_check(params, context):
|
||||
"""Enforce budget limits before each LLM request."""
|
||||
budget_info = getattr(context, 'budget', None)
|
||||
|
||||
if budget_info and budget_info.get('exhausted', False):
|
||||
return BeforeRequestResult.cancel("Budget exhausted")
|
||||
|
||||
if budget_info and budget_info.get('percent_used', 0) >= 95:
|
||||
return BeforeRequestResult.throttle(delay_ms=2000)
|
||||
|
||||
if budget_info and budget_info.get('percent_used', 0) >= 80:
|
||||
return BeforeRequestResult.degrade(to_model="gpt-4o-mini", reason="Approaching limit")
|
||||
|
||||
return BeforeRequestResult.proceed()
|
||||
|
||||
```
|
||||
|
||||
### 3. Initialize Aden (at startup)
|
||||
|
||||
```python
|
||||
instrument(MeterOptions(
|
||||
api_key=os.environ.get("ADEN_API_KEY"),
|
||||
server_url=os.environ.get("ADEN_API_URL"),
|
||||
emit_metric=create_console_emitter(pretty=True),
|
||||
on_alert=lambda alert: print(f"[Aden {alert.level}] {alert.message}"),
|
||||
before_request=budget_check,
|
||||
))
|
||||
|
||||
```
|
||||
|
||||
### 4. Handle Budget Errors in Your Agent
|
||||
|
||||
```python
|
||||
def run_agent(user_input: str):
|
||||
try:
|
||||
# Your agent logic here
|
||||
result = graph.invoke({"messages": [{"role": "user", "content": user_input}]})
|
||||
return result["messages"][-1].content
|
||||
except RequestCancelledError as e:
|
||||
return f"Sorry, you have used up your allowance. {e}"
|
||||
|
||||
```
|
||||
|
||||
### 5. Cleanup (on exit)
|
||||
|
||||
```python
|
||||
uninstrument()
|
||||
|
||||
```
|
||||
|
||||
## Complete Template
|
||||
|
||||
```python
|
||||
"""Agent with Aden instrumentation"""
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv()
|
||||
|
||||
from aden import (
|
||||
instrument, uninstrument, MeterOptions,
|
||||
create_console_emitter, BeforeRequestResult, RequestCancelledError,
|
||||
)
|
||||
|
||||
# Budget enforcement callback
|
||||
def budget_check(params, context):
|
||||
budget_info = getattr(context, 'budget', None)
|
||||
if budget_info and budget_info.get('exhausted', False):
|
||||
return BeforeRequestResult.cancel("Budget exhausted")
|
||||
if budget_info and budget_info.get('percent_used', 0) >= 95:
|
||||
return BeforeRequestResult.throttle(delay_ms=2000)
|
||||
if budget_info and budget_info.get('percent_used', 0) >= 80:
|
||||
return BeforeRequestResult.degrade(to_model="gpt-4o-mini", reason="Approaching limit")
|
||||
return BeforeRequestResult.proceed()
|
||||
|
||||
# Initialize Aden
|
||||
instrument(MeterOptions(
|
||||
api_key=os.environ.get("ADEN_API_KEY"),
|
||||
server_url=os.environ.get("ADEN_API_URL"),
|
||||
emit_metric=create_console_emitter(pretty=True),
|
||||
on_alert=lambda alert: print(f"[Aden {alert.level}] {alert.message}"),
|
||||
before_request=budget_check,
|
||||
))
|
||||
|
||||
# === YOUR AGENT CODE HERE ===
|
||||
|
||||
def run_agent(user_input: str):
|
||||
try:
|
||||
# Your LLM calls here
|
||||
pass
|
||||
except RequestCancelledError as e:
|
||||
return f"Sorry, you have used up your allowance. {e}"
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
# Your main loop
|
||||
pass
|
||||
finally:
|
||||
uninstrument()
|
||||
|
||||
```
|
||||
|
||||
## Budget Actions Reference
|
||||
|
||||
| Action | When | Behavior |
|
||||
| ----------------------------------------------- | ----------------- | ------------------------------ |
|
||||
| `BeforeRequestResult.proceed()` | Within budget | Request continues normally |
|
||||
| `BeforeRequestResult.cancel(msg)` | Budget exhausted | Raises `RequestCancelledError` |
|
||||
| `BeforeRequestResult.throttle(delay_ms=N)` | Near limit | Delays request by N ms |
|
||||
| `BeforeRequestResult.degrade(to_model, reason)` | Approaching limit | Switches to cheaper model |
|
||||
|
||||
## Key Points
|
||||
|
||||
- `emit_metric` is **required** - use `create_console_emitter(pretty=True)` for dev
|
||||
- `before_request` callback enables budget enforcement
|
||||
- Always wrap agent calls in `try/except RequestCancelledError`
|
||||
- Call `uninstrument()` on exit to flush remaining metrics
|
||||
- Control agent connects automatically when `api_key` + `server_url` are provided
|
||||
|
||||
## Documentation
|
||||
|
||||
Full docs: [https://pypi.org/project/aden-py](https://pypi.org/project/aden-py/json)
|
||||
@@ -1,165 +0,0 @@
|
||||
# Aden-py LiveKit Integration Guide
|
||||
|
||||
Quick reference for integrating Aden LLM observability & cost control into LiveKit voice agents.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
`.env` file should contain:
|
||||
|
||||
```
|
||||
OPENAI_API_KEY=sk-xxx
|
||||
ADEN_API_URL=https://hive.adenhq.com
|
||||
ADEN_API_KEY=your-aden-api-key
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
pip install 'aden-py[livekit]' python-dotenv
|
||||
```
|
||||
|
||||
## Setup (4 Steps)
|
||||
|
||||
### 1. Import and Load Environment
|
||||
|
||||
```python
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv()
|
||||
|
||||
from aden import (
|
||||
instrument,
|
||||
MeterOptions,
|
||||
create_console_emitter,
|
||||
BeforeRequestResult,
|
||||
RequestCancelledError,
|
||||
)
|
||||
```
|
||||
|
||||
### 2. Define Budget Check Callback
|
||||
|
||||
```python
|
||||
def budget_check(params, context):
|
||||
"""Enforce budget limits before each LLM request."""
|
||||
budget_info = getattr(context, 'budget', None)
|
||||
|
||||
if budget_info and budget_info.get('exhausted', False):
|
||||
return BeforeRequestResult.cancel("Budget exhausted")
|
||||
|
||||
if budget_info and budget_info.get('percent_used', 0) >= 95:
|
||||
return BeforeRequestResult.throttle(delay_ms=2000)
|
||||
|
||||
if budget_info and budget_info.get('percent_used', 0) >= 80:
|
||||
return BeforeRequestResult.degrade(to_model="gpt-4o-mini", reason="Approaching limit")
|
||||
|
||||
return BeforeRequestResult.proceed()
|
||||
```
|
||||
|
||||
### 3. Create Worker Prewarm Function
|
||||
|
||||
**IMPORTANT:** LiveKit uses multiprocessing. Instrumentation must happen in each worker process, not the main process.
|
||||
|
||||
```python
|
||||
def initialize_aden_in_worker(proc):
|
||||
"""Initialize Aden instrumentation in each worker process."""
|
||||
instrument(MeterOptions(
|
||||
api_key=os.environ.get("ADEN_API_KEY"),
|
||||
server_url=os.environ.get("ADEN_API_URL"),
|
||||
emit_metric=create_console_emitter(pretty=True),
|
||||
on_alert=lambda alert: print(f"[Aden {alert.level}] {alert.message}"),
|
||||
before_request=budget_check,
|
||||
))
|
||||
```
|
||||
|
||||
### 4. Pass Prewarm Function to WorkerOptions
|
||||
|
||||
```python
|
||||
if __name__ == "__main__":
|
||||
agents.cli.run_app(agents.WorkerOptions(
|
||||
entrypoint_fnc=entrypoint,
|
||||
agent_name="my-agent",
|
||||
prewarm_fnc=initialize_aden_in_worker, # <-- This is the key!
|
||||
))
|
||||
```
|
||||
|
||||
## Complete Template
|
||||
|
||||
```python
|
||||
"""LiveKit Voice Agent with Aden instrumentation"""
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv()
|
||||
|
||||
from livekit import agents
|
||||
from livekit.plugins import openai
|
||||
|
||||
from aden import (
|
||||
instrument, MeterOptions, create_console_emitter,
|
||||
BeforeRequestResult, RequestCancelledError,
|
||||
)
|
||||
|
||||
# Budget enforcement callback
|
||||
def budget_check(params, context):
|
||||
budget_info = getattr(context, 'budget', None)
|
||||
if budget_info and budget_info.get('exhausted', False):
|
||||
return BeforeRequestResult.cancel("Budget exhausted")
|
||||
if budget_info and budget_info.get('percent_used', 0) >= 95:
|
||||
return BeforeRequestResult.throttle(delay_ms=2000)
|
||||
if budget_info and budget_info.get('percent_used', 0) >= 80:
|
||||
return BeforeRequestResult.degrade(to_model="gpt-4o-mini", reason="Approaching limit")
|
||||
return BeforeRequestResult.proceed()
|
||||
|
||||
# Worker initialization - runs in each spawned process
|
||||
def initialize_aden_in_worker(proc):
|
||||
instrument(MeterOptions(
|
||||
api_key=os.environ.get("ADEN_API_KEY"),
|
||||
server_url=os.environ.get("ADEN_API_URL"),
|
||||
emit_metric=create_console_emitter(pretty=True),
|
||||
on_alert=lambda alert: print(f"[Aden {alert.level}] {alert.message}"),
|
||||
before_request=budget_check,
|
||||
))
|
||||
|
||||
async def entrypoint(ctx: agents.JobContext):
|
||||
# Your agent logic here
|
||||
session = agents.AgentSession(
|
||||
llm=openai.LLM(model="gpt-4o-mini"),
|
||||
# ...
|
||||
)
|
||||
await session.start(ctx.room)
|
||||
|
||||
if __name__ == "__main__":
|
||||
agents.cli.run_app(agents.WorkerOptions(
|
||||
entrypoint_fnc=entrypoint,
|
||||
agent_name="my-agent",
|
||||
prewarm_fnc=initialize_aden_in_worker,
|
||||
))
|
||||
```
|
||||
|
||||
## Budget Actions Reference
|
||||
|
||||
| Action | When | Behavior |
|
||||
| ----------------------------------------------- | ------------------------ | ------------------------------ |
|
||||
| `BeforeRequestResult.proceed()` | Within budget | Request continues normally |
|
||||
| `BeforeRequestResult.cancel(msg)` | Budget exhausted | Raises `RequestCancelledError` |
|
||||
| `BeforeRequestResult.throttle(delay_ms=N)` | Near limit (95%+) | Delays request by N ms |
|
||||
| `BeforeRequestResult.degrade(to_model, reason)` | Approaching limit (80%+) | Switches to cheaper model |
|
||||
|
||||
## Key Points
|
||||
|
||||
- **Use `prewarm_fnc`** - LiveKit spawns worker processes; instrumentation must happen in each worker
|
||||
- **Don't instrument in main process** - It won't affect the worker processes where LLM calls happen
|
||||
- `emit_metric` is **required** - use `create_console_emitter(pretty=True)` for dev
|
||||
- Control agent connects automatically when `api_key` + `server_url` are provided
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**No metrics showing?**
|
||||
|
||||
- Ensure `prewarm_fnc` is set in `WorkerOptions`
|
||||
- Check that `ADEN_API_KEY` and `ADEN_API_URL` are in your `.env`
|
||||
- Verify you're using `aden-py[livekit]` (with the livekit extra)
|
||||
|
||||
**Metrics in test but not in agent?**
|
||||
|
||||
- LiveKit uses multiprocessing - the main process instrumentation doesn't carry over
|
||||
- The `prewarm_fnc` runs in each worker before your `entrypoint` is called
|
||||
@@ -1,194 +0,0 @@
|
||||
Quick reference for integrating Aden LLM observability & cost control into TypeScript/JavaScript agents.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
`.env` file should contain:
|
||||
|
||||
```
|
||||
OPENAI_API_KEY=sk-xxx {{envVarComment}}
|
||||
ADEN_API_URL={{serverUrl}}
|
||||
ADEN_API_KEY={{apiKey}}
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install aden-ts dotenv
|
||||
|
||||
# Install the LLM SDKs you use
|
||||
npm install openai # For OpenAI
|
||||
npm install @anthropic-ai/sdk # For Anthropic
|
||||
npm install @google/generative-ai # For Google Gemini
|
||||
```
|
||||
|
||||
## Basic Setup
|
||||
|
||||
### 1. Import Aden and SDK (at top of file)
|
||||
|
||||
```typescript
|
||||
import "dotenv/config";
|
||||
import OpenAI from "openai";
|
||||
import {
|
||||
instrument,
|
||||
uninstrument,
|
||||
createConsoleEmitter,
|
||||
RequestCancelledError,
|
||||
} from "aden-ts";
|
||||
import type { BeforeRequestContext, BeforeRequestResult } from "aden-ts";
|
||||
```
|
||||
|
||||
### 2. Define Before Request Callback (optional)
|
||||
|
||||
```typescript
|
||||
// Custom logic before each LLM request
|
||||
// Budget enforcement is handled server-side by the control agent
|
||||
function beforeRequest(
|
||||
_params: Record<string, unknown>,
|
||||
context: BeforeRequestContext
|
||||
): BeforeRequestResult {
|
||||
console.log(`[Aden] Request to model: ${context.model}`);
|
||||
return { action: "proceed" };
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Initialize Aden (at startup, BEFORE using SDK)
|
||||
|
||||
```typescript
|
||||
await instrument({
|
||||
apiKey: process.env.ADEN_API_KEY,
|
||||
serverUrl: process.env.ADEN_API_URL,
|
||||
emitMetric: createConsoleEmitter({ pretty: true }),
|
||||
onAlert: (alert: { level: string; message: string }) =>
|
||||
console.log(`[Aden ${alert.level}] ${alert.message}`),
|
||||
beforeRequest,
|
||||
sdks: { OpenAI },
|
||||
});
|
||||
```
|
||||
|
||||
### 4. Handle Budget Errors in Your Agent
|
||||
|
||||
```typescript
|
||||
async function runAgent(userInput: string): Promise<string> {
|
||||
try {
|
||||
const openai = new OpenAI();
|
||||
const response = await openai.chat.completions.create({
|
||||
model: "gpt-4o",
|
||||
messages: [{ role: "user", content: userInput }],
|
||||
});
|
||||
return response.choices[0]?.message?.content ?? "";
|
||||
} catch (e) {
|
||||
if (e instanceof RequestCancelledError) {
|
||||
return `Sorry, your budget has been exhausted. ${e.message}`;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Cleanup (on exit)
|
||||
|
||||
```typescript
|
||||
await uninstrument();
|
||||
```
|
||||
|
||||
## Complete Template
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* Agent with Aden instrumentation
|
||||
*/
|
||||
import "dotenv/config";
|
||||
import OpenAI from "openai";
|
||||
import {
|
||||
instrument,
|
||||
uninstrument,
|
||||
createConsoleEmitter,
|
||||
RequestCancelledError,
|
||||
} from "aden-ts";
|
||||
import type { BeforeRequestContext, BeforeRequestResult } from "aden-ts";
|
||||
|
||||
// Before request callback (optional)
|
||||
function beforeRequest(
|
||||
_params: Record<string, unknown>,
|
||||
context: BeforeRequestContext
|
||||
): BeforeRequestResult {
|
||||
console.log(`[Aden] Request to model: ${context.model}`);
|
||||
return { action: "proceed" };
|
||||
}
|
||||
|
||||
// Initialize Aden FIRST
|
||||
await instrument({
|
||||
apiKey: process.env.ADEN_API_KEY,
|
||||
serverUrl: process.env.ADEN_API_URL,
|
||||
emitMetric: createConsoleEmitter({ pretty: true }),
|
||||
onAlert: (alert: { level: string; message: string }) =>
|
||||
console.log(`[Aden ${alert.level}] ${alert.message}`),
|
||||
beforeRequest,
|
||||
sdks: { OpenAI },
|
||||
});
|
||||
|
||||
// === YOUR AGENT CODE HERE ===
|
||||
|
||||
async function runAgent(userInput: string): Promise<string> {
|
||||
try {
|
||||
const openai = new OpenAI();
|
||||
const response = await openai.chat.completions.create({
|
||||
model: "gpt-4o",
|
||||
messages: [{ role: "user", content: userInput }],
|
||||
});
|
||||
return response.choices[0]?.message?.content ?? "";
|
||||
} catch (e) {
|
||||
if (e instanceof RequestCancelledError) {
|
||||
return `Sorry, your budget has been exhausted. ${e.message}`;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
// Main entry point
|
||||
async function main() {
|
||||
try {
|
||||
const result = await runAgent("Hello, world!");
|
||||
console.log(result);
|
||||
} finally {
|
||||
await uninstrument();
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
```
|
||||
|
||||
## BeforeRequestContext Reference
|
||||
|
||||
The `context` parameter in `beforeRequest` contains:
|
||||
|
||||
| Field | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| `model` | string | Model being used for this request |
|
||||
| `stream` | boolean | Whether this is a streaming request |
|
||||
| `spanId` | string | Generated span ID (OTel standard) |
|
||||
| `traceId` | string | Trace ID grouping related operations |
|
||||
| `timestamp` | Date | When the request was initiated |
|
||||
| `metadata` | Record<string, unknown> | Custom metadata (optional) |
|
||||
|
||||
## BeforeRequestResult Actions
|
||||
|
||||
| Action | Usage | Behavior |
|
||||
| --- | --- | --- |
|
||||
| `{ action: "proceed" }` | Allow request | Request continues normally |
|
||||
| `{ action: "cancel", reason: "..." }` | Block request | Throws `RequestCancelledError` |
|
||||
| `{ action: "throttle", delayMs: N }` | Rate limit | Delays request by N ms |
|
||||
| `{ action: "degrade", toModel: "...", reason: "..." }` | Downgrade | Switches to specified model |
|
||||
|
||||
## Key Points
|
||||
|
||||
- Module name is `aden-ts` (not `aden`)
|
||||
- `emitMetric` is **required** - use `createConsoleEmitter({ pretty: true })` for dev
|
||||
- Budget enforcement is handled **server-side** by the control agent
|
||||
- Always wrap agent calls in `try/catch` for `RequestCancelledError`
|
||||
- Call `await uninstrument()` on exit to flush remaining metrics
|
||||
- Control agent connects automatically when `apiKey` + `serverUrl` are provided
|
||||
|
||||
## Documentation
|
||||
|
||||
Full docs: [https://www.npmjs.com/package/aden-ts](https://www.npmjs.com/package/aden-ts)
|
||||
@@ -1,297 +0,0 @@
|
||||
Quick reference for integrating Aden LLM observability & cost control into TypeScript/JavaScript agents.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
`.env` file should contain:
|
||||
|
||||
```
|
||||
OPENAI_API_KEY=sk-xxx {{envVarComment}}
|
||||
ADEN_API_URL={{serverUrl}}
|
||||
ADEN_API_KEY={{apiKey}}
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install aden-ts dotenv
|
||||
|
||||
# Install the LLM SDKs you use
|
||||
npm install openai # For OpenAI
|
||||
npm install @anthropic-ai/sdk # For Anthropic
|
||||
npm install @google/generative-ai # For Google Gemini
|
||||
```
|
||||
|
||||
## Basic Setup
|
||||
|
||||
### 1. Import Aden and SDK (at top of file)
|
||||
|
||||
```typescript
|
||||
import "dotenv/config";
|
||||
import OpenAI from "openai";
|
||||
import {
|
||||
instrument,
|
||||
uninstrument,
|
||||
createConsoleEmitter,
|
||||
RequestCancelledError,
|
||||
} from "aden-ts";
|
||||
import type { BeforeRequestContext, BeforeRequestResult } from "aden-ts";
|
||||
```
|
||||
|
||||
### 2. Define Before Request Callback (optional)
|
||||
|
||||
```typescript
|
||||
// Custom logic before each LLM request
|
||||
// Budget enforcement is handled server-side by the control agent
|
||||
function beforeRequest(
|
||||
_params: Record<string, unknown>,
|
||||
context: BeforeRequestContext
|
||||
): BeforeRequestResult {
|
||||
console.log(`[Aden] Request to model: ${context.model}`);
|
||||
return { action: "proceed" };
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Initialize Aden (at startup, BEFORE using SDK)
|
||||
|
||||
```typescript
|
||||
await instrument({
|
||||
apiKey: process.env.ADEN_API_KEY,
|
||||
serverUrl: process.env.ADEN_API_URL,
|
||||
emitMetric: createConsoleEmitter({ pretty: true }),
|
||||
onAlert: (alert: { level: string; message: string }) =>
|
||||
console.log(`[Aden ${alert.level}] ${alert.message}`),
|
||||
beforeRequest,
|
||||
sdks: { OpenAI },
|
||||
});
|
||||
```
|
||||
|
||||
### 4. Handle Budget Errors in Your Agent
|
||||
|
||||
```typescript
|
||||
async function runAgent(userInput: string): Promise<string> {
|
||||
try {
|
||||
const openai = new OpenAI();
|
||||
const response = await openai.chat.completions.create({
|
||||
model: "gpt-4o",
|
||||
messages: [{ role: "user", content: userInput }],
|
||||
});
|
||||
return response.choices[0]?.message?.content ?? "";
|
||||
} catch (e) {
|
||||
if (e instanceof RequestCancelledError) {
|
||||
return `Sorry, your budget has been exhausted. ${e.message}`;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Cleanup (on exit)
|
||||
|
||||
```typescript
|
||||
await uninstrument();
|
||||
```
|
||||
|
||||
## Complete Template (Direct SDK Usage)
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* Agent with Aden instrumentation - Direct SDK usage
|
||||
*/
|
||||
import "dotenv/config";
|
||||
import OpenAI from "openai";
|
||||
import {
|
||||
instrument,
|
||||
uninstrument,
|
||||
createConsoleEmitter,
|
||||
RequestCancelledError,
|
||||
} from "aden-ts";
|
||||
import type { BeforeRequestContext, BeforeRequestResult } from "aden-ts";
|
||||
|
||||
// Before request callback (optional)
|
||||
function beforeRequest(
|
||||
_params: Record<string, unknown>,
|
||||
context: BeforeRequestContext
|
||||
): BeforeRequestResult {
|
||||
console.log(`[Aden] Request to model: ${context.model}`);
|
||||
return { action: "proceed" };
|
||||
}
|
||||
|
||||
// Initialize Aden FIRST
|
||||
await instrument({
|
||||
apiKey: process.env.ADEN_API_KEY,
|
||||
serverUrl: process.env.ADEN_API_URL,
|
||||
emitMetric: createConsoleEmitter({ pretty: true }),
|
||||
onAlert: (alert: { level: string; message: string }) =>
|
||||
console.log(`[Aden ${alert.level}] ${alert.message}`),
|
||||
beforeRequest,
|
||||
sdks: { OpenAI },
|
||||
});
|
||||
|
||||
// === YOUR AGENT CODE HERE ===
|
||||
|
||||
async function runAgent(userInput: string): Promise<string> {
|
||||
try {
|
||||
const openai = new OpenAI();
|
||||
const response = await openai.chat.completions.create({
|
||||
model: "gpt-4o",
|
||||
messages: [{ role: "user", content: userInput }],
|
||||
});
|
||||
return response.choices[0]?.message?.content ?? "";
|
||||
} catch (e) {
|
||||
if (e instanceof RequestCancelledError) {
|
||||
return `Sorry, your budget has been exhausted. ${e.message}`;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
// Main entry point
|
||||
async function main() {
|
||||
try {
|
||||
const result = await runAgent("Hello, world!");
|
||||
console.log(result);
|
||||
} finally {
|
||||
await uninstrument();
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
```
|
||||
|
||||
## LangChain / LangGraph Integration
|
||||
|
||||
When using LangChain or LangGraph, you **MUST** use dynamic imports to ensure instrumentation is applied before LangChain loads the SDK.
|
||||
|
||||
### Critical: SDK Version Matching
|
||||
|
||||
LangChain bundles its own SDK dependencies. To ensure instrumentation works, your SDK version must match LangChain's:
|
||||
|
||||
```bash
|
||||
# Check what version LangChain uses
|
||||
cat node_modules/@langchain/anthropic/node_modules/@anthropic-ai/sdk/package.json | grep version
|
||||
|
||||
# Update your package.json to match that version
|
||||
# e.g., "@anthropic-ai/sdk": "^0.65.0"
|
||||
|
||||
# Reinstall to dedupe
|
||||
rm -rf node_modules package-lock.json && npm install
|
||||
|
||||
# Verify no nested SDK (should show "No such file")
|
||||
ls node_modules/@langchain/anthropic/node_modules 2>/dev/null || echo "OK: SDK is shared"
|
||||
```
|
||||
|
||||
### LangChain Template
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* LangGraph Agent with Aden instrumentation
|
||||
* Key: Use dynamic imports AFTER instrument()
|
||||
*/
|
||||
import "dotenv/config";
|
||||
import Anthropic from "@anthropic-ai/sdk";
|
||||
import {
|
||||
instrument,
|
||||
uninstrument,
|
||||
createConsoleEmitter,
|
||||
RequestCancelledError,
|
||||
} from "aden-ts";
|
||||
import type { BeforeRequestContext, BeforeRequestResult } from "aden-ts";
|
||||
|
||||
function beforeRequest(
|
||||
_params: Record<string, unknown>,
|
||||
context: BeforeRequestContext
|
||||
): BeforeRequestResult {
|
||||
console.log(`[Aden] Request to model: ${context.model}`);
|
||||
return { action: "proceed" };
|
||||
}
|
||||
|
||||
async function main() {
|
||||
// 1. Initialize Aden FIRST (before any LangChain imports)
|
||||
await instrument({
|
||||
apiKey: process.env.ADEN_API_KEY,
|
||||
serverUrl: process.env.ADEN_API_URL,
|
||||
emitMetric: createConsoleEmitter({ pretty: true }),
|
||||
onAlert: (alert: { level: string; message: string }) =>
|
||||
console.log(`[Aden ${alert.level}] ${alert.message}`),
|
||||
beforeRequest,
|
||||
sdks: { Anthropic },
|
||||
});
|
||||
|
||||
// 2. Dynamic imports AFTER instrumentation
|
||||
const { ChatAnthropic } = await import("@langchain/anthropic");
|
||||
const { HumanMessage } = await import("@langchain/core/messages");
|
||||
// ... other LangChain imports
|
||||
|
||||
// 3. Now create your LangChain components
|
||||
const model = new ChatAnthropic({
|
||||
model: "claude-sonnet-4-20250514",
|
||||
temperature: 0,
|
||||
});
|
||||
|
||||
try {
|
||||
// Your agent logic here
|
||||
const response = await model.invoke([new HumanMessage("Hello!")]);
|
||||
console.log(response.content);
|
||||
} catch (error) {
|
||||
if (error instanceof RequestCancelledError) {
|
||||
console.log(`Budget exhausted: ${error.message}`);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
} finally {
|
||||
await uninstrument();
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
```
|
||||
|
||||
## BeforeRequestContext Reference
|
||||
|
||||
The `context` parameter in `beforeRequest` contains:
|
||||
|
||||
| Field | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| `model` | string | Model being used for this request |
|
||||
| `stream` | boolean | Whether this is a streaming request |
|
||||
| `spanId` | string | Generated span ID (OTel standard) |
|
||||
| `traceId` | string | Trace ID grouping related operations |
|
||||
| `timestamp` | Date | When the request was initiated |
|
||||
| `metadata` | Record<string, unknown> | Custom metadata (optional) |
|
||||
|
||||
## BeforeRequestResult Actions
|
||||
|
||||
| Action | Usage | Behavior |
|
||||
| --- | --- | --- |
|
||||
| `{ action: "proceed" }` | Allow request | Request continues normally |
|
||||
| `{ action: "cancel", reason: "..." }` | Block request | Throws `RequestCancelledError` |
|
||||
| `{ action: "throttle", delayMs: N }` | Rate limit | Delays request by N ms |
|
||||
| `{ action: "degrade", toModel: "...", reason: "..." }` | Downgrade | Switches to specified model |
|
||||
|
||||
## Key Points
|
||||
|
||||
- Module name is `aden-ts` (not `aden`)
|
||||
- `emitMetric` is **required** - use `createConsoleEmitter({ pretty: true })` for dev
|
||||
- Budget enforcement is handled **server-side** by the control agent
|
||||
- Always wrap agent calls in `try/catch` for `RequestCancelledError`
|
||||
- Call `await uninstrument()` on exit to flush remaining metrics
|
||||
- Control agent connects automatically when `apiKey` + `serverUrl` are provided
|
||||
- **LangChain users**: Must use dynamic imports and match SDK versions
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### No metrics being captured
|
||||
|
||||
1. **Check SDK version match**: Run `npm ls @anthropic-ai/sdk` - should show only ONE version
|
||||
2. **Use dynamic imports**: Import LangChain modules AFTER `instrument()` is called
|
||||
3. **Verify instrumentation**: Look for `[aden] Instrumented: anthropic + control agent` at startup
|
||||
|
||||
### RequestCancelledError not thrown
|
||||
|
||||
Budget enforcement is server-side. Ensure:
|
||||
- `ADEN_API_KEY` and `ADEN_API_URL` are set correctly
|
||||
- Control agent connection is established (check startup logs)
|
||||
|
||||
## Documentation
|
||||
|
||||
Full docs: [https://www.npmjs.com/package/aden-ts](https://www.npmjs.com/package/aden-ts)
|
||||
@@ -1,164 +0,0 @@
|
||||
Quick reference for integrating Aden LLM observability & cost control into Python agents.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
`.env` file should contain:
|
||||
|
||||
```
|
||||
OPENAI_API_KEY=sk-xxx {{envVarComment}}
|
||||
ADEN_API_URL={{serverUrl}}
|
||||
ADEN_API_KEY={{apiKey}}
|
||||
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
pip install aden-py python-dotenv
|
||||
|
||||
```
|
||||
|
||||
## Basic Setup (3 Steps)
|
||||
|
||||
### 1. Import and Load Environment
|
||||
|
||||
```python
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv()
|
||||
|
||||
from aden import (
|
||||
instrument,
|
||||
uninstrument,
|
||||
MeterOptions,
|
||||
create_console_emitter,
|
||||
BeforeRequestResult,
|
||||
RequestCancelledError,
|
||||
)
|
||||
|
||||
```
|
||||
|
||||
### 2. Define Budget Check Callback
|
||||
|
||||
```python
|
||||
def budget_check(params, context):
|
||||
"""Enforce budget limits before each LLM request."""
|
||||
budget_info = getattr(context, 'budget', None)
|
||||
|
||||
if budget_info and budget_info.get('exhausted', False):
|
||||
return BeforeRequestResult.cancel("Budget exhausted")
|
||||
|
||||
if budget_info and budget_info.get('percent_used', 0) >= 95:
|
||||
return BeforeRequestResult.throttle(delay_ms=2000)
|
||||
|
||||
if budget_info and budget_info.get('percent_used', 0) >= 80:
|
||||
return BeforeRequestResult.degrade(to_model="gpt-4o-mini", reason="Approaching limit")
|
||||
|
||||
return BeforeRequestResult.proceed()
|
||||
|
||||
```
|
||||
|
||||
### 3. Initialize Aden (at startup)
|
||||
|
||||
```python
|
||||
instrument(MeterOptions(
|
||||
api_key=os.environ.get("ADEN_API_KEY"),
|
||||
server_url=os.environ.get("ADEN_API_URL"),
|
||||
emit_metric=create_console_emitter(pretty=True),
|
||||
on_alert=lambda alert: print(f"[Aden {alert.level}] {alert.message}"),
|
||||
before_request=budget_check,
|
||||
))
|
||||
|
||||
```
|
||||
|
||||
### 4. Handle Budget Errors in Your Agent
|
||||
|
||||
```python
|
||||
def run_agent(user_input: str):
|
||||
try:
|
||||
# Your agent logic here
|
||||
result = graph.invoke({"messages": [{"role": "user", "content": user_input}]})
|
||||
return result["messages"][-1].content
|
||||
except RequestCancelledError as e:
|
||||
return f"Sorry, you have used up your allowance. {e}"
|
||||
|
||||
```
|
||||
|
||||
### 5. Cleanup (on exit)
|
||||
|
||||
```python
|
||||
uninstrument()
|
||||
|
||||
```
|
||||
|
||||
## Complete Template
|
||||
|
||||
```python
|
||||
"""Agent with Aden instrumentation"""
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv()
|
||||
|
||||
from aden import (
|
||||
instrument, uninstrument, MeterOptions,
|
||||
create_console_emitter, BeforeRequestResult, RequestCancelledError,
|
||||
)
|
||||
|
||||
# Budget enforcement callback
|
||||
def budget_check(params, context):
|
||||
budget_info = getattr(context, 'budget', None)
|
||||
if budget_info and budget_info.get('exhausted', False):
|
||||
return BeforeRequestResult.cancel("Budget exhausted")
|
||||
if budget_info and budget_info.get('percent_used', 0) >= 95:
|
||||
return BeforeRequestResult.throttle(delay_ms=2000)
|
||||
if budget_info and budget_info.get('percent_used', 0) >= 80:
|
||||
return BeforeRequestResult.degrade(to_model="gpt-4o-mini", reason="Approaching limit")
|
||||
return BeforeRequestResult.proceed()
|
||||
|
||||
# Initialize Aden
|
||||
instrument(MeterOptions(
|
||||
api_key=os.environ.get("ADEN_API_KEY"),
|
||||
server_url=os.environ.get("ADEN_API_URL"),
|
||||
emit_metric=create_console_emitter(pretty=True),
|
||||
on_alert=lambda alert: print(f"[Aden {alert.level}] {alert.message}"),
|
||||
before_request=budget_check,
|
||||
))
|
||||
|
||||
# === YOUR AGENT CODE HERE ===
|
||||
|
||||
def run_agent(user_input: str):
|
||||
try:
|
||||
# Your LLM calls here
|
||||
pass
|
||||
except RequestCancelledError as e:
|
||||
return f"Sorry, you have used up your allowance. {e}"
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
# Your main loop
|
||||
pass
|
||||
finally:
|
||||
uninstrument()
|
||||
|
||||
```
|
||||
|
||||
## Budget Actions Reference
|
||||
|
||||
| Action | When | Behavior |
|
||||
| --- | --- | --- |
|
||||
| `BeforeRequestResult.proceed()` | Within budget | Request continues normally |
|
||||
| `BeforeRequestResult.cancel(msg)` | Budget exhausted | Raises `RequestCancelledError` |
|
||||
| `BeforeRequestResult.throttle(delay_ms=N)` | Near limit | Delays request by N ms |
|
||||
| `BeforeRequestResult.degrade(to_model, reason)` | Approaching limit | Switches to cheaper model |
|
||||
|
||||
## Key Points
|
||||
|
||||
- `emit_metric` is **required** - use `create_console_emitter(pretty=True)` for dev
|
||||
- `before_request` callback enables budget enforcement
|
||||
- Always wrap agent calls in `try/except RequestCancelledError`
|
||||
- Call `uninstrument()` on exit to flush remaining metrics
|
||||
- Control agent connects automatically when `api_key` + `server_url` are provided
|
||||
|
||||
## Documentation
|
||||
|
||||
Full docs: [https://pypi.org/project/aden-py](https://pypi.org/project/aden-py/)
|
||||
@@ -1,191 +0,0 @@
|
||||
Quick reference for integrating Aden LLM observability & cost control into LangFlow applications.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
`.env` file should contain:
|
||||
|
||||
```
|
||||
OPENAI_API_KEY=sk-xxx {{envVarComment}}
|
||||
ADEN_API_URL={{serverUrl}}
|
||||
ADEN_API_KEY={{apiKey}}
|
||||
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
pip install aden-py langflow python-dotenv
|
||||
|
||||
```
|
||||
|
||||
## Basic Setup (3 Steps)
|
||||
|
||||
### 1. Import and Load Environment
|
||||
|
||||
```python
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv()
|
||||
|
||||
from aden import (
|
||||
instrument,
|
||||
uninstrument,
|
||||
MeterOptions,
|
||||
create_console_emitter,
|
||||
BeforeRequestResult,
|
||||
RequestCancelledError,
|
||||
)
|
||||
|
||||
```
|
||||
|
||||
### 2. Define Budget Check Callback
|
||||
|
||||
```python
|
||||
def budget_check(params, context):
|
||||
"""Enforce budget limits before each LLM request."""
|
||||
budget_info = getattr(context, 'budget', None)
|
||||
|
||||
if budget_info and budget_info.get('exhausted', False):
|
||||
return BeforeRequestResult.cancel("Budget exhausted")
|
||||
|
||||
if budget_info and budget_info.get('percent_used', 0) >= 95:
|
||||
return BeforeRequestResult.throttle(delay_ms=2000)
|
||||
|
||||
if budget_info and budget_info.get('percent_used', 0) >= 80:
|
||||
return BeforeRequestResult.degrade(to_model="gpt-4o-mini", reason="Approaching limit")
|
||||
|
||||
return BeforeRequestResult.proceed()
|
||||
|
||||
```
|
||||
|
||||
### 3. Initialize Aden (at startup)
|
||||
|
||||
```python
|
||||
instrument(MeterOptions(
|
||||
api_key=os.environ.get("ADEN_API_KEY"),
|
||||
server_url=os.environ.get("ADEN_API_URL"),
|
||||
emit_metric=create_console_emitter(pretty=True),
|
||||
on_alert=lambda alert: print(f"[Aden {alert.level}] {alert.message}"),
|
||||
before_request=budget_check,
|
||||
))
|
||||
|
||||
```
|
||||
|
||||
### 4. Use LangFlow Components
|
||||
|
||||
```python
|
||||
from langflow.components.models import LanguageModelComponent
|
||||
|
||||
comp = LanguageModelComponent()
|
||||
comp.set_attributes({
|
||||
"provider": "Google", # or "OpenAI"
|
||||
"model_name": "gemini-2.0-flash",
|
||||
"api_key": os.getenv("GOOGLE_API_KEY"),
|
||||
"stream": False,
|
||||
})
|
||||
|
||||
model = comp.build_model()
|
||||
|
||||
try:
|
||||
response = model.invoke("Hello!")
|
||||
print(response.content)
|
||||
except RequestCancelledError as e:
|
||||
print(f"Budget exceeded: {e}")
|
||||
|
||||
```
|
||||
|
||||
### 5. Cleanup (on exit)
|
||||
|
||||
```python
|
||||
uninstrument()
|
||||
|
||||
```
|
||||
|
||||
## Complete Template
|
||||
|
||||
```python
|
||||
"""LangFlow with Aden instrumentation"""
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv()
|
||||
|
||||
from aden import (
|
||||
instrument, uninstrument, MeterOptions,
|
||||
create_console_emitter, BeforeRequestResult, RequestCancelledError,
|
||||
)
|
||||
|
||||
# Budget enforcement callback
|
||||
def budget_check(params, context):
|
||||
budget_info = getattr(context, 'budget', None)
|
||||
if budget_info and budget_info.get('exhausted', False):
|
||||
return BeforeRequestResult.cancel("Budget exhausted")
|
||||
if budget_info and budget_info.get('percent_used', 0) >= 95:
|
||||
return BeforeRequestResult.throttle(delay_ms=2000)
|
||||
if budget_info and budget_info.get('percent_used', 0) >= 80:
|
||||
return BeforeRequestResult.degrade(to_model="gpt-4o-mini", reason="Approaching limit")
|
||||
return BeforeRequestResult.proceed()
|
||||
|
||||
# Initialize Aden
|
||||
instrument(MeterOptions(
|
||||
api_key=os.environ.get("ADEN_API_KEY"),
|
||||
server_url=os.environ.get("ADEN_API_URL"),
|
||||
emit_metric=create_console_emitter(pretty=True),
|
||||
on_alert=lambda alert: print(f"[Aden {alert.level}] {alert.message}"),
|
||||
before_request=budget_check,
|
||||
))
|
||||
|
||||
# === YOUR LANGFLOW CODE HERE ===
|
||||
|
||||
from langflow.components.models import LanguageModelComponent
|
||||
|
||||
def run_model(user_input: str):
|
||||
try:
|
||||
comp = LanguageModelComponent()
|
||||
comp.set_attributes({
|
||||
"provider": "Google",
|
||||
"model_name": "gemini-2.0-flash",
|
||||
"api_key": os.getenv("GOOGLE_API_KEY"),
|
||||
"stream": False,
|
||||
})
|
||||
model = comp.build_model()
|
||||
return model.invoke(user_input).content
|
||||
except RequestCancelledError as e:
|
||||
return f"Sorry, you have used up your allowance. {e}"
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
print(run_model("Say hello!"))
|
||||
finally:
|
||||
uninstrument()
|
||||
|
||||
```
|
||||
|
||||
## Supported Providers
|
||||
|
||||
| Provider | Model Example | Notes |
|
||||
| --- | --- | --- |
|
||||
| OpenAI | gpt-4o, gpt-4o-mini | Direct SDK instrumentation |
|
||||
| Google | gemini-2.0-flash | Uses gRPC client instrumentation |
|
||||
| Anthropic | claude-3-opus | Direct SDK instrumentation |
|
||||
|
||||
## Budget Actions Reference
|
||||
|
||||
| Action | When | Behavior |
|
||||
| --- | --- | --- |
|
||||
| `BeforeRequestResult.proceed()` | Within budget | Request continues normally |
|
||||
| `BeforeRequestResult.cancel(msg)` | Budget exhausted | Raises `RequestCancelledError` |
|
||||
| `BeforeRequestResult.throttle(delay_ms=N)` | Near limit | Delays request by N ms |
|
||||
| `BeforeRequestResult.degrade(to_model, reason)` | Approaching limit | Switches to cheaper model |
|
||||
|
||||
## Key Points
|
||||
|
||||
- `emit_metric` is **required** - use `create_console_emitter(pretty=True)` for dev
|
||||
- `before_request` callback enables budget enforcement
|
||||
- Always wrap model calls in `try/except RequestCancelledError`
|
||||
- Call `uninstrument()` on exit to flush remaining metrics
|
||||
- Control agent connects automatically when `api_key` + `server_url` are provided
|
||||
- Google Gemini support works automatically via gRPC client instrumentation
|
||||
|
||||
## Documentation
|
||||
|
||||
Full docs: [https://pypi.org/project/aden-py](https://pypi.org/project/aden-py/)
|
||||
@@ -1,164 +0,0 @@
|
||||
Quick reference for integrating Aden LLM observability & cost control into Python agents.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
`.env` file should contain:
|
||||
|
||||
```
|
||||
OPENAI_API_KEY=sk-xxx {{envVarComment}}
|
||||
ADEN_API_URL={{serverUrl}}
|
||||
ADEN_API_KEY={{apiKey}}
|
||||
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
pip install aden-py python-dotenv
|
||||
|
||||
```
|
||||
|
||||
## Basic Setup (3 Steps)
|
||||
|
||||
### 1. Import and Load Environment
|
||||
|
||||
```python
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv()
|
||||
|
||||
from aden import (
|
||||
instrument,
|
||||
uninstrument,
|
||||
MeterOptions,
|
||||
create_console_emitter,
|
||||
BeforeRequestResult,
|
||||
RequestCancelledError,
|
||||
)
|
||||
|
||||
```
|
||||
|
||||
### 2. Define Budget Check Callback
|
||||
|
||||
```python
|
||||
def budget_check(params, context):
|
||||
"""Enforce budget limits before each LLM request."""
|
||||
budget_info = getattr(context, 'budget', None)
|
||||
|
||||
if budget_info and budget_info.get('exhausted', False):
|
||||
return BeforeRequestResult.cancel("Budget exhausted")
|
||||
|
||||
if budget_info and budget_info.get('percent_used', 0) >= 95:
|
||||
return BeforeRequestResult.throttle(delay_ms=2000)
|
||||
|
||||
if budget_info and budget_info.get('percent_used', 0) >= 80:
|
||||
return BeforeRequestResult.degrade(to_model="gpt-4o-mini", reason="Approaching limit")
|
||||
|
||||
return BeforeRequestResult.proceed()
|
||||
|
||||
```
|
||||
|
||||
### 3. Initialize Aden (at startup)
|
||||
|
||||
```python
|
||||
instrument(MeterOptions(
|
||||
api_key=os.environ.get("ADEN_API_KEY"),
|
||||
server_url=os.environ.get("ADEN_API_URL"),
|
||||
emit_metric=create_console_emitter(pretty=True),
|
||||
on_alert=lambda alert: print(f"[Aden {alert.level}] {alert.message}"),
|
||||
before_request=budget_check,
|
||||
))
|
||||
|
||||
```
|
||||
|
||||
### 4. Handle Budget Errors in Your Agent
|
||||
|
||||
```python
|
||||
def run_agent(user_input: str):
|
||||
try:
|
||||
# Your agent logic here
|
||||
result = graph.invoke({"messages": [{"role": "user", "content": user_input}]})
|
||||
return result["messages"][-1].content
|
||||
except RequestCancelledError as e:
|
||||
return f"Sorry, you have used up your allowance. {e}"
|
||||
|
||||
```
|
||||
|
||||
### 5. Cleanup (on exit)
|
||||
|
||||
```python
|
||||
uninstrument()
|
||||
|
||||
```
|
||||
|
||||
## Complete Template
|
||||
|
||||
```python
|
||||
"""Agent with Aden instrumentation"""
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv()
|
||||
|
||||
from aden import (
|
||||
instrument, uninstrument, MeterOptions,
|
||||
create_console_emitter, BeforeRequestResult, RequestCancelledError,
|
||||
)
|
||||
|
||||
# Budget enforcement callback
|
||||
def budget_check(params, context):
|
||||
budget_info = getattr(context, 'budget', None)
|
||||
if budget_info and budget_info.get('exhausted', False):
|
||||
return BeforeRequestResult.cancel("Budget exhausted")
|
||||
if budget_info and budget_info.get('percent_used', 0) >= 95:
|
||||
return BeforeRequestResult.throttle(delay_ms=2000)
|
||||
if budget_info and budget_info.get('percent_used', 0) >= 80:
|
||||
return BeforeRequestResult.degrade(to_model="gpt-4o-mini", reason="Approaching limit")
|
||||
return BeforeRequestResult.proceed()
|
||||
|
||||
# Initialize Aden
|
||||
instrument(MeterOptions(
|
||||
api_key=os.environ.get("ADEN_API_KEY"),
|
||||
server_url=os.environ.get("ADEN_API_URL"),
|
||||
emit_metric=create_console_emitter(pretty=True),
|
||||
on_alert=lambda alert: print(f"[Aden {alert.level}] {alert.message}"),
|
||||
before_request=budget_check,
|
||||
))
|
||||
|
||||
# === YOUR AGENT CODE HERE ===
|
||||
|
||||
def run_agent(user_input: str):
|
||||
try:
|
||||
# Your LLM calls here
|
||||
pass
|
||||
except RequestCancelledError as e:
|
||||
return f"Sorry, you have used up your allowance. {e}"
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
# Your main loop
|
||||
pass
|
||||
finally:
|
||||
uninstrument()
|
||||
|
||||
```
|
||||
|
||||
## Budget Actions Reference
|
||||
|
||||
| Action | When | Behavior |
|
||||
| --- | --- | --- |
|
||||
| `BeforeRequestResult.proceed()` | Within budget | Request continues normally |
|
||||
| `BeforeRequestResult.cancel(msg)` | Budget exhausted | Raises `RequestCancelledError` |
|
||||
| `BeforeRequestResult.throttle(delay_ms=N)` | Near limit | Delays request by N ms |
|
||||
| `BeforeRequestResult.degrade(to_model, reason)` | Approaching limit | Switches to cheaper model |
|
||||
|
||||
## Key Points
|
||||
|
||||
- `emit_metric` is **required** - use `create_console_emitter(pretty=True)` for dev
|
||||
- `before_request` callback enables budget enforcement
|
||||
- Always wrap agent calls in `try/except RequestCancelledError`
|
||||
- Call `uninstrument()` on exit to flush remaining metrics
|
||||
- Control agent connects automatically when `api_key` + `server_url` are provided
|
||||
|
||||
## Documentation
|
||||
|
||||
Full docs: [https://pypi.org/project/aden-py](https://pypi.org/project/aden-py/)
|
||||
@@ -1,162 +0,0 @@
|
||||
# Aden-py LiveKit Integration Guide
|
||||
|
||||
Quick reference for integrating Aden LLM observability & cost control into LiveKit voice agents.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
`.env` file should contain:
|
||||
```
|
||||
OPENAI_API_KEY=sk-xxx
|
||||
ADEN_API_URL={{serverUrl}}
|
||||
ADEN_API_KEY={{apiKey}}
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
pip install 'aden-py[livekit]' python-dotenv
|
||||
```
|
||||
|
||||
## Setup (4 Steps)
|
||||
|
||||
### 1. Import and Load Environment
|
||||
|
||||
```python
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv()
|
||||
|
||||
from aden import (
|
||||
instrument,
|
||||
MeterOptions,
|
||||
create_console_emitter,
|
||||
BeforeRequestResult,
|
||||
RequestCancelledError,
|
||||
)
|
||||
```
|
||||
|
||||
### 2. Define Budget Check Callback
|
||||
|
||||
```python
|
||||
def budget_check(params, context):
|
||||
"""Enforce budget limits before each LLM request."""
|
||||
budget_info = getattr(context, 'budget', None)
|
||||
|
||||
if budget_info and budget_info.get('exhausted', False):
|
||||
return BeforeRequestResult.cancel("Budget exhausted")
|
||||
|
||||
if budget_info and budget_info.get('percent_used', 0) >= 95:
|
||||
return BeforeRequestResult.throttle(delay_ms=2000)
|
||||
|
||||
if budget_info and budget_info.get('percent_used', 0) >= 80:
|
||||
return BeforeRequestResult.degrade(to_model="gpt-4o-mini", reason="Approaching limit")
|
||||
|
||||
return BeforeRequestResult.proceed()
|
||||
```
|
||||
|
||||
### 3. Create Worker Prewarm Function
|
||||
|
||||
**IMPORTANT:** LiveKit uses multiprocessing. Instrumentation must happen in each worker process, not the main process.
|
||||
|
||||
```python
|
||||
def initialize_aden_in_worker(proc):
|
||||
"""Initialize Aden instrumentation in each worker process."""
|
||||
instrument(MeterOptions(
|
||||
api_key=os.environ.get("ADEN_API_KEY"),
|
||||
server_url=os.environ.get("ADEN_API_URL"),
|
||||
emit_metric=create_console_emitter(pretty=True),
|
||||
on_alert=lambda alert: print(f"[Aden {alert.level}] {alert.message}"),
|
||||
before_request=budget_check,
|
||||
))
|
||||
```
|
||||
|
||||
### 4. Pass Prewarm Function to WorkerOptions
|
||||
|
||||
```python
|
||||
if __name__ == "__main__":
|
||||
agents.cli.run_app(agents.WorkerOptions(
|
||||
entrypoint_fnc=entrypoint,
|
||||
agent_name="my-agent",
|
||||
prewarm_fnc=initialize_aden_in_worker, # <-- This is the key!
|
||||
))
|
||||
```
|
||||
|
||||
## Complete Template
|
||||
|
||||
```python
|
||||
"""LiveKit Voice Agent with Aden instrumentation"""
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv()
|
||||
|
||||
from livekit import agents
|
||||
from livekit.plugins import openai
|
||||
|
||||
from aden import (
|
||||
instrument, MeterOptions, create_console_emitter,
|
||||
BeforeRequestResult, RequestCancelledError,
|
||||
)
|
||||
|
||||
# Budget enforcement callback
|
||||
def budget_check(params, context):
|
||||
budget_info = getattr(context, 'budget', None)
|
||||
if budget_info and budget_info.get('exhausted', False):
|
||||
return BeforeRequestResult.cancel("Budget exhausted")
|
||||
if budget_info and budget_info.get('percent_used', 0) >= 95:
|
||||
return BeforeRequestResult.throttle(delay_ms=2000)
|
||||
if budget_info and budget_info.get('percent_used', 0) >= 80:
|
||||
return BeforeRequestResult.degrade(to_model="gpt-4o-mini", reason="Approaching limit")
|
||||
return BeforeRequestResult.proceed()
|
||||
|
||||
# Worker initialization - runs in each spawned process
|
||||
def initialize_aden_in_worker(proc):
|
||||
instrument(MeterOptions(
|
||||
api_key=os.environ.get("ADEN_API_KEY"),
|
||||
server_url=os.environ.get("ADEN_API_URL"),
|
||||
emit_metric=create_console_emitter(pretty=True),
|
||||
on_alert=lambda alert: print(f"[Aden {alert.level}] {alert.message}"),
|
||||
before_request=budget_check,
|
||||
))
|
||||
|
||||
async def entrypoint(ctx: agents.JobContext):
|
||||
# Your agent logic here
|
||||
session = agents.AgentSession(
|
||||
llm=openai.LLM(model="gpt-4o-mini"),
|
||||
# ...
|
||||
)
|
||||
await session.start(ctx.room)
|
||||
|
||||
if __name__ == "__main__":
|
||||
agents.cli.run_app(agents.WorkerOptions(
|
||||
entrypoint_fnc=entrypoint,
|
||||
agent_name="my-agent",
|
||||
prewarm_fnc=initialize_aden_in_worker,
|
||||
))
|
||||
```
|
||||
|
||||
## Budget Actions Reference
|
||||
|
||||
| Action | When | Behavior |
|
||||
|--------|------|----------|
|
||||
| `BeforeRequestResult.proceed()` | Within budget | Request continues normally |
|
||||
| `BeforeRequestResult.cancel(msg)` | Budget exhausted | Raises `RequestCancelledError` |
|
||||
| `BeforeRequestResult.throttle(delay_ms=N)` | Near limit (95%+) | Delays request by N ms |
|
||||
| `BeforeRequestResult.degrade(to_model, reason)` | Approaching limit (80%+) | Switches to cheaper model |
|
||||
|
||||
## Key Points
|
||||
|
||||
- **Use `prewarm_fnc`** - LiveKit spawns worker processes; instrumentation must happen in each worker
|
||||
- **Don't instrument in main process** - It won't affect the worker processes where LLM calls happen
|
||||
- `emit_metric` is **required** - use `create_console_emitter(pretty=True)` for dev
|
||||
- Control agent connects automatically when `api_key` + `server_url` are provided
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**No metrics showing?**
|
||||
- Ensure `prewarm_fnc` is set in `WorkerOptions`
|
||||
- Check that `ADEN_API_KEY` and `ADEN_API_URL` are in your `.env`
|
||||
- Verify you're using `aden-py[livekit]` (with the livekit extra)
|
||||
|
||||
**Metrics in test but not in agent?**
|
||||
- LiveKit uses multiprocessing - the main process instrumentation doesn't carry over
|
||||
- The `prewarm_fnc` runs in each worker before your `entrypoint` is called
|
||||
@@ -1,247 +0,0 @@
|
||||
# User Authentication API
|
||||
|
||||
This document describes the user authentication endpoints available in the Hive backend.
|
||||
|
||||
## Base URL
|
||||
|
||||
```
|
||||
http://localhost:4000
|
||||
```
|
||||
|
||||
## Endpoints
|
||||
|
||||
### Register a New User
|
||||
|
||||
Create a new user account and receive an authentication token.
|
||||
|
||||
```
|
||||
POST /user/register
|
||||
```
|
||||
|
||||
#### Request Headers
|
||||
|
||||
| Header | Value | Required |
|
||||
| ------------ | ---------------- | -------- |
|
||||
| Content-Type | application/json | Yes |
|
||||
|
||||
#### Request Body
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
| --------- | ------ | -------- | ------------------------------- |
|
||||
| email | string | Yes | User's email address |
|
||||
| password | string | Yes | Password (minimum 8 characters) |
|
||||
| name | string | No | Display name |
|
||||
| firstname | string | No | First name |
|
||||
| lastname | string | No | Last name |
|
||||
|
||||
#### Example Request
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:4000/user/register \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"email": "user@example.com",
|
||||
"password": "securepassword123",
|
||||
"firstname": "John",
|
||||
"lastname": "Doe"
|
||||
}'
|
||||
```
|
||||
|
||||
#### Success Response (201 Created)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
||||
"email": "user@example.com",
|
||||
"name": "John Doe",
|
||||
"firstname": "John",
|
||||
"lastname": "Doe",
|
||||
"current_team_id": 1,
|
||||
"create_time": "2026-01-13T01:52:56.604Z"
|
||||
}
|
||||
```
|
||||
|
||||
#### Error Responses
|
||||
|
||||
| Status | Code | Message |
|
||||
| ------ | --------------------- | -------------------------------------- |
|
||||
| 400 | Bad Request | Email and password are required |
|
||||
| 400 | Bad Request | Please enter a valid email |
|
||||
| 400 | Bad Request | Password must be at least 8 characters |
|
||||
| 409 | Conflict | Email already registered |
|
||||
| 500 | Internal Server Error | Registration failed. Please try again. |
|
||||
|
||||
---
|
||||
|
||||
### Login
|
||||
|
||||
Authenticate an existing user and receive an authentication token.
|
||||
|
||||
```
|
||||
POST /user/login-v2
|
||||
```
|
||||
|
||||
#### Request Headers
|
||||
|
||||
| Header | Value | Required |
|
||||
| ------------ | ---------------- | -------- |
|
||||
| Content-Type | application/json | Yes |
|
||||
|
||||
#### Request Body
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
| -------- | ------ | -------- | -------------------- |
|
||||
| email | string | Yes | User's email address |
|
||||
| password | string | Yes | User's password |
|
||||
|
||||
#### Example Request
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:4000/user/login-v2 \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"email": "user@example.com",
|
||||
"password": "securepassword123"
|
||||
}'
|
||||
```
|
||||
|
||||
#### Success Response (200 OK)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
||||
"email": "user@example.com",
|
||||
"firstname": "John",
|
||||
"lastname": "Doe",
|
||||
"name": "John Doe",
|
||||
"current_team_id": 1,
|
||||
"create_time": "2026-01-13T01:52:56.594Z"
|
||||
}
|
||||
```
|
||||
|
||||
#### Error Responses
|
||||
|
||||
| Status | Code | Message |
|
||||
| ------ | --------------------- | -------------------------------------- |
|
||||
| 400 | Bad Request | Email and password are required |
|
||||
| 400 | Bad Request | Please enter a valid email |
|
||||
| 400 | Bad Request | Password must be at least 6 characters |
|
||||
| 400 | Bad Request | Please sign in with OAuth |
|
||||
| 401 | Unauthorized | Invalid email or password |
|
||||
| 403 | Forbidden | Your account has been disabled |
|
||||
| 500 | Internal Server Error | Login failed. Please try again. |
|
||||
|
||||
---
|
||||
|
||||
### Get Current User
|
||||
|
||||
Retrieve information about the currently authenticated user.
|
||||
|
||||
```
|
||||
GET /user/me
|
||||
```
|
||||
|
||||
#### Request Headers
|
||||
|
||||
| Header | Value | Required |
|
||||
| ------------- | ------- | -------- |
|
||||
| Authorization | {token} | Yes |
|
||||
|
||||
#### Example Request
|
||||
|
||||
```bash
|
||||
curl -X GET http://localhost:4000/user/me \
|
||||
-H "Authorization: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
|
||||
```
|
||||
|
||||
#### Success Response (200 OK)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"user": {
|
||||
"id": 1,
|
||||
"email": "user@example.com",
|
||||
"name": "John Doe",
|
||||
"firstname": "John",
|
||||
"lastname": "Doe",
|
||||
"current_team_id": 1,
|
||||
"avatar_url": null
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Error Responses
|
||||
|
||||
| Status | Code | Message |
|
||||
| ------ | --------------------- | ----------------------- |
|
||||
| 401 | Unauthorized | No token provided |
|
||||
| 401 | Unauthorized | Invalid token |
|
||||
| 500 | Internal Server Error | Failed to get user info |
|
||||
|
||||
---
|
||||
|
||||
## Authentication
|
||||
|
||||
After successful login or registration, the API returns a JWT token. Include this token in the `Authorization` header for authenticated requests:
|
||||
|
||||
```
|
||||
Authorization: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
|
||||
```
|
||||
|
||||
### Token Structure
|
||||
|
||||
The JWT token contains the following claims:
|
||||
|
||||
| Claim | Description |
|
||||
| --------------- | -------------------------------- |
|
||||
| id | User ID |
|
||||
| email | User email |
|
||||
| firstname | User first name |
|
||||
| lastname | User last name |
|
||||
| current_team_id | User's current team ID |
|
||||
| salt | Random salt for token validation |
|
||||
| iat | Issued at timestamp |
|
||||
| exp | Expiration timestamp |
|
||||
|
||||
### Token Expiration
|
||||
|
||||
By default, tokens expire after 7 days. This can be configured via the `JWT_EXPIRES_IN` environment variable.
|
||||
|
||||
---
|
||||
|
||||
## Development Credentials
|
||||
|
||||
For local development, the following default user is available:
|
||||
|
||||
| Field | Value |
|
||||
| -------- | ------------------- |
|
||||
| Email | dev@honeycomb.local |
|
||||
| Password | honeycomb123 |
|
||||
|
||||
---
|
||||
|
||||
## Error Response Format
|
||||
|
||||
All error responses follow this format:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"msg": "Error message describing what went wrong"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Rate Limiting
|
||||
|
||||
Currently, rate limiting is not enabled by default. It can be enabled via the `features.rate_limiting` config option.
|
||||
|
||||
---
|
||||
|
||||
## CORS
|
||||
|
||||
The API supports CORS. Configure the allowed origin via the `cors.origin` config option (default: `http://localhost:3000`).
|
||||
@@ -1,703 +0,0 @@
|
||||
# Aden SDK Trace Event Specification
|
||||
|
||||
**Version:** 2.0.0
|
||||
**Last Updated:** 2026-01-08
|
||||
|
||||
This document defines the authoritative specification for all events transmitted between the Aden SDK and the Aden Hive control server.
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Overview](#overview)
|
||||
2. [Event Types](#event-types)
|
||||
3. [MetricEvent](#metricevent)
|
||||
4. [ContentCapture (Layer 0)](#contentcapture-layer-0)
|
||||
5. [ToolCallCapture (Layer 6)](#toolcallcapture-layer-6)
|
||||
6. [ControlEvent](#controlevent)
|
||||
7. [HeartbeatEvent](#heartbeatevent)
|
||||
8. [ErrorEvent](#errorevent)
|
||||
9. [API Endpoints](#api-endpoints)
|
||||
10. [Storage Architecture](#storage-architecture)
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
The Aden SDK captures telemetry from LLM API calls and transmits events to the Aden Hive server for:
|
||||
- **Observability**: Token usage, latency, cost tracking
|
||||
- **Governance**: Content capture, tool call validation
|
||||
- **Control**: Budget enforcement, rate limiting, model degradation
|
||||
|
||||
### Providers Supported
|
||||
|
||||
| Provider | Value |
|
||||
|----------|-------|
|
||||
| OpenAI | `openai` |
|
||||
| Anthropic | `anthropic` |
|
||||
| Google Gemini | `gemini` |
|
||||
|
||||
### Transport
|
||||
|
||||
Events are sent via:
|
||||
- **HTTP POST** to `/v1/control/events` (batch)
|
||||
- **WebSocket** for real-time policy sync
|
||||
|
||||
---
|
||||
|
||||
## Event Types
|
||||
|
||||
| Event Type | Description | Direction |
|
||||
|------------|-------------|-----------|
|
||||
| `metric` | LLM call telemetry | SDK → Server |
|
||||
| `control` | Control action taken | SDK → Server |
|
||||
| `heartbeat` | Health status | SDK → Server |
|
||||
| `error` | Error report | SDK → Server |
|
||||
|
||||
---
|
||||
|
||||
## MetricEvent
|
||||
|
||||
The primary event emitted after each LLM API call. Contains flat fields for consistent cross-provider analytics.
|
||||
|
||||
### Envelope Structure
|
||||
|
||||
```json
|
||||
{
|
||||
"event_type": "metric",
|
||||
"timestamp": "2026-01-08T12:00:00.000Z",
|
||||
"sdk_instance_id": "uuid-v4",
|
||||
"data": { /* MetricEvent fields */ }
|
||||
}
|
||||
```
|
||||
|
||||
### MetricEvent Fields
|
||||
|
||||
#### Identity (OpenTelemetry-compatible)
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `trace_id` | string | **Yes** | Trace ID grouping related operations |
|
||||
| `span_id` | string | Yes | Unique span ID for this operation |
|
||||
| `parent_span_id` | string | No | Parent span for nested calls |
|
||||
| `request_id` | string | No | Provider-specific request ID |
|
||||
| `call_sequence` | integer | Yes | Sequence number within the trace |
|
||||
|
||||
#### Provider & Model
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `provider` | string | **Yes** | `openai`, `anthropic`, `gemini` |
|
||||
| `model` | string | **Yes** | Model identifier (e.g., `gpt-4o`, `claude-3-opus`) |
|
||||
| `stream` | boolean | Yes | Whether streaming was enabled |
|
||||
| `timestamp` | string | **Yes** | ISO 8601 timestamp of request start |
|
||||
|
||||
#### Performance
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `latency_ms` | float | Yes | Request latency in milliseconds |
|
||||
| `status_code` | integer | No | HTTP status code |
|
||||
| `error` | string | No | Error message if request failed |
|
||||
|
||||
#### Token Usage
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `input_tokens` | integer | Yes | Input/prompt tokens consumed |
|
||||
| `output_tokens` | integer | Yes | Output/completion tokens consumed |
|
||||
| `total_tokens` | integer | Yes | Total tokens (input + output) |
|
||||
| `cached_tokens` | integer | No | Tokens served from cache |
|
||||
| `reasoning_tokens` | integer | No | Reasoning tokens (o1/o3 models) |
|
||||
|
||||
#### Rate Limits
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `rate_limit_remaining_requests` | integer | No | Remaining requests in window |
|
||||
| `rate_limit_remaining_tokens` | integer | No | Remaining tokens in window |
|
||||
| `rate_limit_reset_requests` | float | No | Seconds until request limit resets |
|
||||
| `rate_limit_reset_tokens` | float | No | Seconds until token limit resets |
|
||||
|
||||
#### Call Context
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `agent_stack` | string[] | No | Stack of agent names leading to this call |
|
||||
| `call_site_file` | string | No | File path of immediate caller |
|
||||
| `call_site_line` | integer | No | Line number |
|
||||
| `call_site_column` | integer | No | Column number |
|
||||
| `call_site_function` | string | No | Function name |
|
||||
| `call_stack` | string[] | No | Full call stack (file:line:function) |
|
||||
|
||||
#### Tool Usage (Summary)
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `tool_call_count` | integer | No | Number of tool calls made |
|
||||
| `tool_names` | string | No | Tool names (comma-separated) |
|
||||
|
||||
#### Provider-specific
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `service_tier` | string | No | Service tier (auto, default, flex, priority) |
|
||||
| `metadata` | object | No | Custom metadata attached to request |
|
||||
|
||||
#### Layer 0: Content Capture
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `content_capture` | ContentCapture | No | Full content capture (see below) |
|
||||
|
||||
#### Layer 6: Tool Call Deep Inspection
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `tool_calls_captured` | ToolCallCapture[] | No | Detailed tool call captures |
|
||||
| `tool_validation_errors_count` | integer | No | Count of validation errors |
|
||||
|
||||
### Example MetricEvent
|
||||
|
||||
```json
|
||||
{
|
||||
"event_type": "metric",
|
||||
"timestamp": "2026-01-08T12:00:00.000Z",
|
||||
"sdk_instance_id": "abc123",
|
||||
"data": {
|
||||
"trace_id": "tr_abc123",
|
||||
"span_id": "sp_def456",
|
||||
"call_sequence": 1,
|
||||
"provider": "openai",
|
||||
"model": "gpt-4o",
|
||||
"stream": false,
|
||||
"latency_ms": 1234.5,
|
||||
"input_tokens": 150,
|
||||
"output_tokens": 50,
|
||||
"total_tokens": 200,
|
||||
"cached_tokens": 0,
|
||||
"agent_stack": ["main_agent", "sub_agent"],
|
||||
"tool_call_count": 2,
|
||||
"tool_names": "search,calculate",
|
||||
"metadata": {
|
||||
"user_id": "user_123",
|
||||
"session_id": "sess_456"
|
||||
},
|
||||
"content_capture": {
|
||||
"system_prompt": "You are a helpful assistant.",
|
||||
"messages": [...],
|
||||
"response_content": "Here is my response...",
|
||||
"finish_reason": "stop"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ContentCapture (Layer 0)
|
||||
|
||||
Full content capture for request and response. Enables governance, debugging, and compliance.
|
||||
|
||||
### Fields
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `system_prompt` | string \| ContentReference | System prompt |
|
||||
| `messages` | MessageCapture[] \| ContentReference | Message history |
|
||||
| `tools` | ToolSchemaCapture[] \| ContentReference | Tools schema |
|
||||
| `params` | RequestParamsCapture | Request parameters |
|
||||
| `response_content` | string \| ContentReference | Response text |
|
||||
| `finish_reason` | string | Why response ended: `stop`, `length`, `tool_calls`, `content_filter` |
|
||||
| `choice_count` | integer | Number of choices (for n > 1) |
|
||||
| `has_images` | boolean | Whether request contained images |
|
||||
| `image_urls` | string[] | Image URLs (never base64) |
|
||||
|
||||
### ContentReference
|
||||
|
||||
When content exceeds `max_content_bytes`, it's stored separately and referenced:
|
||||
|
||||
```json
|
||||
{
|
||||
"content_id": "uuid-v4",
|
||||
"content_hash": "sha256-hex",
|
||||
"byte_size": 12345,
|
||||
"truncated_preview": "First 100 chars..."
|
||||
}
|
||||
```
|
||||
|
||||
### MessageCapture
|
||||
|
||||
```json
|
||||
{
|
||||
"role": "user|assistant|system|tool",
|
||||
"content": "string or ContentReference",
|
||||
"name": "optional name",
|
||||
"tool_call_id": "for tool results"
|
||||
}
|
||||
```
|
||||
|
||||
### ToolSchemaCapture
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "function_name",
|
||||
"description": "Tool description",
|
||||
"parameters_schema": { /* JSON Schema */ }
|
||||
}
|
||||
```
|
||||
|
||||
### RequestParamsCapture
|
||||
|
||||
```json
|
||||
{
|
||||
"temperature": 0.7,
|
||||
"max_tokens": 1000,
|
||||
"top_p": 1.0,
|
||||
"frequency_penalty": 0,
|
||||
"presence_penalty": 0,
|
||||
"stop": ["STOP"],
|
||||
"seed": 12345,
|
||||
"top_k": 40
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ToolCallCapture (Layer 6)
|
||||
|
||||
Detailed tool call capture with validation results.
|
||||
|
||||
### Fields
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `id` | string | Tool call ID for correlation |
|
||||
| `name` | string | Tool/function name |
|
||||
| `arguments` | object \| ContentReference | Parsed arguments |
|
||||
| `arguments_raw` | string \| ContentReference | Raw JSON string |
|
||||
| `validation_errors` | ValidationError[] | Schema validation errors |
|
||||
| `is_valid` | boolean | Whether arguments passed validation |
|
||||
| `index` | integer | Position in tool_calls array |
|
||||
|
||||
### ValidationError
|
||||
|
||||
```json
|
||||
{
|
||||
"path": "properties.name",
|
||||
"message": "Required property missing",
|
||||
"expected_type": "string",
|
||||
"actual_type": "undefined"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ControlEvent
|
||||
|
||||
Emitted when a control action is taken on a request.
|
||||
|
||||
### Fields
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `event_type` | string | Yes | Always `"control"` |
|
||||
| `timestamp` | string | Yes | ISO 8601 timestamp |
|
||||
| `sdk_instance_id` | string | Yes | SDK instance identifier |
|
||||
| `trace_id` | string | Yes | Associated trace ID |
|
||||
| `span_id` | string | Yes | Associated span ID |
|
||||
| `provider` | string | Yes | Provider name |
|
||||
| `original_model` | string | Yes | Originally requested model |
|
||||
| `action` | string | Yes | Action taken (see below) |
|
||||
| `reason` | string | No | Human-readable reason |
|
||||
| `degraded_to` | string | No | Model switched to (if degraded) |
|
||||
| `throttle_delay_ms` | integer | No | Delay applied (if throttled) |
|
||||
| `estimated_cost` | float | No | Estimated cost that triggered decision |
|
||||
| `policy_id` | string | Yes | Policy ID (default: `"default"`) |
|
||||
| `budget_id` | string | No | Budget that triggered action |
|
||||
| `context_id` | string | No | Context ID (user, session, etc.) |
|
||||
|
||||
### Control Actions
|
||||
|
||||
| Action | Description |
|
||||
|--------|-------------|
|
||||
| `allow` | Request proceeds normally |
|
||||
| `block` | Request is rejected |
|
||||
| `throttle` | Request is delayed before proceeding |
|
||||
| `degrade` | Request uses a cheaper/fallback model |
|
||||
| `alert` | Request proceeds but triggers alert |
|
||||
|
||||
### Example ControlEvent
|
||||
|
||||
```json
|
||||
{
|
||||
"event_type": "control",
|
||||
"timestamp": "2026-01-08T12:00:00.000Z",
|
||||
"sdk_instance_id": "abc123",
|
||||
"trace_id": "tr_abc123",
|
||||
"span_id": "sp_def456",
|
||||
"provider": "openai",
|
||||
"original_model": "gpt-4o",
|
||||
"action": "degrade",
|
||||
"reason": "Budget limit exceeded",
|
||||
"degraded_to": "gpt-4o-mini",
|
||||
"estimated_cost": 0.05,
|
||||
"policy_id": "default",
|
||||
"budget_id": "budget_monthly"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## HeartbeatEvent
|
||||
|
||||
Periodic health check sent by the SDK.
|
||||
|
||||
### Fields
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `event_type` | string | Yes | Always `"heartbeat"` |
|
||||
| `timestamp` | string | Yes | ISO 8601 timestamp |
|
||||
| `sdk_instance_id` | string | Yes | SDK instance identifier |
|
||||
| `status` | string | Yes | `healthy`, `degraded`, `reconnecting` |
|
||||
| `requests_since_last` | integer | Yes | Requests since last heartbeat |
|
||||
| `errors_since_last` | integer | Yes | Errors since last heartbeat |
|
||||
| `policy_cache_age_seconds` | integer | Yes | Policy cache age |
|
||||
| `websocket_connected` | boolean | Yes | WebSocket connection status |
|
||||
| `sdk_version` | string | Yes | SDK version |
|
||||
|
||||
---
|
||||
|
||||
## ErrorEvent
|
||||
|
||||
Emitted when an error occurs in the SDK.
|
||||
|
||||
### Fields
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `event_type` | string | Yes | Always `"error"` |
|
||||
| `timestamp` | string | Yes | ISO 8601 timestamp |
|
||||
| `sdk_instance_id` | string | Yes | SDK instance identifier |
|
||||
| `message` | string | Yes | Error message |
|
||||
| `code` | string | No | Error code |
|
||||
| `stack` | string | No | Stack trace |
|
||||
| `trace_id` | string | No | Related trace ID |
|
||||
|
||||
---
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### POST /v1/control/events
|
||||
|
||||
Submit events batch.
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{
|
||||
"events": [
|
||||
{ "event_type": "metric", "timestamp": "...", "data": {...} },
|
||||
{ "event_type": "control", "timestamp": "...", ... }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"processed": 2
|
||||
}
|
||||
```
|
||||
|
||||
### POST /v1/control/content
|
||||
|
||||
Store large content items (MongoDB - for SDK content references).
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"content_id": "uuid",
|
||||
"content_hash": "sha256-hex",
|
||||
"content": "full content string",
|
||||
"byte_size": 12345
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"stored": 1
|
||||
}
|
||||
```
|
||||
|
||||
### GET /v1/control/content/:contentId
|
||||
|
||||
Retrieve stored content by ID (MongoDB).
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"content_id": "uuid",
|
||||
"content_hash": "sha256-hex",
|
||||
"content": "full content string",
|
||||
"byte_size": 12345
|
||||
}
|
||||
```
|
||||
|
||||
### GET /v1/control/events/:traceId/:callSequence/content
|
||||
|
||||
Retrieve content for a specific event from TSDB warm/cold storage.
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"trace_id": "tr_abc123",
|
||||
"call_sequence": 1,
|
||||
"content_items": [
|
||||
{
|
||||
"content_type": "system_prompt",
|
||||
"content_hash": "sha256-hex",
|
||||
"byte_size": 256,
|
||||
"truncated_preview": "You are a helpful...",
|
||||
"content": "You are a helpful assistant..."
|
||||
},
|
||||
{
|
||||
"content_type": "messages",
|
||||
"content_hash": "sha256-hex",
|
||||
"byte_size": 4096,
|
||||
"message_count": 5,
|
||||
"truncated_preview": "[{\"role\":\"user\"...",
|
||||
"content": "[{\"role\":\"user\",\"content\":\"Hello\"}...]"
|
||||
},
|
||||
{
|
||||
"content_type": "response",
|
||||
"content_hash": "sha256-hex",
|
||||
"byte_size": 512,
|
||||
"truncated_preview": "Here is my response...",
|
||||
"content": "Here is my response to your question..."
|
||||
}
|
||||
],
|
||||
"count": 3
|
||||
}
|
||||
```
|
||||
|
||||
### GET /v1/control/content/hash/:contentHash
|
||||
|
||||
Retrieve content from cold storage by SHA-256 hash.
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"content_hash": "sha256-hex",
|
||||
"content": "full content string",
|
||||
"byte_size": 12345
|
||||
}
|
||||
```
|
||||
|
||||
### GET /v1/control/policy
|
||||
|
||||
Fetch current control policy.
|
||||
|
||||
### POST /v1/control/budget/validate
|
||||
|
||||
Server-side budget validation (hybrid enforcement).
|
||||
|
||||
---
|
||||
|
||||
## Storage Architecture
|
||||
|
||||
The storage system uses a **hot/warm/cold** architecture optimized for time-series analytics with content deduplication.
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ SDK Event Ingestion │
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ Event Normalization & Content Extraction │
|
||||
│ │
|
||||
│ • Extract content_capture fields │
|
||||
│ • Hash content with SHA-256 │
|
||||
│ • Create lightweight content flags for hot table │
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
┌───────────────┼───────────────┐
|
||||
▼ ▼ ▼
|
||||
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
|
||||
│ HOT TABLE │ │ WARM TABLE │ │ COLD TABLE │
|
||||
│ llm_events │ │llm_event_ │ │llm_content_ │
|
||||
│ │ │ content │ │ store │
|
||||
│ Metrics only │ │Content refs │ │ Deduplicated │
|
||||
│ Fast queries │ │ per event │ │ content │
|
||||
└──────────────┘ └──────────────┘ └──────────────┘
|
||||
```
|
||||
|
||||
### Design Principles
|
||||
|
||||
1. **Hot/Cold Separation**: Metrics stay in the hot table for fast time-series queries; content is stored separately
|
||||
2. **Content Deduplication**: Identical content (same SHA-256 hash) is stored once, regardless of how many events reference it
|
||||
3. **Reference Counting**: Cold storage tracks how many events reference each piece of content
|
||||
4. **Preview Without Fetch**: Warm table stores truncated previews for quick scanning without fetching full content
|
||||
|
||||
### TSDB Hot Table: `llm_events`
|
||||
|
||||
Stores metric events for fast time-series analytics. **Content is NOT stored here** (only lightweight flags).
|
||||
|
||||
| Column | Type | Description |
|
||||
|--------|------|-------------|
|
||||
| `timestamp` | timestamptz | Event timestamp (partition key) |
|
||||
| `ingest_date` | date | Ingestion date |
|
||||
| `team_id` | text | Team identifier |
|
||||
| `user_id` | text | User identifier |
|
||||
| `trace_id` | text | Trace ID |
|
||||
| `span_id` | text | Span ID |
|
||||
| `parent_span_id` | text | Parent span ID |
|
||||
| `request_id` | text | Provider request ID |
|
||||
| `provider` | text | Provider name |
|
||||
| `call_sequence` | integer | Sequence within trace |
|
||||
| `model` | text | Model identifier |
|
||||
| `stream` | boolean | Streaming flag |
|
||||
| `agent` | text | Primary agent name |
|
||||
| `agent_stack` | jsonb | Full agent stack |
|
||||
| `latency_ms` | double precision | Latency in ms |
|
||||
| `usage_input_tokens` | double precision | Input tokens |
|
||||
| `usage_output_tokens` | double precision | Output tokens |
|
||||
| `usage_total_tokens` | double precision | Total tokens |
|
||||
| `usage_cached_tokens` | double precision | Cached tokens |
|
||||
| `usage_reasoning_tokens` | double precision | Reasoning tokens |
|
||||
| `cost_total` | numeric | Calculated cost |
|
||||
| `metadata` | jsonb | Custom metadata |
|
||||
| `call_site` | jsonb | Call site info |
|
||||
| `has_content` | boolean | Whether content was captured |
|
||||
| `finish_reason` | text | Response finish reason |
|
||||
| `tool_call_count` | integer | Number of tool calls |
|
||||
| `created_at` | timestamptz | Record creation time |
|
||||
|
||||
**Primary Key:** `(timestamp, trace_id, call_sequence)`
|
||||
|
||||
**Indexes:**
|
||||
- `idx_llm_events_ts` - timestamp DESC
|
||||
- `idx_llm_events_team_ts` - team_id, timestamp DESC
|
||||
- `idx_llm_events_model` - model
|
||||
- `idx_llm_events_agent` - agent
|
||||
- `idx_llm_events_trace` - trace_id
|
||||
|
||||
### TSDB Warm Table: `llm_event_content`
|
||||
|
||||
Links events to deduplicated content in cold storage. One row per content type per event.
|
||||
|
||||
| Column | Type | Description |
|
||||
|--------|------|-------------|
|
||||
| `id` | bigserial | Auto-increment ID |
|
||||
| `timestamp` | timestamptz | Event timestamp |
|
||||
| `trace_id` | text | Trace ID |
|
||||
| `call_sequence` | integer | Sequence within trace |
|
||||
| `team_id` | text | Team identifier |
|
||||
| `content_type` | text | Type: `system_prompt`, `messages`, `response`, `tools`, `params` |
|
||||
| `content_hash` | text | SHA-256 hash (FK to cold store) |
|
||||
| `byte_size` | integer | Content size in bytes |
|
||||
| `message_count` | integer | Number of messages (for `messages` type) |
|
||||
| `truncated_preview` | text | First 200 chars for quick preview |
|
||||
| `created_at` | timestamptz | Record creation time |
|
||||
|
||||
**Primary Key:** `(id)`
|
||||
|
||||
**Indexes:**
|
||||
- `idx_llm_event_content_event` - trace_id, call_sequence, timestamp
|
||||
- `idx_llm_event_content_type` - team_id, content_type, timestamp DESC
|
||||
- `idx_llm_event_content_hash` - content_hash
|
||||
|
||||
### TSDB Cold Table: `llm_content_store`
|
||||
|
||||
Content-addressable storage with SHA-256 hashes. Deduplicated across all events.
|
||||
|
||||
| Column | Type | Description |
|
||||
|--------|------|-------------|
|
||||
| `content_hash` | text | SHA-256 hash of content (PK) |
|
||||
| `team_id` | text | Team identifier (PK) |
|
||||
| `content` | text | Full content string |
|
||||
| `byte_size` | integer | Content size in bytes |
|
||||
| `ref_count` | integer | Number of events referencing this content |
|
||||
| `first_seen_at` | timestamptz | When content was first stored |
|
||||
| `last_seen_at` | timestamptz | When content was last referenced |
|
||||
|
||||
**Primary Key:** `(content_hash, team_id)`
|
||||
|
||||
**Indexes:**
|
||||
- `idx_llm_content_store_refs` - team_id, ref_count, last_seen_at (for cleanup)
|
||||
|
||||
### MongoDB: `aden_control_content`
|
||||
|
||||
Stores large content items from SDK's content reference system (separate from TSDB storage).
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `content_id` | string | Unique content identifier |
|
||||
| `team_id` | string | Team identifier |
|
||||
| `content_hash` | string | SHA-256 hash |
|
||||
| `content` | string | Full content |
|
||||
| `byte_size` | number | Content size in bytes |
|
||||
| `created_at` | string | Creation timestamp |
|
||||
| `updated_at` | string | Last update timestamp |
|
||||
|
||||
**Index:** `{ content_id: 1, team_id: 1 }` (unique)
|
||||
|
||||
### MongoDB: `aden_control_policies`
|
||||
|
||||
Stores control policies for teams.
|
||||
|
||||
---
|
||||
|
||||
## Content Types
|
||||
|
||||
The warm table stores references to different content types:
|
||||
|
||||
| Type | Description | Example |
|
||||
|------|-------------|---------|
|
||||
| `system_prompt` | System/developer message | "You are a helpful assistant..." |
|
||||
| `messages` | Full conversation history | JSON array of messages |
|
||||
| `response` | Model's response content | "Here is my response..." |
|
||||
| `tools` | Tool/function schemas | JSON array of tool definitions |
|
||||
| `params` | Request parameters | `{"temperature": 0.7, "max_tokens": 1000}` |
|
||||
|
||||
---
|
||||
|
||||
## Deduplication Example
|
||||
|
||||
When the same system prompt is used across multiple requests:
|
||||
|
||||
```
|
||||
Request 1: system_prompt = "You are a helpful assistant."
|
||||
→ Hash: abc123...
|
||||
→ Cold store: INSERT (ref_count = 1)
|
||||
→ Warm store: INSERT reference for event 1
|
||||
|
||||
Request 2: system_prompt = "You are a helpful assistant." (same)
|
||||
→ Hash: abc123... (same hash)
|
||||
→ Cold store: UPDATE ref_count = 2
|
||||
→ Warm store: INSERT reference for event 2
|
||||
|
||||
Request 3: system_prompt = "You are a code reviewer."
|
||||
→ Hash: def456... (different)
|
||||
→ Cold store: INSERT (ref_count = 1)
|
||||
→ Warm store: INSERT reference for event 3
|
||||
```
|
||||
|
||||
This means the first system prompt is stored **once** but referenced by two events.
|
||||
|
||||
---
|
||||
|
||||
## Version History
|
||||
|
||||
| Version | Date | Changes |
|
||||
|---------|------|---------|
|
||||
| 2.0.0 | 2026-01-08 | Hot/warm/cold storage architecture; content deduplication |
|
||||
| 1.0.0 | 2026-01-08 | Initial specification |
|
||||
@@ -1,101 +0,0 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: aden-hive
|
||||
labels:
|
||||
app: aden-hive
|
||||
app.kubernetes.io/name: aden-hive
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: aden-hive
|
||||
strategy:
|
||||
type: RollingUpdate
|
||||
rollingUpdate:
|
||||
maxSurge: 25%
|
||||
maxUnavailable: 25%
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: aden-hive
|
||||
app.kubernetes.io/name: aden-hive
|
||||
spec:
|
||||
containers:
|
||||
- name: aden-hive
|
||||
image: aden-hive
|
||||
imagePullPolicy: IfNotPresent
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 3001
|
||||
protocol: TCP
|
||||
env:
|
||||
- name: POD_NAME
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
fieldPath: metadata.name
|
||||
- name: POD_NAMESPACE
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
fieldPath: metadata.namespace
|
||||
- name: POD_IP
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
fieldPath: status.podIP
|
||||
- name: MYSQL_SSL_CA
|
||||
value: /mnt/certs/mysql/server-ca.pem
|
||||
- name: MYSQL_SSL_KEY
|
||||
value: /mnt/certs/mysql/client-key.pem
|
||||
- name: MYSQL_SSL_CERT
|
||||
value: /mnt/certs/mysql/client-cert.pem
|
||||
volumeMounts:
|
||||
- name: mysql-ssl-certs
|
||||
mountPath: /mnt/certs/mysql
|
||||
readOnly: true
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
name: aden-hive-config
|
||||
- secretRef:
|
||||
name: aden-hive-secrets
|
||||
resources:
|
||||
requests:
|
||||
cpu: 250m
|
||||
memory: 256Mi
|
||||
limits:
|
||||
cpu: 1000m
|
||||
memory: 512Mi
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: 3001
|
||||
initialDelaySeconds: 60
|
||||
periodSeconds: 60
|
||||
timeoutSeconds: 15
|
||||
failureThreshold: 5
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: 3001
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 30
|
||||
timeoutSeconds: 10
|
||||
failureThreshold: 5
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
runAsNonRoot: true
|
||||
runAsUser: 1001
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
volumes:
|
||||
- name: mysql-ssl-certs
|
||||
secret:
|
||||
secretName: mysql-ssl-certs
|
||||
defaultMode: 0444
|
||||
items:
|
||||
- key: server-ca.pem
|
||||
path: server-ca.pem
|
||||
- key: client-key.pem
|
||||
path: client-key.pem
|
||||
- key: client-cert.pem
|
||||
path: client-cert.pem
|
||||
@@ -1,6 +0,0 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
|
||||
resources:
|
||||
- deployment.yaml
|
||||
- service.yaml
|
||||
@@ -1,15 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: aden-hive
|
||||
labels:
|
||||
app: aden-hive
|
||||
spec:
|
||||
type: ClusterIP
|
||||
ports:
|
||||
- port: 80
|
||||
targetPort: 3001
|
||||
protocol: TCP
|
||||
name: http
|
||||
selector:
|
||||
app: aden-hive
|
||||
@@ -1,21 +0,0 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
|
||||
namespace: production
|
||||
|
||||
resources:
|
||||
- ../../base
|
||||
- namespace.yaml
|
||||
|
||||
namePrefix: prod-
|
||||
|
||||
commonLabels:
|
||||
environment: production
|
||||
|
||||
images:
|
||||
- name: aden-hive
|
||||
newName: gcr.io/tool-for-analyst/aden-hive
|
||||
newTag: latest
|
||||
|
||||
patches:
|
||||
- path: patches/deployment.yaml
|
||||
@@ -1,6 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: production
|
||||
labels:
|
||||
environment: production
|
||||
@@ -1,27 +0,0 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: aden-hive
|
||||
spec:
|
||||
replicas: 2
|
||||
template:
|
||||
spec:
|
||||
containers:
|
||||
- name: aden-hive
|
||||
env:
|
||||
- name: NODE_ENV
|
||||
value: production
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
name: aden-api-server-config
|
||||
- secretRef:
|
||||
name: aden-api-server-secrets
|
||||
- secretRef:
|
||||
name: database-secrets
|
||||
resources:
|
||||
requests:
|
||||
cpu: 500m
|
||||
memory: 512Mi
|
||||
limits:
|
||||
cpu: 1000m
|
||||
memory: 1Gi
|
||||
@@ -1,21 +0,0 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
|
||||
namespace: staging
|
||||
|
||||
resources:
|
||||
- ../../base
|
||||
- namespace.yaml
|
||||
|
||||
namePrefix: staging-
|
||||
|
||||
commonLabels:
|
||||
environment: staging
|
||||
|
||||
images:
|
||||
- name: aden-hive
|
||||
newName: gcr.io/acho-alpha-project/aden-hive
|
||||
newTag: latest
|
||||
|
||||
patches:
|
||||
- path: patches/deployment.yaml
|
||||
@@ -1,6 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: staging
|
||||
labels:
|
||||
environment: staging
|
||||
@@ -1,27 +0,0 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: aden-hive
|
||||
spec:
|
||||
replicas: 1
|
||||
template:
|
||||
spec:
|
||||
containers:
|
||||
- name: aden-hive
|
||||
env:
|
||||
- name: NODE_ENV
|
||||
value: staging
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
name: aden-api-server-config
|
||||
- secretRef:
|
||||
name: aden-api-server-secrets
|
||||
- secretRef:
|
||||
name: database-secrets
|
||||
resources:
|
||||
requests:
|
||||
cpu: 250m
|
||||
memory: 256Mi
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 512Mi
|
||||
@@ -1,61 +0,0 @@
|
||||
{
|
||||
"name": "hive",
|
||||
"version": "1.0.0",
|
||||
"description": "Aden Hive - LLM observability and control plane backend",
|
||||
"private": true,
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
"dev": "ts-node-dev --respawn --transpile-only src/index.ts",
|
||||
"build": "tsc && npm run build:copy-sql",
|
||||
"build:copy-sql": "find src -name '*.sql' -exec sh -c 'mkdir -p dist/$(dirname ${1#src/}) && cp \"$1\" dist/${1#src/}' _ {} \\;",
|
||||
"start": "node dist/index.js",
|
||||
"test": "jest --passWithNoTests",
|
||||
"test:mcp": "ts-node --transpile-only scripts/test-mcp.ts",
|
||||
"test:mcp:quick": "./scripts/test-mcp-curl.sh",
|
||||
"lint": "eslint src/",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"clean": "rm -rf dist node_modules"
|
||||
},
|
||||
"dependencies": {
|
||||
"@acho-inc/administration": "^1.0.7",
|
||||
"@modelcontextprotocol/sdk": "^1.25.2",
|
||||
"@socket.io/redis-adapter": "^8.2.1",
|
||||
"@socket.io/redis-emitter": "^5.1.0",
|
||||
"compression": "^1.7.4",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.3.1",
|
||||
"express": "^4.18.2",
|
||||
"helmet": "^7.1.0",
|
||||
"http-errors": "^2.0.0",
|
||||
"ioredis": "^5.3.2",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"mongodb": "^6.3.0",
|
||||
"morgan": "^1.10.0",
|
||||
"passport": "^0.7.0",
|
||||
"passport-jwt": "^4.0.1",
|
||||
"pg": "^8.11.3",
|
||||
"socket.io": "^4.6.1",
|
||||
"zod": "^4.3.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/compression": "^1.7.5",
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/jsonwebtoken": "^9.0.5",
|
||||
"@types/morgan": "^1.9.9",
|
||||
"@types/node": "^20.10.0",
|
||||
"@types/passport": "^1.0.16",
|
||||
"@types/passport-jwt": "^4.0.1",
|
||||
"@types/pg": "^8.10.9",
|
||||
"@typescript-eslint/eslint-plugin": "^6.14.0",
|
||||
"@typescript-eslint/parser": "^6.14.0",
|
||||
"eslint": "^8.56.0",
|
||||
"jest": "^29.7.0",
|
||||
"ts-node": "^10.9.2",
|
||||
"ts-node-dev": "^2.0.0",
|
||||
"typescript": "^5.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
}
|
||||
@@ -1,129 +0,0 @@
|
||||
/**
|
||||
* Migration: Add agent_name column to llm_events table
|
||||
*
|
||||
* This script adds the `agent_name` column to all existing team schemas.
|
||||
* Run with: npx ts-node scripts/migrate-add-agent-name.ts
|
||||
*
|
||||
* Environment variables required:
|
||||
* - PGHOST, PGUSER, PGPASSWORD, PGDATABASE, PGPORT (or PG_CONNECTION_STRING)
|
||||
*/
|
||||
|
||||
import { Pool } from "pg";
|
||||
|
||||
const getPool = (): Pool => {
|
||||
// Support multiple env var names
|
||||
const connectionString =
|
||||
process.env.TSDB_PG_URL ||
|
||||
process.env.PG_CONNECTION_STRING ||
|
||||
process.env.DATABASE_URL;
|
||||
|
||||
if (connectionString) {
|
||||
return new Pool({ connectionString });
|
||||
}
|
||||
|
||||
return new Pool({
|
||||
host: process.env.PGHOST || "localhost",
|
||||
user: process.env.PGUSER || "postgres",
|
||||
password: process.env.PGPASSWORD || "postgres",
|
||||
database: process.env.PGDATABASE || "aden",
|
||||
port: parseInt(process.env.PGPORT || "5432", 10),
|
||||
});
|
||||
};
|
||||
|
||||
async function migrate() {
|
||||
const pool = getPool();
|
||||
|
||||
try {
|
||||
console.log("[Migration] Starting agent_name column migration...");
|
||||
|
||||
// Find all team schemas (schemas starting with 'team_')
|
||||
const schemasResult = await pool.query(`
|
||||
SELECT schema_name
|
||||
FROM information_schema.schemata
|
||||
WHERE schema_name LIKE 'team_%'
|
||||
ORDER BY schema_name
|
||||
`);
|
||||
|
||||
const schemas = schemasResult.rows.map((r) => r.schema_name as string);
|
||||
console.log(`[Migration] Found ${schemas.length} team schemas`);
|
||||
|
||||
if (schemas.length === 0) {
|
||||
console.log("[Migration] No team schemas found. Nothing to migrate.");
|
||||
return;
|
||||
}
|
||||
|
||||
let successCount = 0;
|
||||
let skipCount = 0;
|
||||
let errorCount = 0;
|
||||
|
||||
for (const schema of schemas) {
|
||||
try {
|
||||
// Check if llm_events table exists in this schema
|
||||
const tableExists = await pool.query(
|
||||
`
|
||||
SELECT 1
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = $1 AND table_name = 'llm_events'
|
||||
`,
|
||||
[schema]
|
||||
);
|
||||
|
||||
if (tableExists.rows.length === 0) {
|
||||
console.log(`[Migration] ${schema}: No llm_events table, skipping`);
|
||||
skipCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if agent_name column already exists
|
||||
const columnExists = await pool.query(
|
||||
`
|
||||
SELECT 1
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = $1
|
||||
AND table_name = 'llm_events'
|
||||
AND column_name = 'agent_name'
|
||||
`,
|
||||
[schema]
|
||||
);
|
||||
|
||||
if (columnExists.rows.length > 0) {
|
||||
console.log(`[Migration] ${schema}: agent_name column already exists, skipping`);
|
||||
skipCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Add the agent_name column after agent column
|
||||
await pool.query(`
|
||||
ALTER TABLE ${schema}.llm_events
|
||||
ADD COLUMN agent_name text
|
||||
`);
|
||||
|
||||
console.log(`[Migration] ${schema}: Added agent_name column`);
|
||||
successCount++;
|
||||
} catch (err) {
|
||||
console.error(`[Migration] ${schema}: Error - ${(err as Error).message}`);
|
||||
errorCount++;
|
||||
}
|
||||
}
|
||||
|
||||
console.log("\n[Migration] Summary:");
|
||||
console.log(` - Schemas processed: ${schemas.length}`);
|
||||
console.log(` - Successfully migrated: ${successCount}`);
|
||||
console.log(` - Skipped (already migrated or no table): ${skipCount}`);
|
||||
console.log(` - Errors: ${errorCount}`);
|
||||
|
||||
if (errorCount === 0) {
|
||||
console.log("\n[Migration] Completed successfully!");
|
||||
} else {
|
||||
console.log("\n[Migration] Completed with errors. Please review above.");
|
||||
process.exit(1);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("[Migration] Fatal error:", (err as Error).message);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
await pool.end();
|
||||
}
|
||||
}
|
||||
|
||||
migrate();
|
||||
@@ -1,61 +0,0 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Quick MCP Server Test using curl
|
||||
#
|
||||
# Usage:
|
||||
# ADEN_AUTH_TOKEN=your-jwt-token ./scripts/test-mcp-curl.sh
|
||||
#
|
||||
# The script tests basic connectivity and endpoints.
|
||||
|
||||
set -e
|
||||
|
||||
API_URL="${ADEN_API_URL:-http://localhost:3000}"
|
||||
TOKEN="${ADEN_AUTH_TOKEN}"
|
||||
|
||||
if [ -z "$TOKEN" ]; then
|
||||
echo "Error: ADEN_AUTH_TOKEN environment variable is required"
|
||||
echo "Usage: ADEN_AUTH_TOKEN=your-jwt-token ./scripts/test-mcp-curl.sh"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "============================================================"
|
||||
echo "MCP Server Quick Test"
|
||||
echo "============================================================"
|
||||
echo "API URL: $API_URL"
|
||||
echo ""
|
||||
|
||||
# Test 1: Health check
|
||||
echo "1. Health Check (GET /mcp/health)"
|
||||
curl -s "$API_URL/mcp/health" | jq .
|
||||
echo ""
|
||||
|
||||
# Test 2: List sessions (should be empty or show existing)
|
||||
echo "2. List Sessions (GET /mcp/sessions)"
|
||||
curl -s -H "Authorization: Bearer $TOKEN" "$API_URL/mcp/sessions" | jq .
|
||||
echo ""
|
||||
|
||||
# Test 3: Start SSE connection and capture session ID
|
||||
echo "3. Testing SSE Connection (GET /mcp)"
|
||||
echo " Starting connection (will timeout after 2s)..."
|
||||
|
||||
# Use timeout to limit the SSE connection
|
||||
SESSION_ID=$(timeout 2s curl -s -N \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Accept: text/event-stream" \
|
||||
"$API_URL/mcp" 2>&1 | head -5 || true)
|
||||
|
||||
echo " Response (first 5 lines):"
|
||||
echo "$SESSION_ID" | head -5
|
||||
echo ""
|
||||
|
||||
# Test 4: Check sessions again
|
||||
echo "4. Sessions After Connection (GET /mcp/sessions)"
|
||||
curl -s -H "Authorization: Bearer $TOKEN" "$API_URL/mcp/sessions" | jq .
|
||||
echo ""
|
||||
|
||||
echo "============================================================"
|
||||
echo "Quick test completed!"
|
||||
echo ""
|
||||
echo "For full tool testing, use the TypeScript test client:"
|
||||
echo " ADEN_AUTH_TOKEN=\$TOKEN npx ts-node scripts/test-mcp.ts"
|
||||
echo "============================================================"
|
||||
@@ -1,176 +0,0 @@
|
||||
/**
|
||||
* MCP Server Test Script
|
||||
*
|
||||
* Tests the MCP server by connecting via HTTP/SSE and invoking tools.
|
||||
*
|
||||
* Usage:
|
||||
* npx ts-node scripts/test-mcp.ts
|
||||
*
|
||||
* Environment:
|
||||
* ADEN_API_URL - Base URL (default: http://localhost:3000)
|
||||
* ADEN_AUTH_TOKEN - JWT token for authentication
|
||||
*/
|
||||
|
||||
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
||||
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
|
||||
|
||||
const API_URL = process.env.ADEN_API_URL || "http://localhost:3000";
|
||||
const AUTH_TOKEN = process.env.ADEN_AUTH_TOKEN;
|
||||
|
||||
if (!AUTH_TOKEN) {
|
||||
console.error("Error: ADEN_AUTH_TOKEN environment variable is required");
|
||||
console.error("Usage: ADEN_AUTH_TOKEN=your-jwt-token npx ts-node scripts/test-mcp.ts");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log("=".repeat(60));
|
||||
console.log("MCP Server Test");
|
||||
console.log("=".repeat(60));
|
||||
console.log(`API URL: ${API_URL}`);
|
||||
console.log("");
|
||||
|
||||
// Create MCP client
|
||||
const client = new Client({
|
||||
name: "mcp-test-client",
|
||||
version: "1.0.0",
|
||||
});
|
||||
|
||||
// Create SSE transport with auth headers
|
||||
const transport = new SSEClientTransport(new URL(`${API_URL}/mcp`), {
|
||||
requestInit: {
|
||||
headers: {
|
||||
Authorization: `Bearer ${AUTH_TOKEN}`,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
// Connect to MCP server
|
||||
console.log("Connecting to MCP server...");
|
||||
await client.connect(transport);
|
||||
console.log("✓ Connected successfully\n");
|
||||
|
||||
// List available tools
|
||||
console.log("Listing available tools...");
|
||||
const tools = await client.listTools();
|
||||
console.log(`✓ Found ${tools.tools.length} tools:\n`);
|
||||
|
||||
// Group tools by category
|
||||
const categories: Record<string, string[]> = {
|
||||
budget: [],
|
||||
agents: [],
|
||||
analytics: [],
|
||||
policies: [],
|
||||
};
|
||||
|
||||
for (const tool of tools.tools) {
|
||||
if (tool.name.includes("budget")) {
|
||||
categories.budget.push(tool.name);
|
||||
} else if (tool.name.includes("agent")) {
|
||||
categories.agents.push(tool.name);
|
||||
} else if (
|
||||
tool.name.includes("analytics") ||
|
||||
tool.name.includes("insights") ||
|
||||
tool.name.includes("metrics") ||
|
||||
tool.name.includes("logs")
|
||||
) {
|
||||
categories.analytics.push(tool.name);
|
||||
} else if (tool.name.includes("polic")) {
|
||||
categories.policies.push(tool.name);
|
||||
}
|
||||
}
|
||||
|
||||
for (const [category, toolNames] of Object.entries(categories)) {
|
||||
console.log(` ${category.toUpperCase()} (${toolNames.length}):`);
|
||||
for (const name of toolNames) {
|
||||
console.log(` - ${name}`);
|
||||
}
|
||||
}
|
||||
console.log("");
|
||||
|
||||
// Run test scenarios
|
||||
console.log("=".repeat(60));
|
||||
console.log("Running Test Scenarios");
|
||||
console.log("=".repeat(60));
|
||||
console.log("");
|
||||
|
||||
// Test 1: Get policy
|
||||
await runTest(client, "hive_policy_get", { policyId: "default" }, "Get default policy");
|
||||
|
||||
// Test 2: List agents
|
||||
await runTest(client, "hive_agents_summary", {}, "Get agent fleet summary");
|
||||
|
||||
// Test 3: Get insights
|
||||
await runTest(client, "hive_insights", { days: 7 }, "Get 7-day insights");
|
||||
|
||||
// Test 4: Get metrics
|
||||
await runTest(client, "hive_metrics", { days: 30 }, "Get 30-day metrics");
|
||||
|
||||
// Test 5: Budget validation (dry run)
|
||||
await runTest(
|
||||
client,
|
||||
"hive_budget_validate",
|
||||
{
|
||||
estimatedCost: 0.01,
|
||||
context: { agent: "test-agent" },
|
||||
},
|
||||
"Validate budget (dry run)"
|
||||
);
|
||||
|
||||
console.log("=".repeat(60));
|
||||
console.log("All tests completed!");
|
||||
console.log("=".repeat(60));
|
||||
} catch (error) {
|
||||
console.error("Error:", error);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
await client.close();
|
||||
}
|
||||
}
|
||||
|
||||
async function runTest(
|
||||
client: Client,
|
||||
toolName: string,
|
||||
args: Record<string, unknown>,
|
||||
description: string
|
||||
) {
|
||||
console.log(`Test: ${description}`);
|
||||
console.log(` Tool: ${toolName}`);
|
||||
console.log(` Args: ${JSON.stringify(args)}`);
|
||||
|
||||
try {
|
||||
const startTime = Date.now();
|
||||
const result = await client.callTool({ name: toolName, arguments: args });
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
console.log(` Status: ✓ Success (${duration}ms)`);
|
||||
|
||||
// Parse and display result
|
||||
if (result.content && result.content.length > 0) {
|
||||
const textContent = result.content.find((c) => c.type === "text");
|
||||
if (textContent && "text" in textContent) {
|
||||
try {
|
||||
const parsed = JSON.parse(textContent.text);
|
||||
console.log(` Result: ${JSON.stringify(parsed, null, 2).split("\n").slice(0, 10).join("\n")}`);
|
||||
if (JSON.stringify(parsed, null, 2).split("\n").length > 10) {
|
||||
console.log(" ... (truncated)");
|
||||
}
|
||||
} catch {
|
||||
console.log(` Result: ${textContent.text.slice(0, 200)}...`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (result.isError) {
|
||||
console.log(` Warning: Tool returned isError=true`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(` Status: ✗ Failed`);
|
||||
console.log(` Error: ${error instanceof Error ? error.message : error}`);
|
||||
}
|
||||
|
||||
console.log("");
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
-150
@@ -1,150 +0,0 @@
|
||||
/**
|
||||
* Express App Configuration
|
||||
*
|
||||
* Sets up Express with middleware and routes.
|
||||
* No global state - uses dependency injection.
|
||||
* Supports both MySQL (production) and PostgreSQL (local development) for user auth.
|
||||
*/
|
||||
|
||||
import express, { Request, Response } from 'express';
|
||||
import compression from 'compression';
|
||||
import cors from 'cors';
|
||||
import passport from 'passport';
|
||||
import { Pool } from 'pg';
|
||||
|
||||
import { auth, database, models } from '@acho-inc/administration';
|
||||
import config from './config';
|
||||
import routes from './routes';
|
||||
import { errorHandler } from './middleware/error-handler.middleware';
|
||||
import { createMcpRouter } from './mcp';
|
||||
|
||||
// Initialize Express app
|
||||
const app = express();
|
||||
|
||||
// =============================================================================
|
||||
// Middleware
|
||||
// =============================================================================
|
||||
|
||||
app.use(compression({
|
||||
filter: (req, res) => {
|
||||
// Don't compress SSE responses - compression breaks streaming
|
||||
if (req.headers.accept === 'text/event-stream' ||
|
||||
req.path.endsWith('/stream')) {
|
||||
return false;
|
||||
}
|
||||
return compression.filter(req, res);
|
||||
}
|
||||
}));
|
||||
app.use(cors());
|
||||
|
||||
// Skip body parsing for MCP message route (SDK's handlePostMessage reads raw body stream)
|
||||
app.use((req, res, next) => {
|
||||
if (req.path === '/mcp/message') {
|
||||
return next();
|
||||
}
|
||||
express.json({ limit: '10mb' })(req, res, next);
|
||||
});
|
||||
app.use((req, res, next) => {
|
||||
if (req.path === '/mcp/message') {
|
||||
return next();
|
||||
}
|
||||
express.urlencoded({ extended: true })(req, res, next);
|
||||
});
|
||||
|
||||
// Disable x-powered-by header
|
||||
app.disable('x-powered-by');
|
||||
|
||||
// =============================================================================
|
||||
// Database Connections
|
||||
// =============================================================================
|
||||
|
||||
let userDbService: ReturnType<typeof models.createUserDbService>;
|
||||
|
||||
if (config.userDbType === 'postgres') {
|
||||
// PostgreSQL for local development
|
||||
console.log('[App] Using PostgreSQL for user authentication');
|
||||
|
||||
const pgPool = new Pool({
|
||||
connectionString: config.userDb.url,
|
||||
});
|
||||
|
||||
userDbService = models.createUserDbService({
|
||||
pgPool,
|
||||
dbType: 'postgres',
|
||||
tables: {
|
||||
USER: 'users',
|
||||
DEVELOPERS: 'developers',
|
||||
},
|
||||
});
|
||||
|
||||
app.locals.pgPool = pgPool;
|
||||
} else {
|
||||
// MySQL for production
|
||||
console.log('[App] Using MySQL for user authentication');
|
||||
|
||||
const mysqlPool = database.createMySQLPool(config.mysql);
|
||||
|
||||
userDbService = models.createUserDbService({
|
||||
mysqlPool,
|
||||
tables: {
|
||||
USER: 'user',
|
||||
DEVELOPERS: 'developers',
|
||||
},
|
||||
});
|
||||
|
||||
app.locals.mysqlPool = mysqlPool;
|
||||
}
|
||||
|
||||
// Store user service in app.locals for access in routes
|
||||
app.locals.userDbService = userDbService;
|
||||
|
||||
// =============================================================================
|
||||
// Passport Authentication
|
||||
// =============================================================================
|
||||
|
||||
const passportStrategy = auth.createPassportStrategy({
|
||||
findSaltByToken: userDbService.findSaltByToken,
|
||||
jwtSecret: config.jwt.secret,
|
||||
});
|
||||
|
||||
passport.use(passportStrategy);
|
||||
app.use(passport.initialize());
|
||||
|
||||
// =============================================================================
|
||||
// Routes
|
||||
// =============================================================================
|
||||
|
||||
// Health check (unauthenticated)
|
||||
app.get('/health', (req: Request, res: Response) => {
|
||||
res.json({
|
||||
status: 'ok',
|
||||
service: 'aden-hive',
|
||||
timestamp: new Date().toISOString(),
|
||||
userDbType: config.userDbType,
|
||||
});
|
||||
});
|
||||
|
||||
// API routes
|
||||
app.use('/', routes);
|
||||
|
||||
// MCP Server routes (Model Context Protocol)
|
||||
// The controlEmitter is set in index.ts after WebSocket initialization
|
||||
const mcpRouter = createMcpRouter(() => app.locals.controlEmitter);
|
||||
app.use('/mcp', mcpRouter);
|
||||
|
||||
// =============================================================================
|
||||
// Error Handling
|
||||
// =============================================================================
|
||||
|
||||
// 404 handler
|
||||
app.use((req: Request, res: Response) => {
|
||||
res.status(404).json({
|
||||
error: 'not_found',
|
||||
message: `Route ${req.method} ${req.path} not found`,
|
||||
});
|
||||
});
|
||||
|
||||
// Global error handler
|
||||
app.use(errorHandler);
|
||||
|
||||
export default app;
|
||||
@@ -1,134 +0,0 @@
|
||||
/**
|
||||
* Configuration Module
|
||||
*
|
||||
* Centralizes all configuration loading and validation.
|
||||
* Supports both MySQL (production) and PostgreSQL (local development) for user database.
|
||||
*/
|
||||
|
||||
import fs from 'fs';
|
||||
|
||||
/**
|
||||
* Helper function to safely read SSL certificates
|
||||
* @param {string} envKey - Environment variable containing cert path
|
||||
* @param {string} fallbackPath - Fallback path if env var not set
|
||||
* @returns {Buffer|null} Certificate content or null
|
||||
*/
|
||||
function readCertificate(envKey: string, fallbackPath: string): Buffer | null {
|
||||
const certPath = process.env[envKey];
|
||||
if (certPath && fs.existsSync(certPath)) {
|
||||
return fs.readFileSync(certPath);
|
||||
}
|
||||
if (fallbackPath && fs.existsSync(fallbackPath)) {
|
||||
return fs.readFileSync(fallbackPath);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load MySQL SSL certificates from environment or default paths
|
||||
* @returns {Object|null} SSL config object or null if certs not found
|
||||
*/
|
||||
function loadMySQLSSL(): { ca: Buffer; key: Buffer; cert: Buffer } | null {
|
||||
const ca = readCertificate('MYSQL_SSL_CA', '/mnt/certs/mysql/server-ca.pem');
|
||||
const key = readCertificate('MYSQL_SSL_KEY', '/mnt/certs/mysql/client-key.pem');
|
||||
const cert = readCertificate('MYSQL_SSL_CERT', '/mnt/certs/mysql/client-cert.pem');
|
||||
|
||||
return ca && key && cert ? { ca, key, cert } : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine which database type to use for user authentication
|
||||
* Priority: USER_DB_TYPE env var > MySQL if configured > PostgreSQL fallback
|
||||
*/
|
||||
function getUserDbType(): 'mysql' | 'postgres' {
|
||||
const explicit = process.env.USER_DB_TYPE?.toLowerCase();
|
||||
if (explicit === 'mysql' || explicit === 'postgres') {
|
||||
return explicit;
|
||||
}
|
||||
// Default to MySQL if MySQL host is configured, otherwise use PostgreSQL
|
||||
return process.env.MYSQL_HOST ? 'mysql' : 'postgres';
|
||||
}
|
||||
|
||||
const config = {
|
||||
// Server
|
||||
port: parseInt(process.env.PORT as string, 10) || 4000,
|
||||
nodeEnv: process.env.NODE_ENV || 'development',
|
||||
|
||||
// TSDB PostgreSQL (metrics storage)
|
||||
tsdb: {
|
||||
url: process.env.TSDB_PG_URL,
|
||||
},
|
||||
|
||||
// User Database Type ('mysql' or 'postgres')
|
||||
userDbType: getUserDbType(),
|
||||
|
||||
// User Database (MySQL) - for production
|
||||
mysql: {
|
||||
host: process.env.MYSQL_HOST,
|
||||
port: parseInt(process.env.MYSQL_PORT as string, 10) || 3306,
|
||||
user: process.env.MYSQL_USER,
|
||||
password: process.env.MYSQL_PASSWORD,
|
||||
database: process.env.MYSQL_DATABASE,
|
||||
ssl: loadMySQLSSL(),
|
||||
},
|
||||
|
||||
// User Database (PostgreSQL) - for local development
|
||||
// Defaults to same DB as TSDB if not specified
|
||||
userDb: {
|
||||
url: process.env.USER_DB_PG_URL || process.env.TSDB_PG_URL,
|
||||
},
|
||||
|
||||
// MongoDB
|
||||
mongodb: {
|
||||
url: process.env.MONGODB_URL,
|
||||
dbName: process.env.MONGODB_DBNAME || 'aden',
|
||||
erpDbName: process.env.MONGODB_ERP_DBNAME || 'erp',
|
||||
},
|
||||
|
||||
// Redis
|
||||
redis: {
|
||||
url: process.env.REDIS_URL,
|
||||
},
|
||||
|
||||
// JWT
|
||||
jwt: {
|
||||
secret: process.env.JWT_SECRET || 'dev-secret-change-in-production',
|
||||
expiresIn: process.env.JWT_EXPIRES_IN || '7d',
|
||||
passphrase: process.env.PASSPHRASE,
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Validates required configuration
|
||||
* @throws {Error} If required config is missing
|
||||
*/
|
||||
function validateConfig(): void {
|
||||
const required: [string, string | undefined][] = [
|
||||
['TSDB_PG_URL', config.tsdb.url],
|
||||
];
|
||||
|
||||
// Add database-specific requirements
|
||||
if (config.userDbType === 'mysql') {
|
||||
required.push(
|
||||
['MYSQL_HOST', config.mysql.host],
|
||||
['MYSQL_USER', config.mysql.user],
|
||||
['MYSQL_DATABASE', config.mysql.database],
|
||||
);
|
||||
} else {
|
||||
required.push(['USER_DB_PG_URL or TSDB_PG_URL', config.userDb.url]);
|
||||
}
|
||||
|
||||
const missing = required.filter(([, value]) => !value);
|
||||
|
||||
if (missing.length > 0) {
|
||||
const names = missing.map(([name]) => name).join(', ');
|
||||
console.warn(`[Config] Warning: Missing environment variables: ${names}`);
|
||||
}
|
||||
|
||||
console.log(`[Config] User database type: ${config.userDbType}`);
|
||||
}
|
||||
|
||||
// Validate on load
|
||||
validateConfig();
|
||||
|
||||
export default config;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,154 +0,0 @@
|
||||
/**
|
||||
* IAM Controller
|
||||
*
|
||||
* Handles Identity and Access Management endpoints.
|
||||
*/
|
||||
|
||||
import { Router, Request, Response } from 'express';
|
||||
|
||||
const router = Router();
|
||||
|
||||
/**
|
||||
* Extract token from Authorization header
|
||||
* Supports: "jwt <token>", "Bearer <token>", or raw "<token>"
|
||||
*/
|
||||
function extractToken(authHeader: string): string {
|
||||
if (authHeader.startsWith('jwt ')) {
|
||||
return authHeader.slice(4);
|
||||
}
|
||||
if (authHeader.startsWith('Bearer ')) {
|
||||
return authHeader.slice(7);
|
||||
}
|
||||
return authHeader;
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /iam/get-current-team
|
||||
*
|
||||
* Get the current team/organization for the authenticated user.
|
||||
*/
|
||||
router.get('/get-current-team', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const authHeader = req.headers.authorization;
|
||||
if (!authHeader) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
msg: 'No token provided',
|
||||
});
|
||||
}
|
||||
|
||||
const userDbService = req.app.locals.userDbService;
|
||||
const user = await userDbService.findByToken(extractToken(authHeader));
|
||||
|
||||
if (!user) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
msg: 'Invalid token',
|
||||
});
|
||||
}
|
||||
|
||||
const pgPool = req.app.locals.pgPool;
|
||||
if (!pgPool) {
|
||||
// Return default team if no database
|
||||
return res.json({
|
||||
orgId: user.current_team_id || 1,
|
||||
orgName: 'Default Organization',
|
||||
teamId: user.current_team_id || 1,
|
||||
teamName: 'Default Team',
|
||||
});
|
||||
}
|
||||
|
||||
// Get team info from database
|
||||
const result = await pgPool.query(
|
||||
`SELECT id, name, slug FROM teams WHERE id = $1`,
|
||||
[user.current_team_id || 1]
|
||||
);
|
||||
|
||||
const team = result.rows[0];
|
||||
|
||||
if (!team) {
|
||||
// Return default if team not found
|
||||
return res.json({
|
||||
orgId: user.current_team_id || 1,
|
||||
orgName: 'Default Organization',
|
||||
teamId: user.current_team_id || 1,
|
||||
teamName: 'Default Team',
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
orgId: team.id,
|
||||
orgName: team.name,
|
||||
teamId: team.id,
|
||||
teamName: team.name,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('[IAMController] /get-current-team error:', err instanceof Error ? err.message : err);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
msg: 'Failed to get current team',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /iam/team/get-team-role-by-id/:teamId
|
||||
*
|
||||
* Get the user's role in a specific team.
|
||||
*/
|
||||
router.get('/team/get-team-role-by-id/:teamId', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const authHeader = req.headers.authorization;
|
||||
if (!authHeader) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
msg: 'No token provided',
|
||||
});
|
||||
}
|
||||
|
||||
const userDbService = req.app.locals.userDbService;
|
||||
const user = await userDbService.findByToken(extractToken(authHeader));
|
||||
|
||||
if (!user) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
msg: 'Invalid token',
|
||||
});
|
||||
}
|
||||
|
||||
const teamId = parseInt(req.params.teamId, 10);
|
||||
|
||||
const pgPool = req.app.locals.pgPool;
|
||||
if (!pgPool) {
|
||||
// Return default role if no database
|
||||
return res.json({ roleId: 1 });
|
||||
}
|
||||
|
||||
// Get user's role in this team
|
||||
const result = await pgPool.query(
|
||||
`SELECT role FROM team_members WHERE user_id = $1 AND team_id = $2`,
|
||||
[user.id, teamId]
|
||||
);
|
||||
|
||||
const membership = result.rows[0];
|
||||
|
||||
// Map role name to roleId (admin=1, member=2, viewer=3)
|
||||
const roleMap: Record<string, number> = {
|
||||
admin: 1,
|
||||
member: 2,
|
||||
viewer: 3,
|
||||
};
|
||||
|
||||
const roleId = membership ? (roleMap[membership.role] || 2) : 2;
|
||||
|
||||
res.json({ roleId });
|
||||
} catch (err) {
|
||||
console.error('[IAMController] /team/get-team-role-by-id error:', err instanceof Error ? err.message : err);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
msg: 'Failed to get team role',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -1,192 +0,0 @@
|
||||
/**
|
||||
* Quickstart Documentation API Controller
|
||||
* Generates SDK quickstart documentation based on agent framework
|
||||
*/
|
||||
import express, { Request, Response, NextFunction } from "express";
|
||||
import passport from "passport";
|
||||
// Passport is initialized in app.js
|
||||
|
||||
import * as quickstartService from "../services/quickstart/quickstart_service";
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
interface AuthenticatedUser {
|
||||
id: number;
|
||||
current_team_id: number;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
interface AuthenticatedRequest extends Request {
|
||||
user?: AuthenticatedUser;
|
||||
}
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /quickstart/options:
|
||||
* get:
|
||||
* summary: Get available options for quickstart generation
|
||||
* tags:
|
||||
* - Quickstart
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Available options for quickstart document generation
|
||||
*/
|
||||
router.get("/options", async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const options = quickstartService.getQuickstartOptions();
|
||||
res.send(options);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /quickstart/generate:
|
||||
* post:
|
||||
* summary: Generate quickstart documentation with user's system token
|
||||
* tags:
|
||||
* - Quickstart
|
||||
* security:
|
||||
* - jwtAuth: []
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* required:
|
||||
* - agentFramework
|
||||
* properties:
|
||||
* agentFramework:
|
||||
* type: string
|
||||
* enum: [generic, langgraph, livekit]
|
||||
* description: The agent framework to use
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Generated quickstart documentation
|
||||
* 400:
|
||||
* description: Invalid parameters
|
||||
* 401:
|
||||
* description: Unauthorized - JWT token required
|
||||
*/
|
||||
router.post(
|
||||
"/generate",
|
||||
passport.authenticate("jwt", { session: false }),
|
||||
async (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { user, body } = req;
|
||||
const { agentFramework, llmVendor, sdkLanguage } = body;
|
||||
|
||||
// Get the user's latest non-system API key
|
||||
const userDbService = req.app.locals.userDbService;
|
||||
const tokenObj = user ? await userDbService.getLatestUserDevToken(user) : null;
|
||||
|
||||
let apiKey: string;
|
||||
let tokenName: string;
|
||||
if (tokenObj) {
|
||||
apiKey = tokenObj.token;
|
||||
tokenName = tokenObj.label;
|
||||
} else {
|
||||
// No user API key - use placeholder
|
||||
apiKey = "eyJ-xxx";
|
||||
tokenName = "No Key";
|
||||
}
|
||||
|
||||
// Generate the quickstart document
|
||||
const markdown = quickstartService.generateQuickstart({
|
||||
agentFramework,
|
||||
llmVendor,
|
||||
sdkLanguage,
|
||||
apiKey,
|
||||
});
|
||||
|
||||
res.send({
|
||||
markdown,
|
||||
metadata: {
|
||||
agentFramework,
|
||||
llmVendor,
|
||||
sdkLanguage,
|
||||
tokenName,
|
||||
generatedAt: new Date().toISOString(),
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
if ((error as Error).message.includes("Invalid")) {
|
||||
return res.status(400).send({ error: (error as Error).message });
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /quickstart/generate-with-key:
|
||||
* post:
|
||||
* summary: Generate quickstart documentation with a provided API key
|
||||
* description: Generate documentation without requiring authentication - API key is provided directly
|
||||
* tags:
|
||||
* - Quickstart
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* required:
|
||||
* - agentFramework
|
||||
* - apiKey
|
||||
* properties:
|
||||
* agentFramework:
|
||||
* type: string
|
||||
* enum: [generic, livekit]
|
||||
* apiKey:
|
||||
* type: string
|
||||
* description: The Aden API key to embed in the documentation
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Generated quickstart documentation
|
||||
* 400:
|
||||
* description: Invalid parameters
|
||||
*/
|
||||
router.post("/generate-with-key", async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { agentFramework, llmVendor, sdkLanguage, apiKey } = req.body;
|
||||
|
||||
if (!apiKey) {
|
||||
return res.status(400).send({
|
||||
error: "API key is required",
|
||||
message: "Please provide an apiKey in the request body",
|
||||
});
|
||||
}
|
||||
|
||||
// Generate the quickstart document
|
||||
const markdown = quickstartService.generateQuickstart({
|
||||
agentFramework,
|
||||
llmVendor,
|
||||
sdkLanguage,
|
||||
apiKey,
|
||||
});
|
||||
|
||||
res.send({
|
||||
markdown,
|
||||
metadata: {
|
||||
agentFramework,
|
||||
llmVendor,
|
||||
sdkLanguage,
|
||||
generatedAt: new Date().toISOString(),
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
if (
|
||||
(error as Error).message.includes("Invalid") ||
|
||||
(error as Error).message.includes("required")
|
||||
) {
|
||||
return res.status(400).send({ error: (error as Error).message });
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,618 +0,0 @@
|
||||
/**
|
||||
* User Controller
|
||||
*
|
||||
* Handles user authentication endpoints including login-v2.
|
||||
*/
|
||||
|
||||
import { Router, Request, Response, NextFunction } from "express";
|
||||
import config from "../config";
|
||||
|
||||
const router = Router();
|
||||
|
||||
/**
|
||||
* Extract token from Authorization header
|
||||
* Supports: "jwt <token>", "Bearer <token>", or raw "<token>"
|
||||
*/
|
||||
function extractToken(authHeader: string): string {
|
||||
if (authHeader.startsWith("jwt ")) {
|
||||
return authHeader.slice(4);
|
||||
}
|
||||
if (authHeader.startsWith("Bearer ")) {
|
||||
return authHeader.slice(7);
|
||||
}
|
||||
return authHeader;
|
||||
}
|
||||
|
||||
// Email validation regex
|
||||
const EMAIL_REGEX =
|
||||
/[\w!#$%&'*+/=?^_`{|}~-]+(?:\.[\w!#$%&'*+/=?^_`{|}~-]+)*@(?:[\w](?:[\w-]*[\w])?\.)+[\w](?:[\w-]*[\w])?/;
|
||||
|
||||
/**
|
||||
* POST /user/login-v2
|
||||
*
|
||||
* Authenticate a user with email and password.
|
||||
* Returns a JWT token on success.
|
||||
*/
|
||||
router.post(
|
||||
"/login-v2",
|
||||
async (req: Request, res: Response, _next: NextFunction) => {
|
||||
try {
|
||||
let { email } = req.body;
|
||||
const { password } = req.body;
|
||||
|
||||
// Validate required fields
|
||||
if (
|
||||
!email ||
|
||||
typeof email !== "string" ||
|
||||
!password ||
|
||||
typeof password !== "string"
|
||||
) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
msg: "Email and password are required",
|
||||
});
|
||||
}
|
||||
|
||||
// Validate email format
|
||||
if (!EMAIL_REGEX.test(email)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
msg: "Please enter a valid email",
|
||||
});
|
||||
}
|
||||
|
||||
// Trim email
|
||||
email = email.trim().toLowerCase();
|
||||
|
||||
// Validate password length
|
||||
if (password.length < 6) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
msg: "Password must be at least 6 characters",
|
||||
});
|
||||
}
|
||||
|
||||
// Get userDbService from app.locals
|
||||
const userDbService = req.app.locals.userDbService;
|
||||
if (!userDbService) {
|
||||
console.error("[UserController] userDbService not found in app.locals");
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
msg: "Internal server error",
|
||||
});
|
||||
}
|
||||
|
||||
// Attempt login
|
||||
const result = await userDbService.login(email, password, {
|
||||
jwtSecret: config.jwt.secret,
|
||||
expiresIn: config.jwt.expiresIn,
|
||||
});
|
||||
|
||||
console.log(
|
||||
`[UserController] login-v2: User ${email} logged in successfully`
|
||||
);
|
||||
|
||||
// Return success response
|
||||
res.json({
|
||||
success: true,
|
||||
token: result.token,
|
||||
email: result.email,
|
||||
firstname: result.firstname,
|
||||
lastname: result.lastname,
|
||||
name: result.name,
|
||||
current_team_id: result.current_team_id,
|
||||
create_time: result.created_at,
|
||||
});
|
||||
} catch (err) {
|
||||
const error = err as { message?: string; code?: string };
|
||||
console.error("[UserController] login-v2 error:", error.message);
|
||||
|
||||
// Handle specific error codes
|
||||
if (
|
||||
error.code === "USER_NOT_FOUND" ||
|
||||
error.code === "INVALID_CREDENTIALS"
|
||||
) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
msg: "Invalid email or password",
|
||||
});
|
||||
}
|
||||
|
||||
if (error.code === "OAUTH_REQUIRED") {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
msg: error.message,
|
||||
});
|
||||
}
|
||||
|
||||
if (error.code === "ACCOUNT_DISABLED") {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
msg: "Your account has been disabled",
|
||||
});
|
||||
}
|
||||
|
||||
// Generic error
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
msg: "Login failed. Please try again.",
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* POST /user/register
|
||||
*
|
||||
* Register a new user account.
|
||||
* Returns a JWT token on success.
|
||||
*/
|
||||
router.post("/register", async (req: Request, res: Response) => {
|
||||
try {
|
||||
let { email } = req.body;
|
||||
const { password, name, firstname, lastname } = req.body;
|
||||
|
||||
// Validate required fields
|
||||
if (
|
||||
!email ||
|
||||
typeof email !== "string" ||
|
||||
!password ||
|
||||
typeof password !== "string"
|
||||
) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
msg: "Email and password are required",
|
||||
});
|
||||
}
|
||||
|
||||
// Validate email format
|
||||
if (!EMAIL_REGEX.test(email)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
msg: "Please enter a valid email",
|
||||
});
|
||||
}
|
||||
|
||||
// Trim and lowercase email
|
||||
email = email.trim().toLowerCase();
|
||||
|
||||
// Validate password length
|
||||
if (password.length < 8) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
msg: "Password must be at least 8 characters",
|
||||
});
|
||||
}
|
||||
|
||||
// Get userDbService from app.locals
|
||||
const userDbService = req.app.locals.userDbService;
|
||||
if (!userDbService) {
|
||||
console.error("[UserController] userDbService not found in app.locals");
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
msg: "Internal server error",
|
||||
});
|
||||
}
|
||||
|
||||
// Attempt registration
|
||||
const result = await userDbService.register(
|
||||
{ email, password, name, firstname, lastname },
|
||||
{
|
||||
jwtSecret: config.jwt.secret,
|
||||
expiresIn: config.jwt.expiresIn,
|
||||
defaultTeamId: 1, // Default to team 1 for local dev
|
||||
}
|
||||
);
|
||||
|
||||
console.log(
|
||||
`[UserController] register: User ${email} registered successfully`
|
||||
);
|
||||
|
||||
// Return success response
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
token: result.token,
|
||||
email: result.email,
|
||||
name: result.name,
|
||||
firstname: result.firstname,
|
||||
lastname: result.lastname,
|
||||
current_team_id: result.current_team_id,
|
||||
create_time: result.created_at,
|
||||
});
|
||||
} catch (err: any) {
|
||||
console.error("[UserController] register error:", err.message);
|
||||
|
||||
// Handle specific error codes
|
||||
if (err.code === "EMAIL_EXISTS") {
|
||||
return res.status(409).json({
|
||||
success: false,
|
||||
msg: "Email already registered",
|
||||
});
|
||||
}
|
||||
|
||||
// Generic error
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
msg: "Registration failed. Please try again.",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /user/profile
|
||||
*
|
||||
* Get current user profile.
|
||||
* Requires authentication.
|
||||
*/
|
||||
router.get("/profile", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const authHeader = req.headers.authorization;
|
||||
if (!authHeader) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
msg: "No token provided",
|
||||
});
|
||||
}
|
||||
|
||||
const userDbService = req.app.locals.userDbService;
|
||||
const user = await userDbService.findByToken(extractToken(authHeader));
|
||||
|
||||
if (!user) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
msg: "Invalid token",
|
||||
});
|
||||
}
|
||||
|
||||
// Return in format expected by frontend
|
||||
res.json({
|
||||
data: {
|
||||
firstname: user.firstname || "",
|
||||
lastname: user.lastname || "",
|
||||
email: user.email,
|
||||
company_name: user.company_name || null,
|
||||
profile_img_url: user.avatar_url || null,
|
||||
roleId: user.role_id || 1,
|
||||
user_id: String(user.id),
|
||||
team_id: String(user.current_team_id || 1),
|
||||
roles: user.roles || ["user"],
|
||||
},
|
||||
});
|
||||
} catch (err: any) {
|
||||
console.error("[UserController] /profile error:", err.message);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
msg: "Failed to get user profile",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* PUT /user/profile
|
||||
*
|
||||
* Update current user profile.
|
||||
* Requires authentication.
|
||||
*/
|
||||
router.put("/profile", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const authHeader = req.headers.authorization;
|
||||
if (!authHeader) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
msg: "No token provided",
|
||||
});
|
||||
}
|
||||
|
||||
const userDbService = req.app.locals.userDbService;
|
||||
const user = await userDbService.findByToken(extractToken(authHeader));
|
||||
|
||||
if (!user) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
msg: "Invalid token",
|
||||
});
|
||||
}
|
||||
|
||||
const { firstname, lastname } = req.body;
|
||||
|
||||
// Update user profile (basic implementation)
|
||||
if (userDbService.updateProfile) {
|
||||
await userDbService.updateProfile(user.id, { firstname, lastname });
|
||||
}
|
||||
|
||||
res.json({ message: "Profile updated successfully" });
|
||||
} catch (err: any) {
|
||||
console.error("[UserController] PUT /profile error:", err.message);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
msg: "Failed to update profile",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /user/me
|
||||
*
|
||||
* Get current user info from token.
|
||||
* Requires authentication.
|
||||
*/
|
||||
router.get("/me", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const authHeader = req.headers.authorization;
|
||||
if (!authHeader) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
msg: "No token provided",
|
||||
});
|
||||
}
|
||||
|
||||
const userDbService = req.app.locals.userDbService;
|
||||
const user = await userDbService.findByToken(extractToken(authHeader));
|
||||
|
||||
if (!user) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
msg: "Invalid token",
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
user: {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
firstname: user.firstname,
|
||||
lastname: user.lastname,
|
||||
current_team_id: user.current_team_id,
|
||||
avatar_url: user.avatar_url,
|
||||
},
|
||||
});
|
||||
} catch (err: any) {
|
||||
console.error("[UserController] /me error:", err.message);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
msg: "Failed to get user info",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /user/get-dev-tokens
|
||||
*
|
||||
* Get all developer API tokens for the current user.
|
||||
* Requires authentication.
|
||||
*/
|
||||
router.get("/get-dev-tokens", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const authHeader = req.headers.authorization;
|
||||
if (!authHeader) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
msg: "No token provided",
|
||||
});
|
||||
}
|
||||
|
||||
const userDbService = req.app.locals.userDbService;
|
||||
const user = await userDbService.findByToken(extractToken(authHeader));
|
||||
|
||||
if (!user) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
msg: "Invalid token",
|
||||
});
|
||||
}
|
||||
|
||||
const tokens = await userDbService.getDevTokens(user);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: tokens,
|
||||
});
|
||||
} catch (err: any) {
|
||||
console.error("[UserController] /get-dev-tokens error:", err.message);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
msg: "Failed to get API tokens",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /user/generate-dev-token
|
||||
*
|
||||
* Generate a new developer API token.
|
||||
* Requires authentication.
|
||||
*/
|
||||
router.post("/generate-dev-token", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const authHeader = req.headers.authorization;
|
||||
if (!authHeader) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
msg: "No token provided",
|
||||
});
|
||||
}
|
||||
|
||||
const userDbService = req.app.locals.userDbService;
|
||||
const user = await userDbService.findByToken(extractToken(authHeader));
|
||||
|
||||
if (!user) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
msg: "Invalid token",
|
||||
});
|
||||
}
|
||||
|
||||
const { label, ttl } = req.body;
|
||||
|
||||
const tokenResult = await userDbService.generateDevToken(user, {
|
||||
label,
|
||||
ttl,
|
||||
jwtSecret: config.jwt.secret,
|
||||
});
|
||||
|
||||
console.log(
|
||||
`[UserController] generate-dev-token: Created token for user ${user.id}`
|
||||
);
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
data: tokenResult,
|
||||
});
|
||||
} catch (err: any) {
|
||||
console.error("[UserController] /generate-dev-token error:", err.message);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
msg: "Failed to generate API token",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// UI Settings Endpoints
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Default UI settings for new users
|
||||
*/
|
||||
const DEFAULT_UI_SETTINGS = {
|
||||
sidebarCollapsed: false,
|
||||
performanceDashboardTimeRange: "today",
|
||||
};
|
||||
|
||||
/**
|
||||
* GET /user/settings
|
||||
*
|
||||
* Get user UI settings from preferences column.
|
||||
* Returns defaults if no settings exist.
|
||||
*/
|
||||
router.get("/settings", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const authHeader = req.headers.authorization;
|
||||
if (!authHeader) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
msg: "No token provided",
|
||||
});
|
||||
}
|
||||
|
||||
const userDbService = req.app.locals.userDbService;
|
||||
const user = await userDbService.findByToken(extractToken(authHeader));
|
||||
|
||||
if (!user) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
msg: "Invalid token",
|
||||
});
|
||||
}
|
||||
|
||||
// Extract UI settings from preferences, merge with defaults
|
||||
const preferences = user.preferences || {};
|
||||
const uiSettings = {
|
||||
sidebarCollapsed:
|
||||
preferences.sidebarCollapsed ?? DEFAULT_UI_SETTINGS.sidebarCollapsed,
|
||||
performanceDashboardTimeRange:
|
||||
preferences.performanceDashboardTimeRange ??
|
||||
DEFAULT_UI_SETTINGS.performanceDashboardTimeRange,
|
||||
};
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: uiSettings,
|
||||
});
|
||||
} catch (err: any) {
|
||||
console.error("[UserController] GET /settings error:", err.message);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
msg: "Failed to get settings",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* PUT /user/settings
|
||||
*
|
||||
* Update user UI settings in preferences column.
|
||||
* Supports partial updates - merges with existing preferences.
|
||||
*/
|
||||
router.put("/settings", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const authHeader = req.headers.authorization;
|
||||
if (!authHeader) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
msg: "No token provided",
|
||||
});
|
||||
}
|
||||
|
||||
const userDbService = req.app.locals.userDbService;
|
||||
const user = await userDbService.findByToken(extractToken(authHeader));
|
||||
|
||||
if (!user) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
msg: "Invalid token",
|
||||
});
|
||||
}
|
||||
|
||||
const { sidebarCollapsed, performanceDashboardTimeRange } = req.body;
|
||||
|
||||
// Build update object with only provided fields
|
||||
const updates: Record<string, any> = {};
|
||||
if (typeof sidebarCollapsed === "boolean") {
|
||||
updates.sidebarCollapsed = sidebarCollapsed;
|
||||
}
|
||||
if (performanceDashboardTimeRange !== undefined) {
|
||||
updates.performanceDashboardTimeRange = performanceDashboardTimeRange;
|
||||
}
|
||||
|
||||
// Merge with existing preferences
|
||||
const currentPreferences = user.preferences || {};
|
||||
const newPreferences = { ...currentPreferences, ...updates };
|
||||
|
||||
// Update in database - use pgPool for Postgres, mysqlPool for MySQL
|
||||
const pgPool = req.app.locals.pgPool;
|
||||
const mysqlPool = req.app.locals.mysqlPool;
|
||||
|
||||
if (pgPool) {
|
||||
// PostgreSQL - use JSONB
|
||||
await pgPool.query(
|
||||
"UPDATE users SET preferences = $1, updated_at = NOW() WHERE id = $2",
|
||||
[JSON.stringify(newPreferences), user.id]
|
||||
);
|
||||
} else if (mysqlPool) {
|
||||
// MySQL - use JSON column
|
||||
await mysqlPool.query(
|
||||
"UPDATE user SET preferences = ?, updated_at = NOW() WHERE id = ?",
|
||||
[JSON.stringify(newPreferences), user.id]
|
||||
);
|
||||
} else {
|
||||
console.warn(
|
||||
"[UserController] PUT /settings: No database pool available, settings not persisted"
|
||||
);
|
||||
}
|
||||
|
||||
// Return updated settings
|
||||
const uiSettings = {
|
||||
sidebarCollapsed:
|
||||
newPreferences.sidebarCollapsed ?? DEFAULT_UI_SETTINGS.sidebarCollapsed,
|
||||
performanceDashboardTimeRange:
|
||||
newPreferences.performanceDashboardTimeRange ??
|
||||
DEFAULT_UI_SETTINGS.performanceDashboardTimeRange,
|
||||
};
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: uiSettings,
|
||||
});
|
||||
} catch (err: any) {
|
||||
console.error("[UserController] PUT /settings error:", err.message);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
msg: "Failed to update settings",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -1,120 +0,0 @@
|
||||
/**
|
||||
* Aden Hive - DevTool Backend Entry Point
|
||||
*
|
||||
* LLM observability and control plane service.
|
||||
*/
|
||||
|
||||
import "dotenv/config";
|
||||
|
||||
import http from "http";
|
||||
import { MongoClient } from "mongodb";
|
||||
import app from "./app";
|
||||
import config from "./config";
|
||||
import { initializeSockets, setUserDbService } from "./sockets/control.socket";
|
||||
|
||||
const PORT = process.env.PORT || 4000;
|
||||
|
||||
// Declare globals for MongoDB (used by services)
|
||||
// eslint-disable-next-line no-var
|
||||
declare global {
|
||||
// eslint-disable-next-line no-var
|
||||
var _ACHO_MG_DB: MongoClient;
|
||||
// eslint-disable-next-line no-var
|
||||
var _ACHO_MDB_CONFIG: { ERP_DBNAME: string; DBNAME: string };
|
||||
// eslint-disable-next-line no-var
|
||||
var _ACHO_MDB_COLLECTIONS: {
|
||||
ADEN_CONTROL_POLICIES: string;
|
||||
ADEN_CONTROL_CONTENT: string;
|
||||
LLM_PRICING: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize MongoDB connection
|
||||
*/
|
||||
async function initMongoDB(): Promise<void> {
|
||||
if (!config.mongodb.url) {
|
||||
console.warn(
|
||||
"[MongoDB] No MONGODB_URL configured, skipping MongoDB initialization"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const client = new MongoClient(config.mongodb.url);
|
||||
await client.connect();
|
||||
|
||||
// Set global MongoDB client and config
|
||||
global._ACHO_MG_DB = client;
|
||||
global._ACHO_MDB_CONFIG = {
|
||||
ERP_DBNAME: config.mongodb.erpDbName,
|
||||
DBNAME: config.mongodb.dbName,
|
||||
};
|
||||
global._ACHO_MDB_COLLECTIONS = {
|
||||
ADEN_CONTROL_POLICIES: "aden_control_policies",
|
||||
ADEN_CONTROL_CONTENT: "aden_control_content",
|
||||
LLM_PRICING: "llm_pricing",
|
||||
};
|
||||
|
||||
console.log("[MongoDB] Connected successfully");
|
||||
} catch (error) {
|
||||
console.error("[MongoDB] Connection error:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Create HTTP server
|
||||
const server = http.createServer(app);
|
||||
|
||||
/**
|
||||
* Start the server
|
||||
*/
|
||||
async function start(): Promise<void> {
|
||||
// Initialize MongoDB
|
||||
await initMongoDB();
|
||||
|
||||
// Pass userDbService to socket layer for JWT verification
|
||||
if (app.locals.userDbService) {
|
||||
setUserDbService(app.locals.userDbService, config.jwt.secret);
|
||||
}
|
||||
|
||||
// Initialize WebSockets
|
||||
const { controlEmitter } = await initializeSockets(server);
|
||||
|
||||
// Make control emitter available for policy updates
|
||||
app.locals.controlEmitter = controlEmitter;
|
||||
console.log("[Aden Hive] WebSocket initialized");
|
||||
|
||||
// Start server
|
||||
server.listen(PORT, () => {
|
||||
console.log(`[Aden Hive] Server running on port ${PORT}`);
|
||||
console.log(
|
||||
`[Aden Hive] Environment: ${process.env.NODE_ENV || "development"}`
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// Start the application
|
||||
start().catch((error) => {
|
||||
console.error("[Aden Hive] Failed to start:", error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
// Graceful shutdown
|
||||
process.on("SIGTERM", () => {
|
||||
console.log("[Aden Hive] SIGTERM received, shutting down gracefully");
|
||||
server.close(() => {
|
||||
console.log("[Aden Hive] Server closed");
|
||||
process.exit(0);
|
||||
});
|
||||
});
|
||||
|
||||
process.on("SIGINT", () => {
|
||||
console.log("[Aden Hive] SIGINT received, shutting down gracefully");
|
||||
server.close(() => {
|
||||
console.log("[Aden Hive] Server closed");
|
||||
process.exit(0);
|
||||
});
|
||||
});
|
||||
|
||||
export default server;
|
||||
@@ -1,62 +0,0 @@
|
||||
/**
|
||||
* Aden Hive MCP Server
|
||||
*
|
||||
* Model Context Protocol server for LLM governance.
|
||||
* Exposes 19 tools:
|
||||
*
|
||||
* Budget Tools (6):
|
||||
* - hive_budget_get, hive_budget_reset, hive_budget_validate
|
||||
* - hive_budget_rule_create, hive_budget_rule_update, hive_budget_rule_delete
|
||||
*
|
||||
* Agent Status Tools (3):
|
||||
* - hive_agents_list, hive_agent_health_check, hive_agents_summary
|
||||
*
|
||||
* Analytics Tools (5):
|
||||
* - hive_analytics_wide, hive_analytics_narrow, hive_insights
|
||||
* - hive_metrics, hive_logs
|
||||
*
|
||||
* Policy Tools (5):
|
||||
* - hive_policies_list, hive_policy_get, hive_policy_create
|
||||
* - hive_policy_update, hive_policy_clear
|
||||
*
|
||||
* Usage:
|
||||
* import { createMcpRouter } from './mcp';
|
||||
* app.use('/mcp', createMcpRouter(getControlEmitter));
|
||||
*/
|
||||
|
||||
// Server creation
|
||||
export { createHiveMcpServer, TOOL_CATALOG } from "./server";
|
||||
export type { HiveMcpServerOptions } from "./server";
|
||||
|
||||
// HTTP transport
|
||||
export {
|
||||
createMcpRouter,
|
||||
getActiveMcpSessionCount,
|
||||
getTeamMcpSessions,
|
||||
} from "./transport/http";
|
||||
|
||||
// API client for direct usage
|
||||
export { createApiClient } from "./utils/api-client";
|
||||
export type { ApiClient, ApiContext } from "./utils/api-client";
|
||||
|
||||
// Response helpers
|
||||
export {
|
||||
createSuccessResponse,
|
||||
createErrorResponse,
|
||||
handleToolError,
|
||||
} from "./utils/response-helpers";
|
||||
|
||||
// Schema helpers
|
||||
export {
|
||||
idSchema,
|
||||
dateSchema,
|
||||
dateTimeSchema,
|
||||
amountSchema,
|
||||
budgetTypeSchema,
|
||||
limitActionSchema,
|
||||
analyticsWindowSchema,
|
||||
validationContextSchema,
|
||||
budgetAlertSchema,
|
||||
budgetNotificationsSchema,
|
||||
paginationSchema,
|
||||
} from "./utils/schema-helpers";
|
||||
@@ -1,90 +0,0 @@
|
||||
/**
|
||||
* Aden Hive MCP Server
|
||||
*
|
||||
* MCP server with tools for:
|
||||
* - Cost control (budget management)
|
||||
* - Agent status (fleet monitoring)
|
||||
* - Analytics (insights, metrics, logs)
|
||||
* - Policy management
|
||||
*/
|
||||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { createApiClient, type ApiContext } from "./utils/api-client";
|
||||
import { registerBudgetTools } from "./tools/budget";
|
||||
import { registerAgentTools, type ControlEmitter } from "./tools/agents";
|
||||
import { registerAnalyticsTools } from "./tools/analytics";
|
||||
import { registerPolicyTools } from "./tools/policies";
|
||||
|
||||
export interface HiveMcpServerOptions {
|
||||
context: ApiContext;
|
||||
getControlEmitter?: () => ControlEmitter | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and configure the Aden Hive MCP server
|
||||
*/
|
||||
export function createHiveMcpServer(options: HiveMcpServerOptions): McpServer {
|
||||
const { context, getControlEmitter } = options;
|
||||
|
||||
// Create MCP server
|
||||
const server = new McpServer({
|
||||
name: "aden-hive",
|
||||
version: "1.0.0",
|
||||
});
|
||||
|
||||
// Create API client bound to team context
|
||||
const api = createApiClient(context);
|
||||
|
||||
// Register all tool categories
|
||||
registerBudgetTools(server, api);
|
||||
registerAgentTools(server, api, getControlEmitter || (() => undefined));
|
||||
registerAnalyticsTools(server, api);
|
||||
registerPolicyTools(server, api);
|
||||
|
||||
console.log(
|
||||
`[MCP] Aden Hive server created with ${19} tools for team ${context.teamId}`
|
||||
);
|
||||
|
||||
return server;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tool categories and counts for reference
|
||||
*/
|
||||
export const TOOL_CATALOG = {
|
||||
budget: {
|
||||
count: 6,
|
||||
tools: [
|
||||
"hive_budget_get",
|
||||
"hive_budget_reset",
|
||||
"hive_budget_validate",
|
||||
"hive_budget_rule_create",
|
||||
"hive_budget_rule_update",
|
||||
"hive_budget_rule_delete",
|
||||
],
|
||||
},
|
||||
agents: {
|
||||
count: 3,
|
||||
tools: ["hive_agents_list", "hive_agent_health_check", "hive_agents_summary"],
|
||||
},
|
||||
analytics: {
|
||||
count: 5,
|
||||
tools: [
|
||||
"hive_analytics_wide",
|
||||
"hive_analytics_narrow",
|
||||
"hive_insights",
|
||||
"hive_metrics",
|
||||
"hive_logs",
|
||||
],
|
||||
},
|
||||
policies: {
|
||||
count: 5,
|
||||
tools: [
|
||||
"hive_policies_list",
|
||||
"hive_policy_get",
|
||||
"hive_policy_create",
|
||||
"hive_policy_update",
|
||||
"hive_policy_clear",
|
||||
],
|
||||
},
|
||||
total: 19,
|
||||
};
|
||||
@@ -1,197 +0,0 @@
|
||||
/**
|
||||
* Agent Status MCP Tools
|
||||
*
|
||||
* Tools for monitoring connected SDK agent instances:
|
||||
* - hive_agents_list: List all connected SDK instances
|
||||
* - hive_agent_health_check: Check health of specific agent
|
||||
* - hive_agents_summary: Get fleet health overview
|
||||
*/
|
||||
import { z } from "zod";
|
||||
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import type { ApiClient } from "../utils/api-client";
|
||||
import {
|
||||
createSuccessResponse,
|
||||
handleToolError,
|
||||
} from "../utils/response-helpers";
|
||||
|
||||
export interface ControlEmitter {
|
||||
getConnectedCount: (teamId: string) => number;
|
||||
getConnectedInstances: (teamId: string) => Array<{
|
||||
instance_id: string;
|
||||
agent?: string;
|
||||
policy_id?: string | null;
|
||||
connected_at: string;
|
||||
last_heartbeat: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export function registerAgentTools(
|
||||
server: McpServer,
|
||||
api: ApiClient,
|
||||
getControlEmitter: () => ControlEmitter | undefined
|
||||
) {
|
||||
// ==================== hive_agents_list ====================
|
||||
server.tool(
|
||||
"hive_agents_list",
|
||||
"Get list of all connected SDK agent instances with health status and connection details",
|
||||
{
|
||||
includeMetrics: z
|
||||
.boolean()
|
||||
.default(false)
|
||||
.describe("Include per-agent metrics (connection duration, heartbeat lag)"),
|
||||
},
|
||||
async (params) => {
|
||||
try {
|
||||
const controlEmitter = getControlEmitter();
|
||||
const result = api.agents.getList(controlEmitter);
|
||||
|
||||
if (params.includeMetrics && result.instances) {
|
||||
const now = Date.now();
|
||||
const enrichedInstances = (result.instances as Array<{
|
||||
instance_id: string;
|
||||
connected_at: string;
|
||||
last_heartbeat: string;
|
||||
}>).map((instance) => {
|
||||
const connectedAt = new Date(instance.connected_at).getTime();
|
||||
const lastHeartbeat = new Date(instance.last_heartbeat).getTime();
|
||||
|
||||
return {
|
||||
...instance,
|
||||
metrics: {
|
||||
connection_duration_ms: now - connectedAt,
|
||||
connection_duration_seconds: Math.round((now - connectedAt) / 1000),
|
||||
heartbeat_lag_ms: now - lastHeartbeat,
|
||||
heartbeat_lag_seconds: Math.round((now - lastHeartbeat) / 1000),
|
||||
is_healthy: now - lastHeartbeat < 60000,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
return createSuccessResponse({
|
||||
...result,
|
||||
instances: enrichedInstances,
|
||||
});
|
||||
}
|
||||
|
||||
return createSuccessResponse(result);
|
||||
} catch (error) {
|
||||
return handleToolError(error, "hive_agents_list");
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// ==================== hive_agent_health_check ====================
|
||||
server.tool(
|
||||
"hive_agent_health_check",
|
||||
"Check health of a specific agent by instance ID or agent name. Returns health status, last heartbeat, and connection details.",
|
||||
{
|
||||
instanceId: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("SDK instance ID to check"),
|
||||
agentName: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Agent name to filter (returns all instances with this name)"),
|
||||
},
|
||||
async (params) => {
|
||||
try {
|
||||
if (!params.instanceId && !params.agentName) {
|
||||
return handleToolError(
|
||||
new Error("Either instanceId or agentName is required"),
|
||||
"hive_agent_health_check"
|
||||
);
|
||||
}
|
||||
|
||||
const controlEmitter = getControlEmitter();
|
||||
const result = api.agents.getList(controlEmitter);
|
||||
|
||||
if (!result.instances || result.instances.length === 0) {
|
||||
return createSuccessResponse({
|
||||
found: false,
|
||||
message: "No agents connected",
|
||||
query: params,
|
||||
});
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
const STALE_THRESHOLD_MS = 60000; // 60 seconds
|
||||
|
||||
// Filter instances based on query
|
||||
const instances = (result.instances as Array<{
|
||||
instance_id: string;
|
||||
agent?: string;
|
||||
connected_at: string;
|
||||
last_heartbeat: string;
|
||||
}>).filter((instance) => {
|
||||
if (params.instanceId && instance.instance_id === params.instanceId) {
|
||||
return true;
|
||||
}
|
||||
if (params.agentName && instance.agent === params.agentName) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
if (instances.length === 0) {
|
||||
return createSuccessResponse({
|
||||
found: false,
|
||||
message: params.instanceId
|
||||
? `Instance ${params.instanceId} not found`
|
||||
: `No instances found for agent ${params.agentName}`,
|
||||
query: params,
|
||||
total_connected: result.count,
|
||||
});
|
||||
}
|
||||
|
||||
// Enrich with health status
|
||||
const healthResults = instances.map((instance) => {
|
||||
const lastHeartbeat = new Date(instance.last_heartbeat).getTime();
|
||||
const heartbeatLag = now - lastHeartbeat;
|
||||
const isHealthy = heartbeatLag < STALE_THRESHOLD_MS;
|
||||
|
||||
return {
|
||||
instance_id: instance.instance_id,
|
||||
agent_name: instance.agent || "unknown",
|
||||
status: isHealthy ? "healthy" : "unhealthy",
|
||||
last_heartbeat: instance.last_heartbeat,
|
||||
last_heartbeat_ago_seconds: Math.round(heartbeatLag / 1000),
|
||||
connected_at: instance.connected_at,
|
||||
connection_duration_seconds: Math.round(
|
||||
(now - new Date(instance.connected_at).getTime()) / 1000
|
||||
),
|
||||
health_threshold_seconds: STALE_THRESHOLD_MS / 1000,
|
||||
};
|
||||
});
|
||||
|
||||
return createSuccessResponse({
|
||||
found: true,
|
||||
count: healthResults.length,
|
||||
instances: healthResults,
|
||||
summary: {
|
||||
healthy: healthResults.filter((h) => h.status === "healthy").length,
|
||||
unhealthy: healthResults.filter((h) => h.status === "unhealthy").length,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
return handleToolError(error, "hive_agent_health_check");
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// ==================== hive_agents_summary ====================
|
||||
server.tool(
|
||||
"hive_agents_summary",
|
||||
"Get summary of agent fleet health: total active, healthy count, unhealthy count, and breakdown by agent name",
|
||||
{},
|
||||
async () => {
|
||||
try {
|
||||
const controlEmitter = getControlEmitter();
|
||||
const result = api.agents.getSummary(controlEmitter);
|
||||
return createSuccessResponse(result);
|
||||
} catch (error) {
|
||||
return handleToolError(error, "hive_agents_summary");
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -1,169 +0,0 @@
|
||||
/**
|
||||
* Analytics MCP Tools
|
||||
*
|
||||
* Tools for querying analytics and insights:
|
||||
* - hive_analytics_wide: Dashboard analytics with daily resolution
|
||||
* - hive_analytics_narrow: Hourly analytics for today
|
||||
* - hive_insights: Actionable insights and anomalies
|
||||
* - hive_metrics: Summary metrics with period-over-period change
|
||||
* - hive_logs: Raw or aggregated event logs
|
||||
*/
|
||||
import { z } from "zod";
|
||||
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import type { ApiClient } from "../utils/api-client";
|
||||
import {
|
||||
createSuccessResponse,
|
||||
handleToolError,
|
||||
} from "../utils/response-helpers";
|
||||
import { analyticsWindowSchema, dateTimeSchema } from "../utils/schema-helpers";
|
||||
|
||||
export function registerAnalyticsTools(server: McpServer, api: ApiClient) {
|
||||
// ==================== hive_analytics_wide ====================
|
||||
server.tool(
|
||||
"hive_analytics_wide",
|
||||
"Get dashboard analytics with daily resolution. Use for trend analysis over days/weeks/months. Returns volume, cost, tokens, and performance data points by day.",
|
||||
{
|
||||
window: analyticsWindowSchema.describe(
|
||||
"Time window: all_time, this_month, this_week, last_2_weeks, or today"
|
||||
),
|
||||
},
|
||||
async (params) => {
|
||||
try {
|
||||
const result = await api.analytics.getWide(params.window);
|
||||
return createSuccessResponse(result);
|
||||
} catch (error) {
|
||||
return handleToolError(error, "hive_analytics_wide");
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// ==================== hive_analytics_narrow ====================
|
||||
server.tool(
|
||||
"hive_analytics_narrow",
|
||||
"Get hourly analytics for today. Use for intraday monitoring, detecting recent spikes, and real-time cost tracking.",
|
||||
{},
|
||||
async () => {
|
||||
try {
|
||||
const result = await api.analytics.getNarrow();
|
||||
return createSuccessResponse(result);
|
||||
} catch (error) {
|
||||
return handleToolError(error, "hive_analytics_narrow");
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// ==================== hive_insights ====================
|
||||
server.tool(
|
||||
"hive_insights",
|
||||
"Get actionable insights: cost spikes, anomalies, trends, cache efficiency, and recommendations. Critical for autonomous monitoring and cost control.",
|
||||
{
|
||||
days: z
|
||||
.number()
|
||||
.min(1)
|
||||
.max(90)
|
||||
.default(30)
|
||||
.describe("Analysis period in days (1-90)"),
|
||||
},
|
||||
async (params) => {
|
||||
try {
|
||||
const result = await api.analytics.getInsights(params.days);
|
||||
return createSuccessResponse(result);
|
||||
} catch (error) {
|
||||
return handleToolError(error, "hive_insights");
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// ==================== hive_metrics ====================
|
||||
server.tool(
|
||||
"hive_metrics",
|
||||
"Get summary metrics with period-over-period percentage change. Good for quick health checks and comparing current vs previous period.",
|
||||
{
|
||||
days: z
|
||||
.number()
|
||||
.min(1)
|
||||
.max(365)
|
||||
.default(30)
|
||||
.describe("Period in days for current window and comparison"),
|
||||
},
|
||||
async (params) => {
|
||||
try {
|
||||
const result = await api.analytics.getMetrics(params.days);
|
||||
return createSuccessResponse(result);
|
||||
} catch (error) {
|
||||
return handleToolError(error, "hive_metrics");
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// ==================== hive_logs ====================
|
||||
server.tool(
|
||||
"hive_logs",
|
||||
"Query raw or aggregated event logs. Use for investigation, drill-down, and detailed analysis. Supports grouping by model, agent, or provider.",
|
||||
{
|
||||
start: dateTimeSchema.describe("Start time (ISO 8601 format)"),
|
||||
end: dateTimeSchema.describe("End time (ISO 8601 format)"),
|
||||
groupBy: z
|
||||
.enum(["model", "agent", "provider", "model,agent", "model,provider"])
|
||||
.optional()
|
||||
.describe(
|
||||
"Aggregate by field(s). If not specified, returns raw log rows."
|
||||
),
|
||||
limit: z
|
||||
.number()
|
||||
.min(1)
|
||||
.max(5000)
|
||||
.default(500)
|
||||
.describe("Maximum rows/aggregations to return"),
|
||||
offset: z
|
||||
.number()
|
||||
.min(0)
|
||||
.default(0)
|
||||
.describe("Number of rows to skip (for pagination)"),
|
||||
},
|
||||
async (params) => {
|
||||
try {
|
||||
// Validate date range
|
||||
const startDate = new Date(params.start);
|
||||
const endDate = new Date(params.end);
|
||||
|
||||
if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) {
|
||||
return handleToolError(
|
||||
new Error("Invalid date format. Use ISO 8601 format."),
|
||||
"hive_logs"
|
||||
);
|
||||
}
|
||||
|
||||
if (endDate < startDate) {
|
||||
return handleToolError(
|
||||
new Error("End date must be after start date"),
|
||||
"hive_logs"
|
||||
);
|
||||
}
|
||||
|
||||
// Warn if range is too large
|
||||
const rangeDays =
|
||||
(endDate.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24);
|
||||
if (rangeDays > 90 && !params.groupBy) {
|
||||
console.warn(
|
||||
`[MCP] hive_logs: Large date range (${rangeDays.toFixed(
|
||||
0
|
||||
)} days) without aggregation may be slow`
|
||||
);
|
||||
}
|
||||
|
||||
const result = await api.analytics.getLogs({
|
||||
start: params.start,
|
||||
end: params.end,
|
||||
groupBy: params.groupBy,
|
||||
limit: params.limit,
|
||||
offset: params.offset,
|
||||
});
|
||||
|
||||
return createSuccessResponse(result);
|
||||
} catch (error) {
|
||||
return handleToolError(error, "hive_logs");
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -1,335 +0,0 @@
|
||||
/**
|
||||
* Budget MCP Tools
|
||||
*
|
||||
* Tools for cost control and budget management:
|
||||
* - hive_budget_get: Get budget status
|
||||
* - hive_budget_reset: Reset budget spend
|
||||
* - hive_budget_validate: Validate request against budgets
|
||||
* - hive_budget_rule_create: Create budget rule
|
||||
* - hive_budget_rule_update: Update budget rule
|
||||
* - hive_budget_rule_delete: Delete budget rule
|
||||
*/
|
||||
import { z } from "zod";
|
||||
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import type { ApiClient } from "../utils/api-client";
|
||||
import {
|
||||
createSuccessResponse,
|
||||
handleToolError,
|
||||
} from "../utils/response-helpers";
|
||||
import {
|
||||
idSchema,
|
||||
budgetTypeSchema,
|
||||
limitActionSchema,
|
||||
validationContextSchema,
|
||||
budgetAlertSchema,
|
||||
budgetNotificationsSchema,
|
||||
} from "../utils/schema-helpers";
|
||||
|
||||
export function registerBudgetTools(server: McpServer, api: ApiClient) {
|
||||
// ==================== hive_budget_get ====================
|
||||
server.tool(
|
||||
"hive_budget_get",
|
||||
"Get budget status including spend, limit, burn rate, and projected spend for a specific budget ID",
|
||||
{
|
||||
budgetId: idSchema.describe("Budget ID to query"),
|
||||
},
|
||||
async (params) => {
|
||||
try {
|
||||
const result = await api.budget.getStatus(params.budgetId);
|
||||
return createSuccessResponse(result);
|
||||
} catch (error) {
|
||||
return handleToolError(error, "hive_budget_get");
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// ==================== hive_budget_reset ====================
|
||||
server.tool(
|
||||
"hive_budget_reset",
|
||||
"Reset a budget spend counter to zero. Use when starting new billing cycle or after resolving overage.",
|
||||
{
|
||||
budgetId: idSchema.describe("Budget ID to reset"),
|
||||
reason: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Reason for reset (for audit trail)"),
|
||||
},
|
||||
async (params) => {
|
||||
try {
|
||||
const result = await api.budget.reset(params.budgetId);
|
||||
return createSuccessResponse({
|
||||
...result,
|
||||
reason: params.reason,
|
||||
reset_at: new Date().toISOString(),
|
||||
});
|
||||
} catch (error) {
|
||||
return handleToolError(error, "hive_budget_reset");
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// ==================== hive_budget_validate ====================
|
||||
server.tool(
|
||||
"hive_budget_validate",
|
||||
"Validate if a request should be allowed based on budget constraints. Returns allow/throttle/degrade/block decision with authoritative spend data.",
|
||||
{
|
||||
budgetId: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Specific budget ID to validate against"),
|
||||
estimatedCost: z
|
||||
.number()
|
||||
.min(0)
|
||||
.describe("Estimated cost of the request in USD"),
|
||||
context: validationContextSchema
|
||||
.optional()
|
||||
.describe(
|
||||
"Context for multi-budget matching (agent, tenant_id, customer_id, feature, tags)"
|
||||
),
|
||||
localSpend: z
|
||||
.number()
|
||||
.optional()
|
||||
.describe("Local spend tracked by SDK (for drift detection)"),
|
||||
},
|
||||
async (params) => {
|
||||
try {
|
||||
const result = await api.budget.validate({
|
||||
budgetId: params.budgetId,
|
||||
estimatedCost: params.estimatedCost,
|
||||
context: params.context,
|
||||
localSpend: params.localSpend,
|
||||
});
|
||||
return createSuccessResponse(result);
|
||||
} catch (error) {
|
||||
return handleToolError(error, "hive_budget_validate");
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// ==================== hive_budget_rule_create ====================
|
||||
server.tool(
|
||||
"hive_budget_rule_create",
|
||||
"Create a new budget rule within a policy. Budget rules define spending limits and actions when exceeded.",
|
||||
{
|
||||
policyId: z
|
||||
.string()
|
||||
.default("default")
|
||||
.describe('Policy ID (use "default" for default policy)'),
|
||||
id: idSchema.describe("Unique budget rule ID"),
|
||||
name: z.string().min(1).describe("Human-readable budget name"),
|
||||
type: budgetTypeSchema.describe("Budget scope type"),
|
||||
limit: z.number().min(0).describe("Budget limit in USD"),
|
||||
limitAction: limitActionSchema
|
||||
.default("kill")
|
||||
.describe("Action when limit exceeded"),
|
||||
degradeToModel: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe('Target model for degradation (required when limitAction is "degrade")'),
|
||||
degradeToProvider: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe('Target provider for degradation (required when limitAction is "degrade")'),
|
||||
tags: z
|
||||
.array(z.string())
|
||||
.optional()
|
||||
.describe('Tags for tag-type budgets (required when type is "tag")'),
|
||||
alerts: z
|
||||
.array(budgetAlertSchema)
|
||||
.default([
|
||||
{ threshold: 80, enabled: true },
|
||||
{ threshold: 95, enabled: true },
|
||||
])
|
||||
.describe("Alert thresholds as percentage of limit"),
|
||||
notifications: budgetNotificationsSchema
|
||||
.default({
|
||||
inApp: true,
|
||||
email: false,
|
||||
emailRecipients: [],
|
||||
webhook: false,
|
||||
})
|
||||
.describe("Notification settings"),
|
||||
},
|
||||
async (params) => {
|
||||
try {
|
||||
// Validate degradation requirements
|
||||
if (params.limitAction === "degrade") {
|
||||
if (!params.degradeToModel || !params.degradeToProvider) {
|
||||
return handleToolError(
|
||||
new Error(
|
||||
"degradeToModel and degradeToProvider are required when limitAction is 'degrade'"
|
||||
),
|
||||
"hive_budget_rule_create"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Validate tag requirements
|
||||
if (params.type === "tag") {
|
||||
if (!params.tags || params.tags.length === 0) {
|
||||
return handleToolError(
|
||||
new Error("tags array is required when type is 'tag'"),
|
||||
"hive_budget_rule_create"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const result = await api.policy.addBudgetRule(params.policyId, {
|
||||
id: params.id,
|
||||
name: params.name,
|
||||
type: params.type,
|
||||
limit: params.limit,
|
||||
spent: 0,
|
||||
limitAction: params.limitAction,
|
||||
degradeToModel: params.degradeToModel,
|
||||
degradeToProvider: params.degradeToProvider,
|
||||
tags: params.tags,
|
||||
alerts: params.alerts,
|
||||
notifications: params.notifications,
|
||||
});
|
||||
|
||||
return createSuccessResponse({
|
||||
success: true,
|
||||
budget_id: params.id,
|
||||
policy: result,
|
||||
});
|
||||
} catch (error) {
|
||||
return handleToolError(error, "hive_budget_rule_create");
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// ==================== hive_budget_rule_update ====================
|
||||
server.tool(
|
||||
"hive_budget_rule_update",
|
||||
"Update an existing budget rule. Only provided fields will be updated.",
|
||||
{
|
||||
policyId: z
|
||||
.string()
|
||||
.default("default")
|
||||
.describe('Policy ID (use "default" for default policy)'),
|
||||
budgetId: idSchema.describe("Budget rule ID to update"),
|
||||
name: z.string().optional().describe("New budget name"),
|
||||
limit: z.number().min(0).optional().describe("New budget limit in USD"),
|
||||
limitAction: limitActionSchema.optional().describe("New action when limit exceeded"),
|
||||
degradeToModel: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("New target model for degradation"),
|
||||
degradeToProvider: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("New target provider for degradation"),
|
||||
alerts: z
|
||||
.array(budgetAlertSchema)
|
||||
.optional()
|
||||
.describe("New alert thresholds"),
|
||||
},
|
||||
async (params) => {
|
||||
try {
|
||||
// Get current policy to find and update the budget
|
||||
const policy = await api.policy.get(params.policyId);
|
||||
|
||||
if (!policy) {
|
||||
return handleToolError(
|
||||
new Error("Policy not found"),
|
||||
"hive_budget_rule_update"
|
||||
);
|
||||
}
|
||||
|
||||
const budgets = policy.budgets || [];
|
||||
const budgetIndex = budgets.findIndex(
|
||||
(b: { id: string }) => b.id === params.budgetId
|
||||
);
|
||||
|
||||
if (budgetIndex === -1) {
|
||||
return handleToolError(
|
||||
new Error(`Budget ${params.budgetId} not found in policy`),
|
||||
"hive_budget_rule_update"
|
||||
);
|
||||
}
|
||||
|
||||
// Update the budget with new values
|
||||
const updatedBudget = {
|
||||
...budgets[budgetIndex],
|
||||
...(params.name && { name: params.name }),
|
||||
...(params.limit !== undefined && { limit: params.limit }),
|
||||
...(params.limitAction && { limitAction: params.limitAction }),
|
||||
...(params.degradeToModel && { degradeToModel: params.degradeToModel }),
|
||||
...(params.degradeToProvider && { degradeToProvider: params.degradeToProvider }),
|
||||
...(params.alerts && { alerts: params.alerts }),
|
||||
};
|
||||
|
||||
budgets[budgetIndex] = updatedBudget;
|
||||
|
||||
const result = await api.policy.update(params.policyId, { budgets });
|
||||
|
||||
return createSuccessResponse({
|
||||
success: true,
|
||||
budget_id: params.budgetId,
|
||||
updated_fields: Object.keys(params).filter(
|
||||
(k) =>
|
||||
k !== "policyId" &&
|
||||
k !== "budgetId" &&
|
||||
params[k as keyof typeof params] !== undefined
|
||||
),
|
||||
policy: result,
|
||||
});
|
||||
} catch (error) {
|
||||
return handleToolError(error, "hive_budget_rule_update");
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// ==================== hive_budget_rule_delete ====================
|
||||
server.tool(
|
||||
"hive_budget_rule_delete",
|
||||
"Delete a budget rule from a policy",
|
||||
{
|
||||
policyId: z
|
||||
.string()
|
||||
.default("default")
|
||||
.describe('Policy ID (use "default" for default policy)'),
|
||||
budgetId: idSchema.describe("Budget rule ID to delete"),
|
||||
},
|
||||
async (params) => {
|
||||
try {
|
||||
// Get current policy to remove the budget
|
||||
const policy = await api.policy.get(params.policyId);
|
||||
|
||||
if (!policy) {
|
||||
return handleToolError(
|
||||
new Error("Policy not found"),
|
||||
"hive_budget_rule_delete"
|
||||
);
|
||||
}
|
||||
|
||||
const budgets = policy.budgets || [];
|
||||
const budgetIndex = budgets.findIndex(
|
||||
(b: { id: string }) => b.id === params.budgetId
|
||||
);
|
||||
|
||||
if (budgetIndex === -1) {
|
||||
return handleToolError(
|
||||
new Error(`Budget ${params.budgetId} not found in policy`),
|
||||
"hive_budget_rule_delete"
|
||||
);
|
||||
}
|
||||
|
||||
// Remove the budget
|
||||
budgets.splice(budgetIndex, 1);
|
||||
|
||||
const result = await api.policy.update(params.policyId, { budgets });
|
||||
|
||||
return createSuccessResponse({
|
||||
success: true,
|
||||
deleted_budget_id: params.budgetId,
|
||||
remaining_budgets: budgets.length,
|
||||
policy: result,
|
||||
});
|
||||
} catch (error) {
|
||||
return handleToolError(error, "hive_budget_rule_delete");
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -1,224 +0,0 @@
|
||||
/**
|
||||
* Policy Management MCP Tools
|
||||
*
|
||||
* Tools for managing control policies:
|
||||
* - hive_policies_list: List all policies
|
||||
* - hive_policy_get: Get specific policy with rules
|
||||
* - hive_policy_create: Create new policy
|
||||
* - hive_policy_update: Update policy
|
||||
* - hive_policy_clear: Clear all rules from policy
|
||||
*/
|
||||
import { z } from "zod";
|
||||
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import type { ApiClient } from "../utils/api-client";
|
||||
import {
|
||||
createSuccessResponse,
|
||||
handleToolError,
|
||||
} from "../utils/response-helpers";
|
||||
|
||||
export function registerPolicyTools(server: McpServer, api: ApiClient) {
|
||||
// ==================== hive_policies_list ====================
|
||||
server.tool(
|
||||
"hive_policies_list",
|
||||
"List all policies for the team. Returns policy IDs, names, and rule counts.",
|
||||
{
|
||||
limit: z
|
||||
.number()
|
||||
.min(1)
|
||||
.max(100)
|
||||
.default(100)
|
||||
.describe("Maximum policies to return"),
|
||||
offset: z
|
||||
.number()
|
||||
.min(0)
|
||||
.default(0)
|
||||
.describe("Number of policies to skip"),
|
||||
},
|
||||
async (params) => {
|
||||
try {
|
||||
const policies = await api.policy.list({
|
||||
limit: params.limit,
|
||||
offset: params.offset,
|
||||
});
|
||||
|
||||
// Summarize policies
|
||||
const summary = (policies as unknown as Array<{
|
||||
_id?: string;
|
||||
id?: string;
|
||||
name?: string;
|
||||
budgets?: unknown[];
|
||||
throttles?: unknown[];
|
||||
blocks?: unknown[];
|
||||
degradations?: unknown[];
|
||||
}>).map((p) => ({
|
||||
id: p._id || p.id || "unknown",
|
||||
name: p.name || "Unnamed Policy",
|
||||
rule_counts: {
|
||||
budgets: p.budgets?.length || 0,
|
||||
throttles: p.throttles?.length || 0,
|
||||
blocks: p.blocks?.length || 0,
|
||||
degradations: p.degradations?.length || 0,
|
||||
},
|
||||
}));
|
||||
|
||||
return createSuccessResponse({
|
||||
count: policies.length,
|
||||
policies: summary,
|
||||
});
|
||||
} catch (error) {
|
||||
return handleToolError(error, "hive_policies_list");
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// ==================== hive_policy_get ====================
|
||||
server.tool(
|
||||
"hive_policy_get",
|
||||
'Get a specific policy with all rules (budgets, throttles, blocks, degradations). Use "default" to get the team\'s default policy.',
|
||||
{
|
||||
policyId: z
|
||||
.string()
|
||||
.default("default")
|
||||
.describe('Policy ID or "default" for team default'),
|
||||
},
|
||||
async (params) => {
|
||||
try {
|
||||
const policy = await api.policy.get(params.policyId);
|
||||
|
||||
if (!policy) {
|
||||
return handleToolError(
|
||||
new Error(`Policy ${params.policyId} not found`),
|
||||
"hive_policy_get"
|
||||
);
|
||||
}
|
||||
|
||||
return createSuccessResponse(policy);
|
||||
} catch (error) {
|
||||
return handleToolError(error, "hive_policy_get");
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// ==================== hive_policy_create ====================
|
||||
server.tool(
|
||||
"hive_policy_create",
|
||||
"Create a new policy for the team. New policies start empty (no rules).",
|
||||
{
|
||||
name: z.string().min(1).describe("Policy name"),
|
||||
},
|
||||
async (params) => {
|
||||
try {
|
||||
const policy = await api.policy.create(params.name);
|
||||
|
||||
return createSuccessResponse({
|
||||
success: true,
|
||||
message: "Policy created",
|
||||
policy,
|
||||
});
|
||||
} catch (error) {
|
||||
return handleToolError(error, "hive_policy_create");
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// ==================== hive_policy_update ====================
|
||||
server.tool(
|
||||
"hive_policy_update",
|
||||
"Update a policy's name or replace all rules. For individual rule changes, use budget/throttle/block rule tools.",
|
||||
{
|
||||
policyId: z
|
||||
.string()
|
||||
.default("default")
|
||||
.describe('Policy ID or "default" for team default'),
|
||||
name: z.string().optional().describe("New policy name"),
|
||||
budgets: z
|
||||
.array(z.any())
|
||||
.optional()
|
||||
.describe("Complete budgets array (replaces all budgets)"),
|
||||
throttles: z
|
||||
.array(z.any())
|
||||
.optional()
|
||||
.describe("Complete throttles array (replaces all throttles)"),
|
||||
blocks: z
|
||||
.array(z.any())
|
||||
.optional()
|
||||
.describe("Complete blocks array (replaces all blocks)"),
|
||||
degradations: z
|
||||
.array(z.any())
|
||||
.optional()
|
||||
.describe("Complete degradations array (replaces all degradations)"),
|
||||
},
|
||||
async (params) => {
|
||||
try {
|
||||
// Only pass defined fields
|
||||
const updates: {
|
||||
name?: string;
|
||||
budgets?: unknown[];
|
||||
throttles?: unknown[];
|
||||
blocks?: unknown[];
|
||||
degradations?: unknown[];
|
||||
} = {};
|
||||
|
||||
if (params.name !== undefined) updates.name = params.name;
|
||||
if (params.budgets !== undefined) updates.budgets = params.budgets;
|
||||
if (params.throttles !== undefined) updates.throttles = params.throttles;
|
||||
if (params.blocks !== undefined) updates.blocks = params.blocks;
|
||||
if (params.degradations !== undefined)
|
||||
updates.degradations = params.degradations;
|
||||
|
||||
if (Object.keys(updates).length === 0) {
|
||||
return handleToolError(
|
||||
new Error("No updates provided"),
|
||||
"hive_policy_update"
|
||||
);
|
||||
}
|
||||
|
||||
const policy = await api.policy.update(params.policyId, updates);
|
||||
|
||||
return createSuccessResponse({
|
||||
success: true,
|
||||
updated_fields: Object.keys(updates),
|
||||
policy,
|
||||
});
|
||||
} catch (error) {
|
||||
return handleToolError(error, "hive_policy_update");
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// ==================== hive_policy_clear ====================
|
||||
server.tool(
|
||||
"hive_policy_clear",
|
||||
"Clear all rules from a policy (budgets, throttles, blocks, degradations). The policy itself is preserved.",
|
||||
{
|
||||
policyId: z
|
||||
.string()
|
||||
.default("default")
|
||||
.describe('Policy ID or "default" for team default'),
|
||||
confirm: z
|
||||
.boolean()
|
||||
.describe("Set to true to confirm clearing all rules"),
|
||||
},
|
||||
async (params) => {
|
||||
try {
|
||||
if (!params.confirm) {
|
||||
return createSuccessResponse({
|
||||
warning:
|
||||
"This will delete ALL rules from the policy. Set confirm=true to proceed.",
|
||||
policy_id: params.policyId,
|
||||
});
|
||||
}
|
||||
|
||||
const policy = await api.policy.clear(params.policyId);
|
||||
|
||||
return createSuccessResponse({
|
||||
success: true,
|
||||
message: "All rules cleared from policy",
|
||||
policy,
|
||||
});
|
||||
} catch (error) {
|
||||
return handleToolError(error, "hive_policy_clear");
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -1,238 +0,0 @@
|
||||
/**
|
||||
* HTTP/SSE Transport for Aden Hive MCP Server
|
||||
*
|
||||
* Provides HTTP-based transport for autonomous LLM agents:
|
||||
* - GET /mcp - SSE stream for server-to-client messages
|
||||
* - POST /mcp/message - Client-to-server messages
|
||||
*/
|
||||
import express, { Request, Response, Router } from "express";
|
||||
import passport from "passport";
|
||||
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
||||
import { createHiveMcpServer, type HiveMcpServerOptions } from "../server";
|
||||
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
|
||||
interface AuthenticatedRequest extends Request {
|
||||
user?: {
|
||||
id: string;
|
||||
current_team_id: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface McpSession {
|
||||
server: McpServer;
|
||||
transport: SSEServerTransport;
|
||||
teamId: string;
|
||||
userId: string;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
// Active MCP sessions by session ID
|
||||
const sessions = new Map<string, McpSession>();
|
||||
|
||||
/**
|
||||
* Create MCP HTTP router
|
||||
*/
|
||||
export function createMcpRouter(
|
||||
getControlEmitter?: HiveMcpServerOptions["getControlEmitter"]
|
||||
): Router {
|
||||
const router = express.Router();
|
||||
|
||||
// All MCP routes require authentication
|
||||
const authMiddleware = passport.authenticate("jwt", { session: false });
|
||||
|
||||
/**
|
||||
* GET /mcp
|
||||
* SSE endpoint - establishes persistent connection for server-to-client messages
|
||||
*/
|
||||
router.get(
|
||||
"/",
|
||||
authMiddleware,
|
||||
async (req: AuthenticatedRequest, res: Response) => {
|
||||
const teamId = req.user?.current_team_id;
|
||||
const userId = req.user?.id;
|
||||
|
||||
if (!teamId) {
|
||||
res.status(401).json({ error: "Team ID required" });
|
||||
return;
|
||||
}
|
||||
|
||||
// Set custom headers (SSE headers are set by the transport)
|
||||
res.setHeader("X-Accel-Buffering", "no");
|
||||
|
||||
// Create MCP server for this session
|
||||
const server = createHiveMcpServer({
|
||||
context: {
|
||||
teamId,
|
||||
userId,
|
||||
},
|
||||
getControlEmitter,
|
||||
});
|
||||
|
||||
// Create SSE transport - it generates its own sessionId internally
|
||||
const transport = new SSEServerTransport("/mcp/message", res);
|
||||
|
||||
// Get the SDK's session ID (used in query params for POST requests)
|
||||
const sdkSessionId = transport.sessionId;
|
||||
|
||||
console.log(`[MCP] New SSE connection: session=${sdkSessionId}, team=${teamId}`);
|
||||
|
||||
// Store session by the SDK's session ID
|
||||
sessions.set(sdkSessionId, {
|
||||
server,
|
||||
transport,
|
||||
teamId,
|
||||
userId: userId || "unknown",
|
||||
createdAt: new Date(),
|
||||
});
|
||||
|
||||
// Connect server to transport
|
||||
await server.connect(transport);
|
||||
|
||||
// Handle client disconnect
|
||||
req.on("close", () => {
|
||||
console.log(`[MCP] SSE connection closed: session=${sdkSessionId}`);
|
||||
sessions.delete(sdkSessionId);
|
||||
server.close();
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* POST /mcp/message
|
||||
* Receives messages from client
|
||||
*/
|
||||
// Note: Do NOT use express.json() here - handlePostMessage reads the raw body stream
|
||||
router.post(
|
||||
"/message",
|
||||
authMiddleware,
|
||||
async (req: AuthenticatedRequest, res: Response) => {
|
||||
// SDK passes session ID as query parameter: /mcp/message?sessionId=xxx
|
||||
const sessionId = req.query.sessionId as string;
|
||||
|
||||
if (!sessionId) {
|
||||
res.status(400).json({ error: "sessionId query parameter required" });
|
||||
return;
|
||||
}
|
||||
|
||||
const session = sessions.get(sessionId);
|
||||
|
||||
if (!session) {
|
||||
res.status(404).json({
|
||||
error: "Session not found",
|
||||
hint: "Establish SSE connection first via GET /mcp",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Verify team ID matches
|
||||
if (session.teamId !== req.user?.current_team_id) {
|
||||
res.status(403).json({ error: "Session team mismatch" });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Handle the message through the transport
|
||||
await session.transport.handlePostMessage(req, res);
|
||||
} catch (error) {
|
||||
console.error(`[MCP] Error handling message:`, error);
|
||||
res.status(500).json({
|
||||
error: "Failed to process message",
|
||||
details: error instanceof Error ? error.message : "Unknown error",
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* GET /mcp/sessions
|
||||
* List active MCP sessions (admin/debug endpoint)
|
||||
*/
|
||||
router.get(
|
||||
"/sessions",
|
||||
authMiddleware,
|
||||
(req: AuthenticatedRequest, res: Response) => {
|
||||
const teamId = req.user?.current_team_id;
|
||||
|
||||
// Only show sessions for the requesting team
|
||||
const teamSessions = Array.from(sessions.entries())
|
||||
.filter(([, session]) => session.teamId === teamId)
|
||||
.map(([id, session]) => ({
|
||||
session_id: id,
|
||||
team_id: session.teamId,
|
||||
user_id: session.userId,
|
||||
created_at: session.createdAt.toISOString(),
|
||||
age_seconds: Math.round(
|
||||
(Date.now() - session.createdAt.getTime()) / 1000
|
||||
),
|
||||
}));
|
||||
|
||||
res.json({
|
||||
count: teamSessions.length,
|
||||
sessions: teamSessions,
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* DELETE /mcp/sessions/:sessionId
|
||||
* Close a specific MCP session
|
||||
*/
|
||||
router.delete(
|
||||
"/sessions/:sessionId",
|
||||
authMiddleware,
|
||||
(req: AuthenticatedRequest, res: Response) => {
|
||||
const { sessionId } = req.params;
|
||||
const teamId = req.user?.current_team_id;
|
||||
|
||||
const session = sessions.get(sessionId);
|
||||
|
||||
if (!session) {
|
||||
res.status(404).json({ error: "Session not found" });
|
||||
return;
|
||||
}
|
||||
|
||||
// Verify team ID matches
|
||||
if (session.teamId !== teamId) {
|
||||
res.status(403).json({ error: "Cannot close session from another team" });
|
||||
return;
|
||||
}
|
||||
|
||||
// Close the session
|
||||
session.server.close();
|
||||
sessions.delete(sessionId);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: `Session ${sessionId} closed`,
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* GET /mcp/health
|
||||
* Health check endpoint
|
||||
*/
|
||||
router.get("/health", (_req: Request, res: Response) => {
|
||||
res.json({
|
||||
status: "healthy",
|
||||
active_sessions: sessions.size,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
});
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get count of active MCP sessions
|
||||
*/
|
||||
export function getActiveMcpSessionCount(): number {
|
||||
return sessions.size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get active sessions for a specific team
|
||||
*/
|
||||
export function getTeamMcpSessions(teamId: string): McpSession[] {
|
||||
return Array.from(sessions.values()).filter((s) => s.teamId === teamId);
|
||||
}
|
||||
@@ -1,610 +0,0 @@
|
||||
/**
|
||||
* Internal API client for MCP tools
|
||||
*
|
||||
* This client makes direct calls to the control and tsdb services
|
||||
* rather than HTTP calls, since we're running inside the same process.
|
||||
*/
|
||||
import controlService from "../../services/control/control_service";
|
||||
import * as tsdbService from "../../services/tsdb/tsdb_service";
|
||||
import { buildAnalytics } from "../../services/tsdb/analytics_service";
|
||||
import { getTeamPool, buildSchemaName } from "../../services/tsdb/team_context";
|
||||
import type { PoolClient } from "pg";
|
||||
|
||||
export interface ApiContext {
|
||||
teamId: string;
|
||||
userId?: string;
|
||||
}
|
||||
|
||||
export interface BudgetRule {
|
||||
id: string;
|
||||
name?: string;
|
||||
type?: string;
|
||||
tags?: string[];
|
||||
limit?: number;
|
||||
spent?: number;
|
||||
limitAction?: string;
|
||||
degradeToModel?: string;
|
||||
degradeToProvider?: string;
|
||||
alerts?: Array<{ threshold: number; enabled: boolean }>;
|
||||
notifications?: {
|
||||
inApp: boolean;
|
||||
email: boolean;
|
||||
emailRecipients: string[];
|
||||
webhook: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ValidationContext {
|
||||
agent?: string;
|
||||
tenant_id?: string;
|
||||
customer_id?: string;
|
||||
feature?: string;
|
||||
tags?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an API client bound to a specific team context
|
||||
*/
|
||||
export function createApiClient(context: ApiContext) {
|
||||
const userContext = {
|
||||
user_id: context.userId || "mcp-agent",
|
||||
team_id: context.teamId,
|
||||
};
|
||||
|
||||
return {
|
||||
// ==================== Budget Operations ====================
|
||||
budget: {
|
||||
/**
|
||||
* Get budget status by ID
|
||||
*/
|
||||
async getStatus(budgetId: string) {
|
||||
return controlService.getBudgetStatus(budgetId);
|
||||
},
|
||||
|
||||
/**
|
||||
* Reset budget spend to zero
|
||||
*/
|
||||
async reset(budgetId: string) {
|
||||
await controlService.resetBudget(budgetId);
|
||||
return { success: true, id: budgetId };
|
||||
},
|
||||
|
||||
/**
|
||||
* Validate a request against budgets
|
||||
*/
|
||||
async validate(params: {
|
||||
budgetId?: string;
|
||||
estimatedCost: number;
|
||||
context?: ValidationContext;
|
||||
localSpend?: number;
|
||||
}) {
|
||||
// Get the policy to validate against
|
||||
const policy = await controlService.getPolicy(
|
||||
context.teamId,
|
||||
null,
|
||||
userContext
|
||||
);
|
||||
|
||||
if (!policy) {
|
||||
return {
|
||||
allowed: true,
|
||||
action: "allow",
|
||||
reason: "No policy found",
|
||||
budgets_checked: [],
|
||||
};
|
||||
}
|
||||
|
||||
// Multi-budget validation using context
|
||||
if (params.context && typeof params.context === "object") {
|
||||
const matchingBudgets = controlService.findMatchingBudgetsForContext(
|
||||
policy.budgets || [],
|
||||
params.context
|
||||
);
|
||||
|
||||
if (matchingBudgets.length === 0) {
|
||||
return {
|
||||
allowed: true,
|
||||
action: "allow",
|
||||
reason: "No budgets match the provided context",
|
||||
authoritative_spend: 0,
|
||||
budget_limit: 0,
|
||||
usage_percent: 0,
|
||||
projected_percent: 0,
|
||||
budgets_checked: [],
|
||||
};
|
||||
}
|
||||
|
||||
return controlService.validateMultipleBudgets(
|
||||
matchingBudgets,
|
||||
params.estimatedCost,
|
||||
params.localSpend
|
||||
);
|
||||
}
|
||||
|
||||
// Single budget validation
|
||||
if (params.budgetId) {
|
||||
const budget = policy.budgets?.find(
|
||||
(b: { id: string }) => b.id === params.budgetId
|
||||
);
|
||||
if (!budget) {
|
||||
return {
|
||||
allowed: true,
|
||||
action: "allow",
|
||||
reason: "Budget not found in policy",
|
||||
budgets_checked: [],
|
||||
};
|
||||
}
|
||||
|
||||
return controlService.validateMultipleBudgets(
|
||||
[budget],
|
||||
params.estimatedCost,
|
||||
params.localSpend
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
allowed: true,
|
||||
action: "allow",
|
||||
reason: "No budget_id or context provided",
|
||||
budgets_checked: [],
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
// ==================== Policy Operations ====================
|
||||
policy: {
|
||||
/**
|
||||
* Get all policies for the team
|
||||
*/
|
||||
async list(pagination?: { limit?: number; offset?: number }) {
|
||||
return controlService.getPoliciesByTeam(context.teamId, {
|
||||
limit: pagination?.limit || 100,
|
||||
offset: pagination?.offset || 0,
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Get a specific policy
|
||||
*/
|
||||
async get(policyId: string | null) {
|
||||
const resolvedId =
|
||||
policyId === "default" || !policyId ? null : policyId;
|
||||
return controlService.getPolicy(context.teamId, resolvedId, userContext);
|
||||
},
|
||||
|
||||
/**
|
||||
* Create a new policy
|
||||
*/
|
||||
async create(name: string) {
|
||||
return controlService.updatePolicy(
|
||||
context.teamId,
|
||||
null,
|
||||
{ name },
|
||||
userContext
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Update a policy
|
||||
*/
|
||||
async update(
|
||||
policyId: string | null,
|
||||
updates: {
|
||||
name?: string;
|
||||
budgets?: unknown[];
|
||||
throttles?: unknown[];
|
||||
blocks?: unknown[];
|
||||
degradations?: unknown[];
|
||||
}
|
||||
) {
|
||||
const resolvedId =
|
||||
policyId === "default" || !policyId ? null : policyId;
|
||||
return controlService.updatePolicy(
|
||||
context.teamId,
|
||||
resolvedId,
|
||||
updates as Record<string, unknown>,
|
||||
userContext
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Clear all rules from a policy
|
||||
*/
|
||||
async clear(policyId: string | null) {
|
||||
const resolvedId =
|
||||
policyId === "default" || !policyId ? null : policyId;
|
||||
return controlService.clearPolicy(
|
||||
context.teamId,
|
||||
resolvedId,
|
||||
userContext
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete a policy
|
||||
*/
|
||||
async delete(policyId: string) {
|
||||
return controlService.deletePolicy(
|
||||
context.teamId,
|
||||
policyId,
|
||||
userContext
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Add a budget rule to a policy
|
||||
*/
|
||||
async addBudgetRule(policyId: string | null, rule: BudgetRule) {
|
||||
const resolvedId =
|
||||
policyId === "default" || !policyId ? null : policyId;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return controlService.addBudgetRule(
|
||||
context.teamId,
|
||||
resolvedId,
|
||||
rule as any,
|
||||
userContext
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
// ==================== Analytics Operations ====================
|
||||
analytics: {
|
||||
/**
|
||||
* Get wide analytics (daily resolution)
|
||||
*/
|
||||
async getWide(window: string = "this_month") {
|
||||
const pool = await getTeamPool(context.teamId);
|
||||
const schema = buildSchemaName(context.teamId);
|
||||
const client = await pool.connect();
|
||||
|
||||
try {
|
||||
await client.query(`SET search_path TO ${schema}, public`);
|
||||
await tsdbService.ensureSchema(client);
|
||||
|
||||
return buildAnalytics({
|
||||
windowLabel: window,
|
||||
client,
|
||||
resolution: "day",
|
||||
});
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get narrow analytics (hourly resolution for today)
|
||||
*/
|
||||
async getNarrow() {
|
||||
const pool = await getTeamPool(context.teamId);
|
||||
const schema = buildSchemaName(context.teamId);
|
||||
const client = await pool.connect();
|
||||
|
||||
try {
|
||||
await client.query(`SET search_path TO ${schema}, public`);
|
||||
await tsdbService.ensureSchema(client);
|
||||
|
||||
return buildAnalytics({
|
||||
windowLabel: "today",
|
||||
client,
|
||||
resolution: "hour",
|
||||
});
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get actionable insights
|
||||
*/
|
||||
async getInsights(days: number = 30) {
|
||||
const pool = await getTeamPool(context.teamId);
|
||||
const schema = buildSchemaName(context.teamId);
|
||||
const client = await pool.connect();
|
||||
|
||||
try {
|
||||
await client.query(`SET search_path TO ${schema}, public`);
|
||||
await tsdbService.ensureSchema(client);
|
||||
|
||||
// Use the insights generation logic from tsdb controller
|
||||
// This is a simplified version - full implementation would mirror the controller
|
||||
return this._generateInsights(client, days);
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get summary metrics with period-over-period change
|
||||
*/
|
||||
async getMetrics(days: number = 30) {
|
||||
const pool = await getTeamPool(context.teamId);
|
||||
const schema = buildSchemaName(context.teamId);
|
||||
const client = await pool.connect();
|
||||
|
||||
try {
|
||||
await client.query(`SET search_path TO ${schema}, public`);
|
||||
await tsdbService.ensureSchema(client);
|
||||
|
||||
return this._generateMetrics(client, days);
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get logs (raw or aggregated)
|
||||
*/
|
||||
async getLogs(params: {
|
||||
start: string;
|
||||
end: string;
|
||||
groupBy?: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
}) {
|
||||
const pool = await getTeamPool(context.teamId);
|
||||
const schema = buildSchemaName(context.teamId);
|
||||
const client = await pool.connect();
|
||||
|
||||
try {
|
||||
await client.query(`SET search_path TO ${schema}, public`);
|
||||
await tsdbService.ensureSchema(client);
|
||||
|
||||
return this._getLogs(client, params);
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
},
|
||||
|
||||
// Internal helper methods
|
||||
async _generateInsights(client: PoolClient, days: number) {
|
||||
// Simplified insights generation
|
||||
const now = new Date();
|
||||
const periodStart = new Date(now);
|
||||
periodStart.setDate(periodStart.getDate() - days);
|
||||
|
||||
const { rows } = await client.query(
|
||||
`
|
||||
SELECT
|
||||
COUNT(*) as total_requests,
|
||||
COALESCE(SUM(cost_total), 0) as total_cost,
|
||||
COALESCE(AVG(latency_ms), 0) as avg_latency
|
||||
FROM llm_events
|
||||
WHERE "timestamp" >= $1 AND "timestamp" <= $2
|
||||
`,
|
||||
[periodStart.toISOString(), now.toISOString()]
|
||||
);
|
||||
|
||||
const stats = rows[0];
|
||||
const insights = [];
|
||||
|
||||
// Basic usage summary insight
|
||||
insights.push({
|
||||
id: "usage_snapshot",
|
||||
severity: "summary",
|
||||
title: "Period usage summary",
|
||||
description: `${parseInt(stats.total_requests).toLocaleString()} requests totaling $${parseFloat(stats.total_cost).toFixed(2)} over the last ${days} days.`,
|
||||
metric: {
|
||||
total_requests: parseInt(stats.total_requests),
|
||||
total_cost: parseFloat(stats.total_cost),
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
period: { days, start: periodStart.toISOString(), end: now.toISOString() },
|
||||
insights,
|
||||
summary: {
|
||||
total: insights.length,
|
||||
critical: insights.filter((i) => i.severity === "critical").length,
|
||||
warning: insights.filter((i) => i.severity === "warning").length,
|
||||
info: insights.filter((i) => i.severity === "info").length,
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
async _generateMetrics(client: PoolClient, days: number) {
|
||||
const now = new Date();
|
||||
const currentStart = new Date(now);
|
||||
currentStart.setDate(currentStart.getDate() - days);
|
||||
|
||||
const { rows } = await client.query(
|
||||
`
|
||||
SELECT
|
||||
COUNT(*) as total_requests,
|
||||
COUNT(DISTINCT trace_id) as unique_traces,
|
||||
COALESCE(SUM(usage_input_tokens), 0) as total_input_tokens,
|
||||
COALESCE(SUM(usage_output_tokens), 0) as total_output_tokens,
|
||||
COALESCE(SUM(cost_total), 0) as total_cost,
|
||||
COALESCE(AVG(latency_ms), 0) as avg_latency_ms
|
||||
FROM llm_events
|
||||
WHERE "timestamp" >= $1 AND "timestamp" <= $2
|
||||
`,
|
||||
[currentStart.toISOString(), now.toISOString()]
|
||||
);
|
||||
|
||||
const stats = rows[0];
|
||||
|
||||
return {
|
||||
period: { days, start: currentStart.toISOString(), end: now.toISOString() },
|
||||
volume: {
|
||||
total_requests: parseInt(stats.total_requests),
|
||||
unique_traces: parseInt(stats.unique_traces),
|
||||
},
|
||||
tokens: {
|
||||
total_input_tokens: parseInt(stats.total_input_tokens),
|
||||
total_output_tokens: parseInt(stats.total_output_tokens),
|
||||
},
|
||||
cost: {
|
||||
total_cost: parseFloat(stats.total_cost),
|
||||
},
|
||||
performance: {
|
||||
avg_latency_ms: parseFloat(stats.avg_latency_ms),
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
async _getLogs(
|
||||
client: PoolClient,
|
||||
params: {
|
||||
start: string;
|
||||
end: string;
|
||||
groupBy?: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
}
|
||||
) {
|
||||
const { start, end, groupBy, limit = 500, offset = 0 } = params;
|
||||
|
||||
if (groupBy) {
|
||||
const validFields = ["model", "agent", "provider"];
|
||||
const groupFields = groupBy
|
||||
.split(",")
|
||||
.map((f) => f.trim())
|
||||
.filter((f) => validFields.includes(f));
|
||||
|
||||
if (groupFields.length > 0) {
|
||||
const selectFields = groupFields.join(", ");
|
||||
const { rows } = await client.query(
|
||||
`
|
||||
SELECT
|
||||
${selectFields},
|
||||
COUNT(*) as request_count,
|
||||
COALESCE(SUM(cost_total), 0) as total_cost
|
||||
FROM llm_events
|
||||
WHERE "timestamp" >= $1 AND "timestamp" <= $2
|
||||
GROUP BY ${selectFields}
|
||||
ORDER BY total_cost DESC
|
||||
LIMIT $3 OFFSET $4
|
||||
`,
|
||||
[start, end, limit, offset]
|
||||
);
|
||||
|
||||
return {
|
||||
window: { start, end },
|
||||
group_by: groupFields,
|
||||
count: rows.length,
|
||||
aggregations: rows,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Raw logs
|
||||
const { rows } = await client.query(
|
||||
`
|
||||
SELECT *
|
||||
FROM llm_events
|
||||
WHERE "timestamp" >= $1 AND "timestamp" <= $2
|
||||
ORDER BY "timestamp" DESC
|
||||
LIMIT $3 OFFSET $4
|
||||
`,
|
||||
[start, end, limit, offset]
|
||||
);
|
||||
|
||||
return {
|
||||
window: { start, end },
|
||||
count: rows.length,
|
||||
rows,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
// ==================== Agent Status Operations ====================
|
||||
agents: {
|
||||
/**
|
||||
* Get connected agent instances
|
||||
* This requires access to the controlEmitter which is set on the Express app
|
||||
*/
|
||||
getList(controlEmitter?: {
|
||||
getConnectedCount: (teamId: string) => number;
|
||||
getConnectedInstances: (teamId: string) => unknown[];
|
||||
}) {
|
||||
if (!controlEmitter) {
|
||||
return {
|
||||
active: false,
|
||||
count: 0,
|
||||
instances: [],
|
||||
timestamp: new Date().toISOString(),
|
||||
error: "WebSocket not initialized",
|
||||
};
|
||||
}
|
||||
|
||||
const count = controlEmitter.getConnectedCount(context.teamId);
|
||||
const instances = controlEmitter.getConnectedInstances(context.teamId);
|
||||
|
||||
return {
|
||||
active: count > 0,
|
||||
count,
|
||||
instances,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Get agent fleet summary
|
||||
*/
|
||||
getSummary(controlEmitter?: {
|
||||
getConnectedCount: (teamId: string) => number;
|
||||
getConnectedInstances: (teamId: string) => Array<{
|
||||
instance_id: string;
|
||||
agent?: string;
|
||||
last_heartbeat: string;
|
||||
}>;
|
||||
}) {
|
||||
if (!controlEmitter) {
|
||||
return {
|
||||
total_active: 0,
|
||||
healthy: 0,
|
||||
unhealthy: 0,
|
||||
stale_connections: 0,
|
||||
by_agent_name: {},
|
||||
timestamp: new Date().toISOString(),
|
||||
error: "WebSocket not initialized",
|
||||
};
|
||||
}
|
||||
|
||||
const instances = controlEmitter.getConnectedInstances(context.teamId);
|
||||
const now = Date.now();
|
||||
const STALE_THRESHOLD_MS = 60000; // 60 seconds
|
||||
|
||||
let healthy = 0;
|
||||
let unhealthy = 0;
|
||||
const byAgentName: Record<
|
||||
string,
|
||||
{ count: number; healthy: number; unhealthy: number }
|
||||
> = {};
|
||||
|
||||
for (const instance of instances) {
|
||||
const lastHeartbeat = new Date(instance.last_heartbeat).getTime();
|
||||
const isHealthy = now - lastHeartbeat < STALE_THRESHOLD_MS;
|
||||
|
||||
if (isHealthy) {
|
||||
healthy++;
|
||||
} else {
|
||||
unhealthy++;
|
||||
}
|
||||
|
||||
const agentName = instance.agent || "unknown";
|
||||
if (!byAgentName[agentName]) {
|
||||
byAgentName[agentName] = { count: 0, healthy: 0, unhealthy: 0 };
|
||||
}
|
||||
byAgentName[agentName].count++;
|
||||
if (isHealthy) {
|
||||
byAgentName[agentName].healthy++;
|
||||
} else {
|
||||
byAgentName[agentName].unhealthy++;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
total_active: instances.length,
|
||||
healthy,
|
||||
unhealthy,
|
||||
stale_connections: unhealthy,
|
||||
by_agent_name: byAgentName,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export type ApiClient = ReturnType<typeof createApiClient>;
|
||||
@@ -1,65 +0,0 @@
|
||||
/**
|
||||
* MCP response formatting helpers
|
||||
*/
|
||||
|
||||
export interface MCPResponse {
|
||||
[key: string]: unknown;
|
||||
content: Array<{
|
||||
type: "text";
|
||||
text: string;
|
||||
}>;
|
||||
isError?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a successful MCP response
|
||||
*/
|
||||
export function createSuccessResponse(data: unknown): MCPResponse {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify(data, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an error MCP response
|
||||
*/
|
||||
export function createErrorResponse(
|
||||
error: string,
|
||||
details?: unknown
|
||||
): MCPResponse {
|
||||
const errorData = {
|
||||
error,
|
||||
...(details && { details }),
|
||||
};
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify(errorData, null, 2),
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle tool errors consistently
|
||||
*/
|
||||
export function handleToolError(error: unknown, toolName: string): MCPResponse {
|
||||
console.error(`[MCP] Error in ${toolName}:`, error);
|
||||
|
||||
if (error instanceof Error) {
|
||||
return createErrorResponse(error.message, {
|
||||
tool: toolName,
|
||||
stack: process.env.NODE_ENV === "development" ? error.stack : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
return createErrorResponse("Unknown error occurred", { tool: toolName });
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
/**
|
||||
* Zod schema helpers for MCP tools
|
||||
*/
|
||||
import { z } from "zod";
|
||||
|
||||
// Basic types
|
||||
export const idSchema = z.string().min(1).describe("Unique identifier");
|
||||
export const dateSchema = z
|
||||
.string()
|
||||
.regex(/^\d{4}-\d{2}-\d{2}$/)
|
||||
.describe("Date in YYYY-MM-DD format");
|
||||
export const dateTimeSchema = z
|
||||
.string()
|
||||
.datetime()
|
||||
.describe("ISO 8601 datetime string");
|
||||
export const amountSchema = z.number().describe("Monetary amount in USD");
|
||||
|
||||
// Budget types
|
||||
export const budgetTypeSchema = z
|
||||
.enum(["global", "agent", "tenant", "customer", "feature", "tag"])
|
||||
.describe("Type of budget scope");
|
||||
|
||||
export const limitActionSchema = z
|
||||
.enum(["kill", "throttle", "degrade"])
|
||||
.describe("Action when budget limit exceeded");
|
||||
|
||||
// Pagination
|
||||
export const paginationSchema = z.object({
|
||||
limit: z
|
||||
.number()
|
||||
.min(1)
|
||||
.max(1000)
|
||||
.default(100)
|
||||
.describe("Max items to return"),
|
||||
offset: z.number().min(0).default(0).describe("Number of items to skip"),
|
||||
});
|
||||
|
||||
// Analytics window
|
||||
export const analyticsWindowSchema = z
|
||||
.enum(["all_time", "this_month", "this_week", "last_2_weeks", "today"])
|
||||
.default("this_month")
|
||||
.describe("Time window for analytics data");
|
||||
|
||||
// Budget validation context
|
||||
export const validationContextSchema = z
|
||||
.object({
|
||||
agent: z.string().optional().describe("Agent name for agent-type budgets"),
|
||||
tenant_id: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Tenant ID for tenant-type budgets"),
|
||||
customer_id: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Customer ID for customer-type budgets"),
|
||||
feature: z.string().optional().describe("Feature name for feature-type budgets"),
|
||||
tags: z.array(z.string()).optional().describe("Tags for tag-type budgets"),
|
||||
})
|
||||
.describe("Context for multi-budget matching");
|
||||
|
||||
// Budget alert configuration
|
||||
export const budgetAlertSchema = z.object({
|
||||
threshold: z
|
||||
.number()
|
||||
.min(0)
|
||||
.max(100)
|
||||
.describe("Alert threshold as percentage of limit"),
|
||||
enabled: z.boolean().describe("Whether alert is enabled"),
|
||||
});
|
||||
|
||||
// Budget notifications configuration
|
||||
export const budgetNotificationsSchema = z.object({
|
||||
inApp: z.boolean().default(true).describe("Enable in-app notifications"),
|
||||
email: z.boolean().default(false).describe("Enable email notifications"),
|
||||
emailRecipients: z
|
||||
.array(z.string().email())
|
||||
.default([])
|
||||
.describe("Email recipients"),
|
||||
webhook: z.boolean().default(false).describe("Enable webhook notifications"),
|
||||
});
|
||||
@@ -1,44 +0,0 @@
|
||||
/**
|
||||
* Global Error Handler Middleware
|
||||
*
|
||||
* Handles all errors and sends consistent JSON responses.
|
||||
*/
|
||||
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
|
||||
interface HttpError extends Error {
|
||||
status?: number;
|
||||
statusCode?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Error handler middleware
|
||||
* @param {Error} err - Error object
|
||||
* @param {Object} req - Express request
|
||||
* @param {Object} res - Express response
|
||||
* @param {Function} next - Next middleware
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
function errorHandler(err: HttpError, req: Request, res: Response, _next: NextFunction): void {
|
||||
// Log error
|
||||
console.error('[Error]', {
|
||||
message: err.message,
|
||||
status: err.status || err.statusCode || 500,
|
||||
path: req.path,
|
||||
method: req.method,
|
||||
stack: process.env.NODE_ENV === 'development' ? err.stack : undefined,
|
||||
});
|
||||
|
||||
// Get status code
|
||||
const status = err.status || err.statusCode || 500;
|
||||
|
||||
// Send error response
|
||||
res.status(status).json({
|
||||
error: err.name || 'Error',
|
||||
message: err.message || 'An unexpected error occurred',
|
||||
status,
|
||||
...(process.env.NODE_ENV === 'development' && { stack: err.stack }),
|
||||
});
|
||||
}
|
||||
|
||||
export { errorHandler };
|
||||
@@ -1,43 +0,0 @@
|
||||
/**
|
||||
* Route Definitions
|
||||
*
|
||||
* Central route registration for all DevTool APIs.
|
||||
*/
|
||||
|
||||
import express from 'express';
|
||||
|
||||
// Controllers
|
||||
import tsdbController from './controllers/tsdb.controller';
|
||||
import controlController from './controllers/control.controller';
|
||||
import quickstartController from './controllers/quickstart.controller';
|
||||
import userController from './controllers/user.controller';
|
||||
import iamController from './controllers/iam.controller';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// =============================================================================
|
||||
// User Routes - Authentication and user management
|
||||
// =============================================================================
|
||||
router.use('/user', userController);
|
||||
|
||||
// =============================================================================
|
||||
// IAM Routes - Identity and Access Management
|
||||
// =============================================================================
|
||||
router.use('/iam', iamController);
|
||||
|
||||
// =============================================================================
|
||||
// TSDB Routes - Time Series Database for LLM metrics
|
||||
// =============================================================================
|
||||
router.use('/tsdb', tsdbController);
|
||||
|
||||
// =============================================================================
|
||||
// Control Routes - SDK control plane
|
||||
// =============================================================================
|
||||
router.use('/v1/control', controlController);
|
||||
|
||||
// =============================================================================
|
||||
// Quickstart Routes - SDK documentation generation
|
||||
// =============================================================================
|
||||
router.use('/quickstart', quickstartController);
|
||||
|
||||
export default router;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,733 +0,0 @@
|
||||
/**
|
||||
* Aden Control Sockets
|
||||
*
|
||||
* WebSocket namespace for real-time control plane communication.
|
||||
* Handles:
|
||||
* - SDK connections and authentication
|
||||
* - Real-time policy updates
|
||||
* - Event ingestion
|
||||
* - Heartbeat monitoring
|
||||
*/
|
||||
|
||||
import jwt from "jsonwebtoken";
|
||||
// Note: userDB.findSaltByToken will be injected via initialization
|
||||
import controlService from "./control_service";
|
||||
import llmEventBatcher from "./llm_event_batcher";
|
||||
import type { Server, Socket, Namespace } from "socket.io";
|
||||
|
||||
interface UserDbService {
|
||||
findSaltByToken: (token: string) => Promise<string | null>;
|
||||
}
|
||||
|
||||
let userDbService: UserDbService | null = null;
|
||||
let jwtSecret: string = "";
|
||||
|
||||
/**
|
||||
* Set user DB service for JWT verification
|
||||
* @param service - User DB service with findSaltByToken method
|
||||
* @param secret - JWT secret for token verification
|
||||
*/
|
||||
function setUserDbService(service: UserDbService, secret?: string): void {
|
||||
userDbService = service;
|
||||
if (secret) {
|
||||
jwtSecret = secret;
|
||||
}
|
||||
}
|
||||
|
||||
interface InstanceInfo {
|
||||
socket: Socket;
|
||||
instanceId: string;
|
||||
policyId: string | null;
|
||||
connectedAt: Date;
|
||||
lastHeartbeat: Date;
|
||||
}
|
||||
|
||||
// HTTP-only agents (no socket connection)
|
||||
interface HttpInstanceInfo {
|
||||
instanceId: string;
|
||||
policyId: string | null;
|
||||
agentName: string | null;
|
||||
status: string;
|
||||
firstSeen: Date;
|
||||
lastHeartbeat: Date;
|
||||
}
|
||||
|
||||
// Track connected SDK instances (WebSocket)
|
||||
// teamId -> Map<socketId, { socket, instanceId, policyId, connectedAt }>
|
||||
const connectedInstances = new Map<string, Map<string, InstanceInfo>>();
|
||||
|
||||
// Track HTTP-only SDK instances (no WebSocket, identified by heartbeats)
|
||||
// teamId -> Map<instanceId, { instanceId, policyId, status, firstSeen, lastHeartbeat }>
|
||||
const httpInstances = new Map<string, Map<string, HttpInstanceInfo>>();
|
||||
|
||||
// TTL for HTTP agents (remove if no heartbeat for this duration)
|
||||
const HTTP_AGENT_TTL_MS = 60000; // 60 seconds
|
||||
|
||||
// Store the control emitter globally for agent status broadcasts
|
||||
let globalControlEmitter: ControlEmitterInner | null = null;
|
||||
|
||||
// Track which teams have active subscriptions for agent status (team -> subscriber count)
|
||||
const teamSubscriberCounts = new Map<string, number>();
|
||||
|
||||
// Helper to get teams with active subscribers
|
||||
function getTeamsWithSubscribers(): string[] {
|
||||
return Array.from(teamSubscriberCounts.entries())
|
||||
.filter(([, count]) => count > 0)
|
||||
.map(([teamId]) => teamId);
|
||||
}
|
||||
|
||||
// Interval for periodic agent status broadcasts
|
||||
let agentStatusInterval: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
/**
|
||||
* Register or update an HTTP-only agent from heartbeat
|
||||
* Called from control_service when processing heartbeat events
|
||||
*/
|
||||
function registerHttpAgent(
|
||||
teamId: string | number,
|
||||
instanceId: string,
|
||||
policyId: string | null,
|
||||
agentName: string | null,
|
||||
status: string
|
||||
): void {
|
||||
const teamKey = String(teamId);
|
||||
|
||||
// Check if this instance is already connected via WebSocket
|
||||
const wsInstances = connectedInstances.get(teamKey);
|
||||
if (wsInstances) {
|
||||
for (const info of wsInstances.values()) {
|
||||
if (info.instanceId === instanceId) {
|
||||
// Already tracked via WebSocket, just update heartbeat there
|
||||
info.lastHeartbeat = new Date();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Track as HTTP-only agent
|
||||
if (!httpInstances.has(teamKey)) {
|
||||
httpInstances.set(teamKey, new Map());
|
||||
}
|
||||
|
||||
const existing = httpInstances.get(teamKey)!.get(instanceId);
|
||||
if (existing) {
|
||||
// Update existing
|
||||
existing.lastHeartbeat = new Date();
|
||||
existing.status = status;
|
||||
existing.policyId = policyId;
|
||||
existing.agentName = agentName;
|
||||
} else {
|
||||
// New HTTP agent
|
||||
httpInstances.get(teamKey)!.set(instanceId, {
|
||||
instanceId,
|
||||
policyId,
|
||||
agentName,
|
||||
status,
|
||||
firstSeen: new Date(),
|
||||
lastHeartbeat: new Date(),
|
||||
});
|
||||
console.log(
|
||||
`[Aden Control] HTTP agent registered: ${agentName || instanceId.slice(0, 8)}... (team: ${teamKey})`
|
||||
);
|
||||
|
||||
// Broadcast updated agent status to subscribers
|
||||
broadcastAgentStatus(teamKey);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up stale HTTP agents that haven't sent heartbeats
|
||||
*/
|
||||
function cleanupStaleHttpAgents(): void {
|
||||
const now = Date.now();
|
||||
const teamsWithRemovedAgents: string[] = [];
|
||||
|
||||
for (const [teamId, instances] of httpInstances) {
|
||||
let removed = false;
|
||||
for (const [instanceId, info] of instances) {
|
||||
if (now - info.lastHeartbeat.getTime() > HTTP_AGENT_TTL_MS) {
|
||||
instances.delete(instanceId);
|
||||
removed = true;
|
||||
console.log(
|
||||
`[Aden Control] HTTP agent expired: ${instanceId.slice(0, 8)}... (team: ${teamId})`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (removed) {
|
||||
teamsWithRemovedAgents.push(teamId);
|
||||
}
|
||||
|
||||
// Clean up empty team maps
|
||||
if (instances.size === 0) {
|
||||
httpInstances.delete(teamId);
|
||||
}
|
||||
}
|
||||
|
||||
// Broadcast updated status to teams that had agents removed
|
||||
for (const teamId of teamsWithRemovedAgents) {
|
||||
broadcastAgentStatus(teamId);
|
||||
}
|
||||
}
|
||||
|
||||
// Run cleanup every 30 seconds
|
||||
setInterval(cleanupStaleHttpAgents, 30000);
|
||||
|
||||
/**
|
||||
* Get agent status for a team
|
||||
*/
|
||||
function getAgentStatusForTeam(teamId: string): {
|
||||
type: string;
|
||||
active: boolean;
|
||||
count: number;
|
||||
instances: Array<{
|
||||
instance_id: string;
|
||||
policy_id: string | null;
|
||||
agent_name: string | null;
|
||||
connected_at: string;
|
||||
last_heartbeat: string;
|
||||
connection_type: "websocket" | "http";
|
||||
status?: string;
|
||||
}>;
|
||||
timestamp: string;
|
||||
} {
|
||||
const wsInstances = connectedInstances.get(teamId);
|
||||
const httpInsts = httpInstances.get(teamId);
|
||||
|
||||
const instances: Array<{
|
||||
instance_id: string;
|
||||
policy_id: string | null;
|
||||
agent_name: string | null;
|
||||
connected_at: string;
|
||||
last_heartbeat: string;
|
||||
connection_type: "websocket" | "http";
|
||||
status?: string;
|
||||
}> = [];
|
||||
|
||||
// Add WebSocket-connected instances
|
||||
if (wsInstances) {
|
||||
for (const info of wsInstances.values()) {
|
||||
instances.push({
|
||||
instance_id: info.instanceId,
|
||||
policy_id: info.policyId,
|
||||
agent_name: null,
|
||||
connected_at: info.connectedAt.toISOString(),
|
||||
last_heartbeat: info.lastHeartbeat.toISOString(),
|
||||
connection_type: "websocket",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Add HTTP-only instances
|
||||
if (httpInsts) {
|
||||
for (const info of httpInsts.values()) {
|
||||
instances.push({
|
||||
instance_id: info.instanceId,
|
||||
policy_id: info.policyId,
|
||||
agent_name: info.agentName,
|
||||
connected_at: info.firstSeen.toISOString(),
|
||||
last_heartbeat: info.lastHeartbeat.toISOString(),
|
||||
connection_type: "http",
|
||||
status: info.status,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const count = instances.length;
|
||||
|
||||
return {
|
||||
type: "agent-status",
|
||||
active: count > 0,
|
||||
count,
|
||||
instances,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcast agent status to all subscribed clients for a team
|
||||
*/
|
||||
function broadcastAgentStatus(teamId: string): void {
|
||||
if (!globalControlEmitter) return;
|
||||
|
||||
const status = getAgentStatusForTeam(teamId);
|
||||
const room = `team:${teamId}:llm-events`;
|
||||
globalControlEmitter.to(room).emit("message", status);
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcast agent status to all teams with subscribers
|
||||
*/
|
||||
function broadcastAgentStatusToAllTeams(): void {
|
||||
const teams = getTeamsWithSubscribers();
|
||||
for (const teamId of teams) {
|
||||
broadcastAgentStatus(teamId);
|
||||
}
|
||||
}
|
||||
|
||||
interface AdenSocket extends Socket {
|
||||
user?: Record<string, unknown>;
|
||||
teamId?: string;
|
||||
policyId?: string | null;
|
||||
sdkInstanceId?: string;
|
||||
}
|
||||
|
||||
interface RedisEmitter {
|
||||
of: (namespace: string) => ControlEmitterInner;
|
||||
}
|
||||
|
||||
interface ControlEmitterInner {
|
||||
to: (room: string) => { emit: (event: string, payload: unknown) => void };
|
||||
emit: (event: string, payload: unknown) => void;
|
||||
}
|
||||
|
||||
interface MessageData {
|
||||
event_type?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
interface ControlEmitter {
|
||||
emitPolicyUpdate: (teamId: string | number, policyId: string | null, policy: unknown) => void;
|
||||
emitCommand: (teamId: string | number, command: { action: string; [key: string]: unknown }) => void;
|
||||
emitAlert: (teamId: string | number, policyId: string | null, alert: unknown) => void;
|
||||
emitToInstance: (teamId: string | number, instanceId: string, message: unknown) => boolean;
|
||||
getConnectedCount: (teamId: string | number) => number;
|
||||
getConnectedInstances: (teamId: string | number) => Array<{
|
||||
instance_id: string;
|
||||
policy_id: string | null;
|
||||
agent_name: string | null;
|
||||
connected_at: string;
|
||||
last_heartbeat: string;
|
||||
connection_type: "websocket" | "http";
|
||||
status?: string;
|
||||
}>;
|
||||
getTotalConnectedCount: () => number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize Aden Control WebSocket namespace
|
||||
* @param io - Socket.IO server instance
|
||||
* @param rootEmitter - Redis emitter for cross-instance communication
|
||||
* @returns Control emitter for sending updates
|
||||
*/
|
||||
function initAdenControlSockets(io: Server, rootEmitter: RedisEmitter): ControlEmitter {
|
||||
// Create namespace for control plane
|
||||
const controlNamespace: Namespace = io.of("/v1/control/ws");
|
||||
|
||||
// Create emitter for this namespace
|
||||
const controlEmitter: ControlEmitterInner = rootEmitter.of("/v1/control/ws");
|
||||
|
||||
// Store globally for agent status broadcasts
|
||||
globalControlEmitter = controlEmitter;
|
||||
|
||||
// Start periodic agent status broadcast (every 2 seconds)
|
||||
if (agentStatusInterval) {
|
||||
clearInterval(agentStatusInterval);
|
||||
}
|
||||
agentStatusInterval = setInterval(broadcastAgentStatusToAllTeams, 2000);
|
||||
|
||||
// Initialize LLM event batcher with emitter for real-time streaming
|
||||
llmEventBatcher.setEmitter(controlEmitter as unknown as { to: (room: string) => { emit: (event: string, payload: unknown) => void } });
|
||||
|
||||
// Authentication middleware - verify JWT token
|
||||
controlNamespace.use(async (socket: AdenSocket, next: (err?: Error) => void) => {
|
||||
try {
|
||||
let token: string | undefined =
|
||||
socket.handshake.auth?.token ||
|
||||
socket.handshake.headers?.authorization ||
|
||||
(socket.handshake.query?.token as string | undefined);
|
||||
|
||||
if (!token) {
|
||||
console.error("[Aden Control WS] No authorization provided");
|
||||
return next(new Error("Authentication required"));
|
||||
}
|
||||
|
||||
// Extract token (support "Bearer <token>" and "jwt <token>" formats)
|
||||
if (token.startsWith("Bearer ")) {
|
||||
token = token.slice(7);
|
||||
} else if (token.startsWith("jwt ")) {
|
||||
token = token.slice(4);
|
||||
}
|
||||
|
||||
if (!token) {
|
||||
return next(new Error("Invalid token"));
|
||||
}
|
||||
|
||||
// Verify JWT token using user's salt
|
||||
if (!userDbService) {
|
||||
console.error("[Aden Control WS] userDbService not initialized");
|
||||
return next(new Error("Server configuration error"));
|
||||
}
|
||||
const salt = await userDbService.findSaltByToken(token);
|
||||
if (!salt) {
|
||||
console.error("[Aden Control WS] No salt found for token");
|
||||
return next(new Error("Invalid token"));
|
||||
}
|
||||
// Token is signed with jwtSecret + salt
|
||||
const verifySecret = jwtSecret ? jwtSecret + salt : salt;
|
||||
const decoded = await new Promise<Record<string, unknown>>((resolve, reject) => {
|
||||
jwt.verify(token!, verifySecret, (err, decoded) => {
|
||||
if (err) reject(err);
|
||||
else resolve(decoded as Record<string, unknown>);
|
||||
});
|
||||
});
|
||||
|
||||
// Store user info on socket
|
||||
socket.user = decoded;
|
||||
socket.teamId = decoded.current_team_id as string;
|
||||
socket.policyId =
|
||||
(socket.handshake.headers?.["x-policy-id"] as string) ||
|
||||
(socket.handshake.query?.policy_id as string) ||
|
||||
null;
|
||||
socket.sdkInstanceId =
|
||||
(socket.handshake.headers?.["x-sdk-instance-id"] as string) ||
|
||||
(socket.handshake.query?.instance_id as string) ||
|
||||
socket.id;
|
||||
|
||||
console.log(
|
||||
`[Aden Control WS] SDK connecting: ${socket.sdkInstanceId!.slice(0, 8)}... (team: ${socket.teamId})`
|
||||
);
|
||||
|
||||
next();
|
||||
} catch (error) {
|
||||
console.error("[Aden Control WS] Auth error:", (error as Error).message);
|
||||
next(new Error("Authentication failed"));
|
||||
}
|
||||
});
|
||||
|
||||
// Handle connections
|
||||
controlNamespace.on("connection", async (socket: AdenSocket) => {
|
||||
const { teamId, policyId, sdkInstanceId } = socket;
|
||||
|
||||
console.log(
|
||||
`[Aden Control WS] SDK connected: ${sdkInstanceId!.slice(0, 8)}... (socket: ${socket.id}, team: ${teamId})`
|
||||
);
|
||||
|
||||
// Track this instance by team
|
||||
if (!connectedInstances.has(teamId!)) {
|
||||
connectedInstances.set(teamId!, new Map());
|
||||
}
|
||||
connectedInstances.get(teamId!)!.set(socket.id, {
|
||||
socket,
|
||||
instanceId: sdkInstanceId!,
|
||||
policyId: policyId || null,
|
||||
connectedAt: new Date(),
|
||||
lastHeartbeat: new Date(),
|
||||
});
|
||||
|
||||
// Join room for this team (for policy broadcasts)
|
||||
socket.join(`team:${teamId}`);
|
||||
// Also join policy-specific room if policy specified
|
||||
if (policyId) {
|
||||
socket.join(`team:${teamId}:policy:${policyId}`);
|
||||
}
|
||||
|
||||
// Send current policy immediately
|
||||
try {
|
||||
const policy = await controlService.getPolicy(teamId!, policyId || null);
|
||||
socket.emit("message", {
|
||||
type: "policy",
|
||||
policy,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("[Aden Control WS] Error sending initial policy:", error);
|
||||
}
|
||||
|
||||
// Handle incoming messages from SDK
|
||||
socket.on("message", async (data: MessageData | string) => {
|
||||
try {
|
||||
await handleSdkMessage(socket, data);
|
||||
} catch (error) {
|
||||
console.error("[Aden Control WS] Error handling message:", error);
|
||||
socket.emit("message", {
|
||||
type: "error",
|
||||
error: (error as Error).message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Handle direct event submission (alternative to message)
|
||||
socket.on("event", async (event: Record<string, unknown>) => {
|
||||
try {
|
||||
await controlService.processEvents(teamId!, policyId || null, [event as any]);
|
||||
} catch (error) {
|
||||
console.error("[Aden Control WS] Error processing event:", error);
|
||||
}
|
||||
});
|
||||
|
||||
// Handle disconnection
|
||||
socket.on("disconnect", (reason: string) => {
|
||||
console.log(
|
||||
`[Aden Control WS] SDK disconnected: ${sdkInstanceId!.slice(0, 8)}... (reason: ${reason})`
|
||||
);
|
||||
|
||||
// Remove from tracking
|
||||
const instances = connectedInstances.get(teamId!);
|
||||
if (instances) {
|
||||
instances.delete(socket.id);
|
||||
if (instances.size === 0) {
|
||||
connectedInstances.delete(teamId!);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Handle errors
|
||||
socket.on("error", (error: Error) => {
|
||||
console.error(
|
||||
`[Aden Control WS] Socket error for ${sdkInstanceId!.slice(0, 8)}...:`,
|
||||
error.message
|
||||
);
|
||||
});
|
||||
|
||||
// Handle LLM events stream subscription (for dashboard real-time updates)
|
||||
socket.on("subscribe-llm-events", () => {
|
||||
const room = `team:${teamId}:llm-events`;
|
||||
socket.join(room);
|
||||
console.log(`[Aden Control WS] Socket ${socket.id} subscribed to ${room}`);
|
||||
|
||||
// Track subscriber count for this team
|
||||
const currentCount = teamSubscriberCounts.get(teamId!) || 0;
|
||||
teamSubscriberCounts.set(teamId!, currentCount + 1);
|
||||
|
||||
socket.emit("message", {
|
||||
type: "subscribed",
|
||||
stream: "llm-events",
|
||||
teamId: teamId,
|
||||
});
|
||||
|
||||
// Send initial agent status
|
||||
const status = getAgentStatusForTeam(teamId!);
|
||||
socket.emit("message", status);
|
||||
});
|
||||
|
||||
socket.on("unsubscribe-llm-events", () => {
|
||||
const room = `team:${teamId}:llm-events`;
|
||||
socket.leave(room);
|
||||
console.log(`[Aden Control WS] Socket ${socket.id} unsubscribed from ${room}`);
|
||||
|
||||
// Decrement subscriber count
|
||||
const currentCount = teamSubscriberCounts.get(teamId!) || 0;
|
||||
if (currentCount > 0) {
|
||||
teamSubscriberCounts.set(teamId!, currentCount - 1);
|
||||
}
|
||||
|
||||
socket.emit("message", {
|
||||
type: "unsubscribed",
|
||||
stream: "llm-events",
|
||||
teamId: teamId,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Handle incoming message from SDK
|
||||
*/
|
||||
async function handleSdkMessage(socket: AdenSocket, data: MessageData | string): Promise<void> {
|
||||
// Parse if string
|
||||
let parsedData: MessageData;
|
||||
if (typeof data === "string") {
|
||||
parsedData = JSON.parse(data);
|
||||
} else {
|
||||
parsedData = data;
|
||||
}
|
||||
|
||||
const { teamId, policyId, sdkInstanceId } = socket;
|
||||
|
||||
// Route based on event type
|
||||
switch (parsedData.event_type) {
|
||||
case "metric":
|
||||
case "control":
|
||||
case "heartbeat":
|
||||
case "error":
|
||||
// Process as event
|
||||
await controlService.processEvents(teamId!, policyId || null, [parsedData as any]);
|
||||
|
||||
// Update last heartbeat time
|
||||
if (parsedData.event_type === "heartbeat") {
|
||||
const instances = connectedInstances.get(teamId!);
|
||||
const instance = instances?.get(socket.id);
|
||||
if (instance) {
|
||||
instance.lastHeartbeat = new Date();
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case "get_policy": {
|
||||
// Request for current policy
|
||||
const policy = await controlService.getPolicy(teamId!, policyId || null);
|
||||
socket.emit("message", {
|
||||
type: "policy",
|
||||
policy,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
console.warn(
|
||||
`[Aden Control WS] Unknown event type from ${sdkInstanceId!.slice(0, 8)}...: ${parsedData.event_type}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create emitter object for external use
|
||||
*/
|
||||
const emitter: ControlEmitter = {
|
||||
/**
|
||||
* Emit policy update to all SDK instances for a team/policy
|
||||
* @param teamId - The team ID
|
||||
* @param policyId - The policy ID (optional, broadcasts to all team instances if not specified)
|
||||
* @param policy - The policy object
|
||||
*/
|
||||
emitPolicyUpdate(teamId: string | number, policyId: string | null, policy: unknown): void {
|
||||
console.log(`[Aden Control WS] Broadcasting policy update for team ${teamId}`);
|
||||
|
||||
// If policyId specified, emit only to instances using that policy
|
||||
if (policyId) {
|
||||
controlEmitter.to(`team:${teamId}:policy:${policyId}`).emit("message", {
|
||||
type: "policy",
|
||||
policy,
|
||||
});
|
||||
} else {
|
||||
// Broadcast to all team instances
|
||||
controlEmitter.to(`team:${teamId}`).emit("message", {
|
||||
type: "policy",
|
||||
policy,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Emit a command to all SDK instances for a team
|
||||
*/
|
||||
emitCommand(teamId: string | number, command: { action: string; [key: string]: unknown }): void {
|
||||
console.log(`[Aden Control WS] Broadcasting command: ${command.action}`);
|
||||
|
||||
controlEmitter.to(`team:${teamId}`).emit("message", {
|
||||
type: "command",
|
||||
command,
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Emit alert to team instances
|
||||
*/
|
||||
emitAlert(teamId: string | number, policyId: string | null, alert: unknown): void {
|
||||
console.log(`[Aden Control WS] Broadcasting alert for team ${teamId}`);
|
||||
|
||||
const room = policyId ? `team:${teamId}:policy:${policyId}` : `team:${teamId}`;
|
||||
controlEmitter.to(room).emit("message", {
|
||||
type: "alert",
|
||||
alert,
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Emit to a specific SDK instance
|
||||
*/
|
||||
emitToInstance(teamId: string | number, instanceId: string, message: unknown): boolean {
|
||||
const instances = connectedInstances.get(String(teamId));
|
||||
if (!instances) return false;
|
||||
|
||||
for (const [, info] of instances) {
|
||||
if (info.instanceId === instanceId) {
|
||||
info.socket.emit("message", message);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get connected instance count for a team (WebSocket + HTTP)
|
||||
*/
|
||||
getConnectedCount(teamId: string | number): number {
|
||||
const teamKey = String(teamId);
|
||||
const wsCount = connectedInstances.get(teamKey)?.size || 0;
|
||||
const httpCount = httpInstances.get(teamKey)?.size || 0;
|
||||
return wsCount + httpCount;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get all connected instances info (for dashboard)
|
||||
* Includes both WebSocket and HTTP-only agents
|
||||
*/
|
||||
getConnectedInstances(teamId: string | number): Array<{
|
||||
instance_id: string;
|
||||
policy_id: string | null;
|
||||
agent_name: string | null;
|
||||
connected_at: string;
|
||||
last_heartbeat: string;
|
||||
connection_type: "websocket" | "http";
|
||||
status?: string;
|
||||
}> {
|
||||
const teamKey = String(teamId);
|
||||
const results: Array<{
|
||||
instance_id: string;
|
||||
policy_id: string | null;
|
||||
agent_name: string | null;
|
||||
connected_at: string;
|
||||
last_heartbeat: string;
|
||||
connection_type: "websocket" | "http";
|
||||
status?: string;
|
||||
}> = [];
|
||||
|
||||
// Add WebSocket-connected instances
|
||||
const wsInstances = connectedInstances.get(teamKey);
|
||||
if (wsInstances) {
|
||||
for (const info of wsInstances.values()) {
|
||||
results.push({
|
||||
instance_id: info.instanceId,
|
||||
policy_id: info.policyId,
|
||||
agent_name: null, // WebSocket connections don't have agent_name yet
|
||||
connected_at: info.connectedAt.toISOString(),
|
||||
last_heartbeat: info.lastHeartbeat.toISOString(),
|
||||
connection_type: "websocket",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Add HTTP-only instances
|
||||
const httpInsts = httpInstances.get(teamKey);
|
||||
if (httpInsts) {
|
||||
for (const info of httpInsts.values()) {
|
||||
results.push({
|
||||
instance_id: info.instanceId,
|
||||
policy_id: info.policyId,
|
||||
agent_name: info.agentName,
|
||||
connected_at: info.firstSeen.toISOString(),
|
||||
last_heartbeat: info.lastHeartbeat.toISOString(),
|
||||
connection_type: "http",
|
||||
status: info.status,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get total connected SDK count across all teams (WebSocket + HTTP)
|
||||
*/
|
||||
getTotalConnectedCount(): number {
|
||||
let total = 0;
|
||||
for (const instances of connectedInstances.values()) {
|
||||
total += instances.size;
|
||||
}
|
||||
for (const instances of httpInstances.values()) {
|
||||
total += instances.size;
|
||||
}
|
||||
return total;
|
||||
},
|
||||
};
|
||||
|
||||
// Note: Emitter is returned instead of stored globally
|
||||
// Use app.locals.controlEmitter to access in routes
|
||||
|
||||
console.log("[Aden Control WS] WebSocket namespace initialized at /v1/control/ws");
|
||||
|
||||
return emitter;
|
||||
}
|
||||
|
||||
export default initAdenControlSockets;
|
||||
export { setUserDbService, registerHttpAgent };
|
||||
@@ -1,349 +0,0 @@
|
||||
/**
|
||||
* LLMEventBatcher - Batches LLM events for efficient WebSocket delivery
|
||||
*
|
||||
* Features:
|
||||
* - Per-team in-memory buffers
|
||||
* - 5-second flush interval (configurable)
|
||||
* - Buffer size cap with graceful degradation (drop oldest)
|
||||
* - Payload optimization (only essential fields)
|
||||
* - Periodic cleanup for idle teams
|
||||
*/
|
||||
|
||||
const FLUSH_REASONS = {
|
||||
TIMER: 1,
|
||||
BUFFER_FULL: 2,
|
||||
MANUAL: 3,
|
||||
} as const;
|
||||
|
||||
type FlushReason = typeof FLUSH_REASONS[keyof typeof FLUSH_REASONS];
|
||||
|
||||
interface TsdbEvent {
|
||||
timestamp?: Date | string;
|
||||
trace_id?: string;
|
||||
model?: string;
|
||||
provider?: string;
|
||||
agent?: string;
|
||||
cost_total?: number;
|
||||
latency_ms?: number;
|
||||
usage?: {
|
||||
input_tokens?: number;
|
||||
output_tokens?: number;
|
||||
};
|
||||
usage_input_tokens?: number;
|
||||
usage_output_tokens?: number;
|
||||
}
|
||||
|
||||
interface EventSummary {
|
||||
timestamp: string | undefined;
|
||||
trace_id: string | undefined;
|
||||
model: string;
|
||||
provider: string | null;
|
||||
agent: string | null;
|
||||
input_tokens: number;
|
||||
output_tokens: number;
|
||||
cost: number;
|
||||
latency_ms: number | null;
|
||||
}
|
||||
|
||||
interface TeamBuffer {
|
||||
teamId: string;
|
||||
events: EventSummary[];
|
||||
flushTimer: ReturnType<typeof setTimeout> | null;
|
||||
lastFlush: Date;
|
||||
droppedCount: number;
|
||||
}
|
||||
|
||||
interface BatchPayload {
|
||||
type: string;
|
||||
teamId: string;
|
||||
events: EventSummary[];
|
||||
meta: {
|
||||
batchSize: number;
|
||||
droppedCount: number;
|
||||
windowStart: string | undefined;
|
||||
windowEnd: string | undefined;
|
||||
flushReason: FlushReason;
|
||||
};
|
||||
}
|
||||
|
||||
interface Emitter {
|
||||
to: (room: string) => { emit: (event: string, payload: BatchPayload) => void };
|
||||
}
|
||||
|
||||
interface BatcherOptions {
|
||||
flushIntervalMs?: number;
|
||||
maxBufferSize?: number;
|
||||
maxEventsPerFlush?: number;
|
||||
}
|
||||
|
||||
class LLMEventBatcher {
|
||||
private flushIntervalMs: number;
|
||||
private maxBufferSize: number;
|
||||
private maxEventsPerFlush: number;
|
||||
private teamBuffers: Map<string, TeamBuffer>;
|
||||
private emitter: Emitter | null;
|
||||
private totalEventsBuffered: number;
|
||||
private totalBatchesSent: number;
|
||||
private totalEventsDropped: number;
|
||||
private _cleanupInterval: ReturnType<typeof setInterval>;
|
||||
|
||||
constructor(options: BatcherOptions = {}) {
|
||||
// Configuration
|
||||
this.flushIntervalMs = options.flushIntervalMs || 5000; // 5 seconds
|
||||
this.maxBufferSize = options.maxBufferSize || 500; // Max events per team buffer
|
||||
this.maxEventsPerFlush = options.maxEventsPerFlush || 100; // Max events per batch
|
||||
|
||||
// State
|
||||
this.teamBuffers = new Map(); // teamId -> TeamBuffer
|
||||
this.emitter = null; // Set by setEmitter()
|
||||
|
||||
// Metrics
|
||||
this.totalEventsBuffered = 0;
|
||||
this.totalBatchesSent = 0;
|
||||
this.totalEventsDropped = 0;
|
||||
|
||||
// Start periodic cleanup
|
||||
this._cleanupInterval = setInterval(() => {
|
||||
this.cleanup();
|
||||
}, 300000); // Every 5 minutes
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the Socket.IO emitter for broadcasting
|
||||
* Called during control_sockets initialization
|
||||
* @param {Object} controlEmitter - Socket.IO namespace emitter
|
||||
*/
|
||||
setEmitter(controlEmitter: Emitter): void {
|
||||
this.emitter = controlEmitter;
|
||||
console.log("[LLMEventBatcher] Emitter configured");
|
||||
}
|
||||
|
||||
/**
|
||||
* Add events to the buffer for a team
|
||||
* Called from control_service.js after TSDB insert
|
||||
* @param {string|number} teamId - Team identifier
|
||||
* @param {Array} tsdbEvents - Array of TSDB events
|
||||
*/
|
||||
add(teamId: string | number, tsdbEvents: TsdbEvent[]): void {
|
||||
if (!tsdbEvents || tsdbEvents.length === 0) return;
|
||||
|
||||
const teamIdStr = String(teamId);
|
||||
|
||||
// Transform to lightweight summaries
|
||||
const summaries = tsdbEvents.map((e) => this._transformToSummary(e));
|
||||
|
||||
// Get or create buffer
|
||||
let buffer = this.teamBuffers.get(teamIdStr);
|
||||
if (!buffer) {
|
||||
buffer = this._createBuffer(teamIdStr);
|
||||
this.teamBuffers.set(teamIdStr, buffer);
|
||||
}
|
||||
|
||||
// Add events with overflow handling
|
||||
this._addToBuffer(buffer, summaries);
|
||||
|
||||
// Start/reset flush timer if not already running
|
||||
this._scheduleFlush(teamIdStr, buffer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform full TSDB event to lightweight summary
|
||||
* Only includes fields needed for dashboard display
|
||||
* @param {Object} event - Full TSDB event
|
||||
* @returns {Object} Lightweight event summary
|
||||
*/
|
||||
private _transformToSummary(event: TsdbEvent): EventSummary {
|
||||
// Handle both nested usage object (from transformMetricToTsdbEvent)
|
||||
// and flat fields (from TSDB query results)
|
||||
const inputTokens = event.usage?.input_tokens ?? event.usage_input_tokens ?? 0;
|
||||
const outputTokens = event.usage?.output_tokens ?? event.usage_output_tokens ?? 0;
|
||||
|
||||
return {
|
||||
timestamp: event.timestamp instanceof Date ? event.timestamp.toISOString() : event.timestamp,
|
||||
trace_id: event.trace_id,
|
||||
model: event.model || "",
|
||||
provider: event.provider || null,
|
||||
agent: event.agent || null,
|
||||
input_tokens: inputTokens,
|
||||
output_tokens: outputTokens,
|
||||
cost: event.cost_total || 0,
|
||||
latency_ms: event.latency_ms || null,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Add events to buffer with overflow handling
|
||||
* @param {Object} buffer - Team buffer
|
||||
* @param {Array} summaries - Event summaries to add
|
||||
*/
|
||||
private _addToBuffer(buffer: TeamBuffer, summaries: EventSummary[]): void {
|
||||
for (const summary of summaries) {
|
||||
if (buffer.events.length >= this.maxBufferSize) {
|
||||
// Drop oldest event
|
||||
buffer.events.shift();
|
||||
buffer.droppedCount++;
|
||||
this.totalEventsDropped++;
|
||||
}
|
||||
buffer.events.push(summary);
|
||||
this.totalEventsBuffered++;
|
||||
}
|
||||
|
||||
// Force flush if buffer is full
|
||||
if (buffer.events.length >= this.maxBufferSize) {
|
||||
this._flush(buffer.teamId, FLUSH_REASONS.BUFFER_FULL);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule flush timer for a team
|
||||
* @param {string} teamId - Team identifier
|
||||
* @param {Object} buffer - Team buffer
|
||||
*/
|
||||
private _scheduleFlush(teamId: string, buffer: TeamBuffer): void {
|
||||
// Don't reschedule if timer already running
|
||||
if (buffer.flushTimer) return;
|
||||
|
||||
buffer.flushTimer = setTimeout(() => {
|
||||
this._flush(teamId, FLUSH_REASONS.TIMER);
|
||||
}, this.flushIntervalMs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Flush buffered events to WebSocket
|
||||
* @param {string} teamId - Team identifier
|
||||
* @param {number} flushReason - Reason for flush
|
||||
*/
|
||||
private _flush(teamId: string, flushReason: FlushReason): void {
|
||||
const buffer = this.teamBuffers.get(teamId);
|
||||
if (!buffer || buffer.events.length === 0) return;
|
||||
|
||||
// Clear timer
|
||||
if (buffer.flushTimer) {
|
||||
clearTimeout(buffer.flushTimer);
|
||||
buffer.flushTimer = null;
|
||||
}
|
||||
|
||||
// Extract batch (up to maxEventsPerFlush)
|
||||
const batch = buffer.events.splice(0, this.maxEventsPerFlush);
|
||||
const droppedCount = buffer.droppedCount;
|
||||
buffer.droppedCount = 0;
|
||||
buffer.lastFlush = new Date();
|
||||
|
||||
// Build payload
|
||||
const payload: BatchPayload = {
|
||||
type: "llm-events-batch",
|
||||
teamId: teamId,
|
||||
events: batch,
|
||||
meta: {
|
||||
batchSize: batch.length,
|
||||
droppedCount: droppedCount,
|
||||
windowStart: batch[0]?.timestamp,
|
||||
windowEnd: batch[batch.length - 1]?.timestamp,
|
||||
flushReason: flushReason,
|
||||
},
|
||||
};
|
||||
|
||||
// Emit to team room
|
||||
if (this.emitter) {
|
||||
const room = `team:${teamId}:llm-events`;
|
||||
this.emitter.to(room).emit("message", payload);
|
||||
this.totalBatchesSent++;
|
||||
|
||||
if (batch.length > 0) {
|
||||
console.log(
|
||||
`[LLMEventBatcher] Flushed ${batch.length} events to ${room} ` +
|
||||
`(dropped: ${droppedCount}, reason: ${flushReason})`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Schedule next flush if buffer still has events
|
||||
if (buffer.events.length > 0) {
|
||||
this._scheduleFlush(teamId, buffer);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new buffer for a team
|
||||
* @param {string} teamId - Team identifier
|
||||
* @returns {Object} New team buffer
|
||||
*/
|
||||
private _createBuffer(teamId: string): TeamBuffer {
|
||||
return {
|
||||
teamId: teamId,
|
||||
events: [],
|
||||
flushTimer: null,
|
||||
lastFlush: new Date(),
|
||||
droppedCount: 0,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Manually flush all buffers (useful for shutdown)
|
||||
*/
|
||||
flushAll(): void {
|
||||
for (const [teamId] of this.teamBuffers) {
|
||||
this._flush(teamId, FLUSH_REASONS.MANUAL);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get metrics for monitoring
|
||||
* @returns {Object} Batcher metrics
|
||||
*/
|
||||
getMetrics(): { activeTeams: number; totalBuffered: number; totalEventsBuffered: number; totalBatchesSent: number; totalEventsDropped: number } {
|
||||
const activeTeams = this.teamBuffers.size;
|
||||
const totalBuffered = Array.from(this.teamBuffers.values()).reduce(
|
||||
(sum, b) => sum + b.events.length,
|
||||
0
|
||||
);
|
||||
|
||||
return {
|
||||
activeTeams,
|
||||
totalBuffered,
|
||||
totalEventsBuffered: this.totalEventsBuffered,
|
||||
totalBatchesSent: this.totalBatchesSent,
|
||||
totalEventsDropped: this.totalEventsDropped,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup buffers for teams with no recent activity
|
||||
* Prevents memory leaks from inactive teams
|
||||
* @param {number} maxIdleMs - Max idle time before cleanup (default: 5 minutes)
|
||||
*/
|
||||
cleanup(maxIdleMs = 300000): void {
|
||||
const now = Date.now();
|
||||
let cleaned = 0;
|
||||
|
||||
for (const [teamId, buffer] of this.teamBuffers.entries()) {
|
||||
if (buffer.events.length === 0 && now - buffer.lastFlush.getTime() > maxIdleMs) {
|
||||
if (buffer.flushTimer) {
|
||||
clearTimeout(buffer.flushTimer);
|
||||
}
|
||||
this.teamBuffers.delete(teamId);
|
||||
cleaned++;
|
||||
}
|
||||
}
|
||||
|
||||
if (cleaned > 0) {
|
||||
console.log(`[LLMEventBatcher] Cleaned up ${cleaned} idle team buffers`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shutdown the batcher (cleanup intervals and flush remaining)
|
||||
*/
|
||||
shutdown(): void {
|
||||
if (this._cleanupInterval) {
|
||||
clearInterval(this._cleanupInterval);
|
||||
}
|
||||
this.flushAll();
|
||||
console.log("[LLMEventBatcher] Shutdown complete");
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton instance
|
||||
const llmEventBatcher = new LLMEventBatcher();
|
||||
|
||||
export default llmEventBatcher;
|
||||
@@ -1,26 +0,0 @@
|
||||
import config from "../../config";
|
||||
import { MongoClient } from "mongodb";
|
||||
|
||||
declare const _ACHO_MG_DB: undefined | { db: (name: string) => unknown };
|
||||
|
||||
let client: MongoClient | null = null;
|
||||
|
||||
const getMongoClient = async (): Promise<MongoClient> => {
|
||||
if (client) return client;
|
||||
if (!config.mongodb.url) {
|
||||
throw new Error("Missing MONGODB_URL in environment");
|
||||
}
|
||||
client = new MongoClient(config.mongodb.url);
|
||||
await client.connect();
|
||||
return client;
|
||||
};
|
||||
|
||||
const getMongoDb = async (dbName = config.mongodb.dbName): Promise<unknown> => {
|
||||
if (typeof _ACHO_MG_DB !== "undefined" && _ACHO_MG_DB && typeof _ACHO_MG_DB.db === "function") {
|
||||
return _ACHO_MG_DB.db(dbName);
|
||||
}
|
||||
const c = await getMongoClient();
|
||||
return c.db(dbName);
|
||||
};
|
||||
|
||||
export { getMongoDb };
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user