agentomatic 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agentomatic/__init__.py +59 -0
- agentomatic/_version.py +5 -0
- agentomatic/cli/__init__.py +7 -0
- agentomatic/cli/commands.py +715 -0
- agentomatic/cli/templates.py +188 -0
- agentomatic/config/__init__.py +3 -0
- agentomatic/config/defaults.py +10 -0
- agentomatic/config/settings.py +117 -0
- agentomatic/core/__init__.py +31 -0
- agentomatic/core/lifespan.py +102 -0
- agentomatic/core/manifest.py +100 -0
- agentomatic/core/platform.py +571 -0
- agentomatic/core/registry.py +198 -0
- agentomatic/core/router_factory.py +541 -0
- agentomatic/core/state.py +63 -0
- agentomatic/middleware/__init__.py +18 -0
- agentomatic/middleware/auth.py +53 -0
- agentomatic/middleware/feedback.py +207 -0
- agentomatic/middleware/logging.py +40 -0
- agentomatic/middleware/metrics.py +93 -0
- agentomatic/middleware/rate_limit.py +70 -0
- agentomatic/observability/__init__.py +11 -0
- agentomatic/observability/concurrency.py +109 -0
- agentomatic/observability/metrics.py +101 -0
- agentomatic/observability/telemetry.py +316 -0
- agentomatic/optimize/__init__.py +112 -0
- agentomatic/optimize/dataset.py +142 -0
- agentomatic/optimize/loop.py +870 -0
- agentomatic/optimize/metrics.py +781 -0
- agentomatic/optimize/optimizer.py +891 -0
- agentomatic/optimize/report.py +774 -0
- agentomatic/optimize/runner.py +261 -0
- agentomatic/optimize/strategies.py +592 -0
- agentomatic/optimize/synthesizer.py +729 -0
- agentomatic/prompts/__init__.py +7 -0
- agentomatic/prompts/manager.py +59 -0
- agentomatic/protocols/__init__.py +3 -0
- agentomatic/protocols/decorators.py +75 -0
- agentomatic/providers/__init__.py +3 -0
- agentomatic/providers/embeddings.py +44 -0
- agentomatic/providers/llm.py +116 -0
- agentomatic/py.typed +1 -0
- agentomatic/storage/__init__.py +40 -0
- agentomatic/storage/base.py +192 -0
- agentomatic/storage/memory.py +167 -0
- agentomatic/storage/models.py +129 -0
- agentomatic/storage/sqlalchemy.py +317 -0
- agentomatic/ui/.chainlit/config.toml +14 -0
- agentomatic/ui/__init__.py +50 -0
- agentomatic/ui/chat.py +198 -0
- agentomatic-0.1.0.dist-info/METADATA +363 -0
- agentomatic-0.1.0.dist-info/RECORD +55 -0
- agentomatic-0.1.0.dist-info/WHEEL +4 -0
- agentomatic-0.1.0.dist-info/entry_points.txt +2 -0
- agentomatic-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"""Agent scaffolding templates.
|
|
2
|
+
|
|
3
|
+
Each template is a dict mapping relative file paths to their content.
|
|
4
|
+
Templates: basic, full, rag, chatbot, custom.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _init_py(name: str, description: str, keywords: str, framework: str = "langgraph") -> str:
|
|
11
|
+
return f'''"""Agent: {name}."""\nfrom __future__ import annotations\n\nfrom typing import Any\n\nfrom agentomatic import AgentManifest\n\nmanifest = AgentManifest(\n name="{name}",\n slug="agent-{name}",\n description="{description}",\n intent_keywords=[{keywords}],\n framework="{framework}",\n)\n\n\nasync def node_fn(state: dict[str, Any]) -> dict[str, Any]:\n from .graph import get_graph\n return await get_graph().ainvoke(state)\n'''
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _graph_py(name: str) -> str:
|
|
15
|
+
return f'''"""LangGraph graph for {name}."""\nfrom __future__ import annotations\n\nfrom functools import lru_cache\n\nfrom langgraph.graph import END, StateGraph\n\nfrom agentomatic import BaseAgentState\n\nfrom . import nodes\n\n\ndef build_graph() -> StateGraph:\n g = StateGraph(BaseAgentState)\n g.add_node("process", nodes.process)\n g.set_entry_point("process")\n g.add_edge("process", END)\n return g\n\n\n@lru_cache(maxsize=1)\ndef get_graph():\n return build_graph().compile()\n'''
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _nodes_py(name: str) -> str:
|
|
19
|
+
return f'''"""Node functions for {name}."""\nfrom __future__ import annotations\n\nfrom typing import Any\n\n\nasync def process(state: dict[str, Any]) -> dict[str, Any]:\n query = state.get("current_query", "")\n return {{\n "response": f"Hello from {name}! You asked: {{query}}",\n "agent_type": "agent-{name}",\n "suggestions": ["Tell me more", "Help me with something else"],\n }}\n'''
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _config_py(name: str) -> str:
|
|
23
|
+
title = name.replace("_", " ").title()
|
|
24
|
+
return f'''"""Configuration for {title} agent."""\nfrom __future__ import annotations\n\nfrom pydantic import BaseModel, Field\n\n\nclass {title.replace(" ", "")}Config(BaseModel):\n """Agent-specific configuration."""\n\n prompt_version: str = Field("v1", description="Active prompt version")\n temperature: float = Field(0.1, ge=0.0, le=2.0)\n max_tokens: int = Field(2048, ge=1)\n enable_memory: bool = Field(True, description="Enable conversation memory")\n'''
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _schemas_py(name: str) -> str:
|
|
28
|
+
title = name.replace("_", " ").title().replace(" ", "")
|
|
29
|
+
return f'''"""Custom schemas for {name}."""\nfrom __future__ import annotations\n\nfrom pydantic import BaseModel, Field\n\n\nclass {title}Request(BaseModel):\n """Custom request model."""\n query: str = Field(..., description="User query")\n context: dict = Field(default_factory=dict)\n\n\nclass {title}Response(BaseModel):\n """Custom response model."""\n answer: str\n confidence: float = Field(0.0, ge=0.0, le=1.0)\n sources: list[str] = Field(default_factory=list)\n'''
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _tools_py(name: str) -> str:
|
|
33
|
+
return f'''"""LangChain-compatible tools for {name}."""\nfrom __future__ import annotations\n\n\ndef search(query: str) -> str:\n """Search for information.\n\n Args:\n query: Search query string.\n\n Returns:\n Search results.\n """\n return f"Results for: {{query}}"\n'''
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _api_py(name: str) -> str:
|
|
37
|
+
return f'''"""Custom API router for {name}.\n\nIf this file exports a `router`, it REPLACES the auto-generated endpoints.\nRemove this file to use auto-generated endpoints instead.\n"""\nfrom __future__ import annotations\n\nfrom fastapi import APIRouter\n\nrouter = APIRouter()\n\n\n@router.get("/status")\nasync def status() -> dict:\n """Custom status endpoint."""\n return {{"agent": "{name}", "custom_router": True}}\n'''
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _prompts_json() -> str:
|
|
41
|
+
return """{
|
|
42
|
+
"v1": {
|
|
43
|
+
"system": "You are a helpful AI assistant. Be concise and accurate.",
|
|
44
|
+
"user_template": "{query}"
|
|
45
|
+
},
|
|
46
|
+
"v2": {
|
|
47
|
+
"system": "You are an advanced AI assistant. Provide detailed, well-structured responses with examples when helpful.",
|
|
48
|
+
"user_template": "Please help with the following: {query}"
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _langgraph_json() -> str:
|
|
55
|
+
return """{
|
|
56
|
+
"dependencies": ["."],
|
|
57
|
+
"graphs": {
|
|
58
|
+
"agent": "./graph.py:get_graph"
|
|
59
|
+
},
|
|
60
|
+
"env": ".env"
|
|
61
|
+
}
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _env_example(name: str) -> str:
|
|
66
|
+
upper = name.upper()
|
|
67
|
+
return f"""# {name} agent configuration\n# Copy to .env and fill in values\n\n# LLM Settings\n{upper}_LLM_PROVIDER=ollama\n{upper}_LLM_MODEL=mistral:7b\n{upper}_TEMPERATURE=0.1\n{upper}_MAX_TOKENS=2048\n\n# Feature Flags\n{upper}_ENABLE_MEMORY=true\n{upper}_ENABLE_STREAMING=true\n"""
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _readme_md(name: str, template: str) -> str:
|
|
71
|
+
title = name.replace("_", " ").title()
|
|
72
|
+
return f"""# {title} Agent\n\nGenerated with `agentomatic init {name} --template {template}`.\n\n## Quick Start\n\n```bash\n# Start the platform\nagentomatic run\n\n# Test the agent\ncurl -X POST http://localhost:8000/api/v1/{name}/invoke \\\n -H "Content-Type: application/json" \\\n -d '{{"query": "Hello!"}}'\n```\n\n## Files\n\n| File | Purpose |\n|------|---------|\n| `__init__.py` | Agent manifest and entry point |\n| `graph.py` | LangGraph state graph |\n| `nodes.py` | Node processing functions |\n| `config.py` | Agent-specific configuration |\n| `prompts.json` | Versioned prompt templates |\n| `langgraph.json` | LangGraph Studio config |\n"""
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
# --- RAG-specific templates ---
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _rag_nodes_py(name: str) -> str:
|
|
79
|
+
return f'''"""RAG node functions for {name}."""\nfrom __future__ import annotations\n\nfrom typing import Any\n\n\nasync def retrieve(state: dict[str, Any]) -> dict[str, Any]:\n """Retrieve relevant documents."""\n query = state.get("current_query", "")\n # TODO: Replace with real vector search\n docs = [\n {{"content": f"Document about {{query}}", "source": "knowledge_base"}},\n ]\n return {{"citations": docs, "steps_taken": ["retrieved_docs"]}}\n\n\nasync def generate(state: dict[str, Any]) -> dict[str, Any]:\n """Generate response using retrieved context."""\n query = state.get("current_query", "")\n citations = state.get("citations", [])\n context = "\\n".join(d.get("content", "") for d in citations)\n return {{\n "response": f"Based on the knowledge base: Answer to '{{query}}' using context: {{context}}",\n "agent_type": "agent-{name}",\n "steps_taken": ["generated_response"],\n }}\n'''
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _rag_graph_py(name: str) -> str:
|
|
83
|
+
return f'''"""RAG graph for {name}: retrieve -> generate."""\nfrom __future__ import annotations\n\nfrom functools import lru_cache\n\nfrom langgraph.graph import END, StateGraph\n\nfrom agentomatic import BaseAgentState\n\nfrom . import nodes\n\n\ndef build_graph() -> StateGraph:\n g = StateGraph(BaseAgentState)\n g.add_node("retrieve", nodes.retrieve)\n g.add_node("generate", nodes.generate)\n g.set_entry_point("retrieve")\n g.add_edge("retrieve", "generate")\n g.add_edge("generate", END)\n return g\n\n\n@lru_cache(maxsize=1)\ndef get_graph():\n return build_graph().compile()\n'''
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
# --- Chatbot-specific templates ---
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _chatbot_nodes_py(name: str) -> str:
|
|
90
|
+
return f'''"""Chatbot node functions for {name} with conversation memory."""\nfrom __future__ import annotations\n\nfrom typing import Any\n\n\nasync def respond(state: dict[str, Any]) -> dict[str, Any]:\n """Generate a conversational response."""\n query = state.get("current_query", "")\n messages = state.get("messages", [])\n history_len = len(messages)\n\n # TODO: Replace with real LLM call\n return {{\n "response": f"[Turn {{history_len + 1}}] You said: {{query}}",\n "agent_type": "agent-{name}",\n "suggestions": ["Tell me more", "Change topic", "Goodbye"],\n }}\n'''
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _chatbot_graph_py(name: str) -> str:
|
|
94
|
+
return f'''"""Chatbot graph for {name} with memory."""\nfrom __future__ import annotations\n\nfrom functools import lru_cache\n\nfrom langgraph.graph import END, StateGraph\n\nfrom agentomatic import BaseAgentState\n\nfrom . import nodes\n\n\ndef build_graph() -> StateGraph:\n g = StateGraph(BaseAgentState)\n g.add_node("respond", nodes.respond)\n g.set_entry_point("respond")\n g.add_edge("respond", END)\n return g\n\n\n@lru_cache(maxsize=1)\ndef get_graph():\n return build_graph().compile()\n'''
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
# --- Custom (no LangGraph) template ---
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _custom_init_py(name: str, description: str, keywords: str) -> str:
|
|
101
|
+
return f'''"""Agent: {name} (framework-agnostic)."""\nfrom __future__ import annotations\n\nfrom typing import Any\n\nfrom agentomatic import AgentManifest\n\nmanifest = AgentManifest(\n name="{name}",\n slug="agent-{name}",\n description="{description}",\n intent_keywords=[{keywords}],\n framework="custom",\n)\n\n\nasync def node_fn(state: dict[str, Any]) -> dict[str, Any]:\n """Process the request directly — no graph framework needed."""\n query = state.get("current_query", "")\n return {{\n "response": f"Hello from {name}! You asked: {{query}}",\n "agent_type": "agent-{name}",\n }}\n'''
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
# =====================================================================
|
|
105
|
+
# Template Registry
|
|
106
|
+
# =====================================================================
|
|
107
|
+
|
|
108
|
+
TEMPLATES: dict[str, str] = {
|
|
109
|
+
"basic": "Minimal agent — 3 files, quick start",
|
|
110
|
+
"full": "All overwrite files — config, schemas, api, tools, prompts",
|
|
111
|
+
"rag": "RAG agent — retrieve → generate pipeline",
|
|
112
|
+
"chatbot": "Conversational agent with memory",
|
|
113
|
+
"custom": "Framework-agnostic — no LangGraph dependency",
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def get_template_files(template: str, name: str) -> dict[str, str]:
|
|
118
|
+
"""Get all files for a given template.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
template: Template name (basic, full, rag, chatbot, custom).
|
|
122
|
+
name: Agent name.
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
Dict mapping relative file paths to content strings.
|
|
126
|
+
"""
|
|
127
|
+
title = name.replace("_", " ").title()
|
|
128
|
+
description = f"{title} agent"
|
|
129
|
+
keywords = f'"{name}"'
|
|
130
|
+
|
|
131
|
+
common = {
|
|
132
|
+
"prompts.json": _prompts_json(),
|
|
133
|
+
"langgraph.json": _langgraph_json(),
|
|
134
|
+
".env.example": _env_example(name),
|
|
135
|
+
"README.md": _readme_md(name, template),
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if template == "basic":
|
|
139
|
+
return {
|
|
140
|
+
"__init__.py": _init_py(name, description, keywords),
|
|
141
|
+
"graph.py": _graph_py(name),
|
|
142
|
+
"nodes.py": _nodes_py(name),
|
|
143
|
+
**common,
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
elif template == "full":
|
|
147
|
+
return {
|
|
148
|
+
"__init__.py": _init_py(name, description, keywords),
|
|
149
|
+
"graph.py": _graph_py(name),
|
|
150
|
+
"nodes.py": _nodes_py(name),
|
|
151
|
+
"config.py": _config_py(name),
|
|
152
|
+
"schemas.py": _schemas_py(name),
|
|
153
|
+
"tools.py": _tools_py(name),
|
|
154
|
+
"api.py": _api_py(name),
|
|
155
|
+
**common,
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
elif template == "rag":
|
|
159
|
+
return {
|
|
160
|
+
"__init__.py": _init_py(
|
|
161
|
+
name, f"{title} RAG agent", f'"{name}", "search", "knowledge"'
|
|
162
|
+
),
|
|
163
|
+
"graph.py": _rag_graph_py(name),
|
|
164
|
+
"nodes.py": _rag_nodes_py(name),
|
|
165
|
+
"config.py": _config_py(name),
|
|
166
|
+
"tools.py": _tools_py(name),
|
|
167
|
+
**common,
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
elif template == "chatbot":
|
|
171
|
+
return {
|
|
172
|
+
"__init__.py": _init_py(name, f"{title} chatbot", f'"{name}", "chat", "conversation"'),
|
|
173
|
+
"graph.py": _chatbot_graph_py(name),
|
|
174
|
+
"nodes.py": _chatbot_nodes_py(name),
|
|
175
|
+
"config.py": _config_py(name),
|
|
176
|
+
**common,
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
elif template == "custom":
|
|
180
|
+
return {
|
|
181
|
+
"__init__.py": _custom_init_py(name, description, keywords),
|
|
182
|
+
".env.example": _env_example(name),
|
|
183
|
+
"README.md": _readme_md(name, template),
|
|
184
|
+
"prompts.json": _prompts_json(),
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
else:
|
|
188
|
+
raise ValueError(f"Unknown template: {template}. Choose from: {list(TEMPLATES.keys())}")
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""Default configuration values."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
DEFAULT_API_PREFIX = "/api/v1"
|
|
6
|
+
DEFAULT_LOG_LEVEL = "INFO"
|
|
7
|
+
DEFAULT_LLM_PROVIDER = "ollama"
|
|
8
|
+
DEFAULT_LLM_MODEL = "mistral:7b"
|
|
9
|
+
DEFAULT_TEMPERATURE = 0.1
|
|
10
|
+
DEFAULT_MAX_TOKENS = 4096
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""Platform settings with feature flags and nested configuration."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class LLMSettings(BaseModel):
|
|
10
|
+
"""LLM provider configuration."""
|
|
11
|
+
|
|
12
|
+
provider: str = Field("ollama", description="LLM provider: ollama|azure|openai|vertex|dummy")
|
|
13
|
+
model: str = Field("mistral:7b", description="Model name")
|
|
14
|
+
temperature: float = Field(0.1, ge=0.0, le=2.0)
|
|
15
|
+
max_tokens: int = Field(4096, ge=1)
|
|
16
|
+
ollama_base_url: str = Field("http://localhost:11434")
|
|
17
|
+
azure_api_key: str = Field("", description="Azure OpenAI API key")
|
|
18
|
+
azure_api_base: str = Field("")
|
|
19
|
+
azure_api_version: str = Field("2024-02-15-preview")
|
|
20
|
+
azure_deployment_name: str = Field("")
|
|
21
|
+
openai_api_key: str = Field("")
|
|
22
|
+
vertex_project: str = Field("")
|
|
23
|
+
vertex_location: str = Field("us-central1")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class EmbeddingSettings(BaseModel):
|
|
27
|
+
"""Embedding provider configuration."""
|
|
28
|
+
|
|
29
|
+
provider: str = Field("dummy", description="Embedding provider: ollama|dummy")
|
|
30
|
+
model: str = Field("nomic-embed-text")
|
|
31
|
+
dimension: int = Field(768)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class DatabaseSettings(BaseModel):
|
|
35
|
+
"""Database configuration."""
|
|
36
|
+
|
|
37
|
+
url: str = Field("sqlite+aiosqlite:///data/platform.db")
|
|
38
|
+
pool_size: int = Field(10, ge=1)
|
|
39
|
+
max_overflow: int = Field(20, ge=0)
|
|
40
|
+
pool_timeout: int = Field(30, ge=1)
|
|
41
|
+
echo: bool = Field(False)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class FeatureSettings(BaseModel):
|
|
45
|
+
"""Feature flags for the platform."""
|
|
46
|
+
|
|
47
|
+
enable_streaming: bool = Field(True, description="Enable SSE streaming endpoints")
|
|
48
|
+
enable_a2a: bool = Field(True, description="Enable A2A protocol")
|
|
49
|
+
enable_metrics: bool = Field(True, description="Enable Prometheus metrics")
|
|
50
|
+
enable_rate_limit: bool = Field(False, description="Enable rate limiting")
|
|
51
|
+
enable_auth: bool = Field(False, description="Enable API key authentication")
|
|
52
|
+
enable_db: bool = Field(False, description="Enable database storage")
|
|
53
|
+
enable_feedback: bool = Field(True, description="Enable feedback collection")
|
|
54
|
+
max_concurrent_agents: int = Field(10, ge=1)
|
|
55
|
+
request_timeout: float = Field(30.0, gt=0)
|
|
56
|
+
llm_retry_count: int = Field(3, ge=0)
|
|
57
|
+
llm_retry_delay: float = Field(1.0, ge=0)
|
|
58
|
+
circuit_breaker_threshold: int = Field(5, ge=1)
|
|
59
|
+
circuit_breaker_timeout: float = Field(60.0, gt=0)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class AuthSettings(BaseModel):
|
|
63
|
+
"""Authentication configuration."""
|
|
64
|
+
|
|
65
|
+
api_key: str = Field("", description="API key for authentication")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class RateLimitSettings(BaseModel):
|
|
69
|
+
"""Rate limiting configuration."""
|
|
70
|
+
|
|
71
|
+
requests: int = Field(100, ge=1)
|
|
72
|
+
window_seconds: int = Field(60, ge=1)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class PlatformSettings(BaseSettings):
|
|
76
|
+
"""Root platform configuration.
|
|
77
|
+
|
|
78
|
+
Priority: env vars > .env > YAML > defaults.
|
|
79
|
+
Nested delimiter: __ (double underscore).
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
app_name: str = Field("Agentomatic Platform")
|
|
83
|
+
app_env: str = Field("development")
|
|
84
|
+
log_level: str = Field("INFO")
|
|
85
|
+
api_version: str = Field("v1")
|
|
86
|
+
|
|
87
|
+
llm: LLMSettings = Field(default_factory=LLMSettings) # type: ignore[arg-type]
|
|
88
|
+
embedding: EmbeddingSettings = Field(default_factory=EmbeddingSettings) # type: ignore[arg-type]
|
|
89
|
+
db: DatabaseSettings = Field(default_factory=DatabaseSettings) # type: ignore[arg-type]
|
|
90
|
+
features: FeatureSettings = Field(default_factory=FeatureSettings) # type: ignore[arg-type]
|
|
91
|
+
auth: AuthSettings = Field(default_factory=AuthSettings) # type: ignore[arg-type]
|
|
92
|
+
rate_limit: RateLimitSettings = Field(default_factory=RateLimitSettings) # type: ignore[arg-type]
|
|
93
|
+
|
|
94
|
+
model_config = SettingsConfigDict(
|
|
95
|
+
env_prefix="",
|
|
96
|
+
env_nested_delimiter="__",
|
|
97
|
+
env_file=".env",
|
|
98
|
+
env_file_encoding="utf-8",
|
|
99
|
+
extra="ignore",
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
_settings: PlatformSettings | None = None
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def get_settings() -> PlatformSettings:
|
|
107
|
+
"""Get or create the singleton settings instance."""
|
|
108
|
+
global _settings
|
|
109
|
+
if _settings is None:
|
|
110
|
+
_settings = PlatformSettings() # type: ignore[call-arg]
|
|
111
|
+
return _settings
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def reset_settings() -> None:
|
|
115
|
+
"""Reset settings singleton (for testing)."""
|
|
116
|
+
global _settings
|
|
117
|
+
_settings = None
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Agentomatic core — platform, registry, manifest, state."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from .lifespan import configure_logging, create_lifespan
|
|
6
|
+
from .manifest import AgentManifest, RegisteredAgent
|
|
7
|
+
from .platform import AgentPlatform
|
|
8
|
+
from .registry import AgentRegistry
|
|
9
|
+
from .router_factory import (
|
|
10
|
+
A2ATaskRequest,
|
|
11
|
+
AgentChatRequest,
|
|
12
|
+
AgentInvokeRequest,
|
|
13
|
+
AgentInvokeResponse,
|
|
14
|
+
create_default_router,
|
|
15
|
+
)
|
|
16
|
+
from .state import BaseAgentState
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"A2ATaskRequest",
|
|
20
|
+
"AgentChatRequest",
|
|
21
|
+
"AgentInvokeRequest",
|
|
22
|
+
"AgentInvokeResponse",
|
|
23
|
+
"AgentManifest",
|
|
24
|
+
"AgentPlatform",
|
|
25
|
+
"AgentRegistry",
|
|
26
|
+
"BaseAgentState",
|
|
27
|
+
"RegisteredAgent",
|
|
28
|
+
"configure_logging",
|
|
29
|
+
"create_default_router",
|
|
30
|
+
"create_lifespan",
|
|
31
|
+
]
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""Application lifespan management."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from collections.abc import AsyncIterator, Callable
|
|
7
|
+
from contextlib import asynccontextmanager
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import TYPE_CHECKING, Any
|
|
10
|
+
|
|
11
|
+
from loguru import logger
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from fastapi import FastAPI
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def configure_logging(level: str = "INFO") -> None:
|
|
18
|
+
"""Configure loguru with a standard format.
|
|
19
|
+
|
|
20
|
+
Removes all existing handlers and installs a single ``stdout``
|
|
21
|
+
sink with coloured, structured output.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
level: Minimum log level (e.g. ``"DEBUG"``, ``"INFO"``).
|
|
25
|
+
"""
|
|
26
|
+
logger.remove()
|
|
27
|
+
logger.add(
|
|
28
|
+
sys.stdout,
|
|
29
|
+
level=level.upper(),
|
|
30
|
+
format=(
|
|
31
|
+
"<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> | "
|
|
32
|
+
"<level>{level: <8}</level> | "
|
|
33
|
+
"<cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> — "
|
|
34
|
+
"<level>{message}</level>"
|
|
35
|
+
),
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@asynccontextmanager
|
|
40
|
+
async def create_lifespan(
|
|
41
|
+
registry: Any,
|
|
42
|
+
agents_dir: str | Path,
|
|
43
|
+
package_prefix: str,
|
|
44
|
+
settings: Any,
|
|
45
|
+
on_startup: list[Callable[..., Any]] | None = None,
|
|
46
|
+
on_shutdown: list[Callable[..., Any]] | None = None,
|
|
47
|
+
) -> AsyncIterator[Callable[[FastAPI], Any]]:
|
|
48
|
+
"""Create a FastAPI lifespan context manager.
|
|
49
|
+
|
|
50
|
+
Startup sequence:
|
|
51
|
+
1. Configure logging
|
|
52
|
+
2. Discover agents
|
|
53
|
+
3. Run custom startup hooks
|
|
54
|
+
|
|
55
|
+
Shutdown sequence:
|
|
56
|
+
1. Run custom shutdown hooks
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
registry: The :class:`AgentRegistry` instance.
|
|
60
|
+
agents_dir: Path to the agents directory.
|
|
61
|
+
package_prefix: Python package prefix for agent imports.
|
|
62
|
+
settings: Application settings object.
|
|
63
|
+
on_startup: Optional list of callables to run at startup.
|
|
64
|
+
on_shutdown: Optional list of callables to run at shutdown.
|
|
65
|
+
|
|
66
|
+
Yields:
|
|
67
|
+
A lifespan callable suitable for :class:`~fastapi.FastAPI`.
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
async def _lifespan(app: FastAPI) -> AsyncIterator[None]:
|
|
71
|
+
# --- Startup ---
|
|
72
|
+
configure_logging(getattr(settings, "log_level", "INFO"))
|
|
73
|
+
logger.info("🚀 Agentomatic platform starting...")
|
|
74
|
+
|
|
75
|
+
# Discover agents
|
|
76
|
+
agents_path = Path(agents_dir).resolve()
|
|
77
|
+
if agents_path.parent not in [Path(p) for p in sys.path]:
|
|
78
|
+
sys.path.insert(0, str(agents_path.parent))
|
|
79
|
+
|
|
80
|
+
registry.discover(agents_path, package_prefix)
|
|
81
|
+
logger.info(f"📦 {registry.count} agent(s) ready")
|
|
82
|
+
|
|
83
|
+
# Custom startup hooks
|
|
84
|
+
if on_startup:
|
|
85
|
+
for hook in on_startup:
|
|
86
|
+
if callable(hook):
|
|
87
|
+
result = hook()
|
|
88
|
+
if hasattr(result, "__await__"):
|
|
89
|
+
await result
|
|
90
|
+
|
|
91
|
+
yield
|
|
92
|
+
|
|
93
|
+
# --- Shutdown ---
|
|
94
|
+
logger.info("🛑 Agentomatic platform shutting down...")
|
|
95
|
+
if on_shutdown:
|
|
96
|
+
for hook in on_shutdown:
|
|
97
|
+
if callable(hook):
|
|
98
|
+
result = hook()
|
|
99
|
+
if hasattr(result, "__await__"):
|
|
100
|
+
await result
|
|
101
|
+
|
|
102
|
+
yield _lifespan
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""Agent manifest and registered agent types."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Awaitable, Callable
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from typing import TYPE_CHECKING, Any
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from fastapi import APIRouter
|
|
11
|
+
|
|
12
|
+
from agentomatic.prompts.manager import PromptManager
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True, slots=True)
|
|
16
|
+
class AgentManifest:
|
|
17
|
+
"""Identity card for an agent plugin.
|
|
18
|
+
|
|
19
|
+
Every agent must export a ``manifest`` instance in its ``__init__.py``.
|
|
20
|
+
The registry discovers this automatically.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
name: Short machine name (must match folder name).
|
|
24
|
+
slug: Full unique identifier (e.g. 'my-platform-agent-holidays').
|
|
25
|
+
description: Human-readable description.
|
|
26
|
+
intent_keywords: Keywords for orchestrator intent routing.
|
|
27
|
+
version: SemVer version string.
|
|
28
|
+
is_subagent: Whether this agent is routable by an orchestrator.
|
|
29
|
+
framework: Agent framework type ('langgraph', 'langchain', 'custom').
|
|
30
|
+
metadata: Arbitrary metadata (used in A2A agent cards).
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
name: str
|
|
34
|
+
slug: str
|
|
35
|
+
description: str = ""
|
|
36
|
+
intent_keywords: list[str] = field(default_factory=list)
|
|
37
|
+
version: str = "1.0.0"
|
|
38
|
+
is_subagent: bool = True
|
|
39
|
+
framework: str = "langgraph" # 'langgraph' | 'langchain' | 'custom'
|
|
40
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass(slots=True)
|
|
44
|
+
class RegisteredAgent:
|
|
45
|
+
"""An agent that has been discovered and registered by the platform.
|
|
46
|
+
|
|
47
|
+
Contains the manifest, callable functions, and optional enhancements
|
|
48
|
+
(router, config, prompt manager) discovered from the agent's folder.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
manifest: AgentManifest
|
|
52
|
+
node_fn: Callable[..., Awaitable[Any]] | None = None
|
|
53
|
+
graph_fn: Callable[[], Any] | None = None
|
|
54
|
+
module_path: str = ""
|
|
55
|
+
|
|
56
|
+
# Optional enhancements (populated during discovery)
|
|
57
|
+
router: APIRouter | None = None
|
|
58
|
+
config: Any = None
|
|
59
|
+
prompt_manager: PromptManager | None = None
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def name(self) -> str:
|
|
63
|
+
"""Return the agent's short machine name."""
|
|
64
|
+
return self.manifest.name
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def slug(self) -> str:
|
|
68
|
+
"""Return the agent's full unique identifier."""
|
|
69
|
+
return self.manifest.slug
|
|
70
|
+
|
|
71
|
+
async def health_check(self) -> dict[str, Any]:
|
|
72
|
+
"""Check agent health."""
|
|
73
|
+
result: dict[str, Any] = {
|
|
74
|
+
"agent": self.name,
|
|
75
|
+
"slug": self.slug,
|
|
76
|
+
"version": self.manifest.version,
|
|
77
|
+
"framework": self.manifest.framework,
|
|
78
|
+
}
|
|
79
|
+
# Check node function
|
|
80
|
+
result["node_fn_ready"] = self.node_fn is not None
|
|
81
|
+
# Check graph
|
|
82
|
+
if self.graph_fn:
|
|
83
|
+
try:
|
|
84
|
+
graph = self.graph_fn()
|
|
85
|
+
result["graph_ready"] = graph is not None
|
|
86
|
+
except Exception as exc:
|
|
87
|
+
result["graph_ready"] = False
|
|
88
|
+
result["graph_error"] = str(exc)
|
|
89
|
+
else:
|
|
90
|
+
result["graph_ready"] = False
|
|
91
|
+
# Check prompts
|
|
92
|
+
if self.prompt_manager:
|
|
93
|
+
result["prompt_versions"] = self.prompt_manager.list_versions()
|
|
94
|
+
# Check config
|
|
95
|
+
result["has_config"] = self.config is not None
|
|
96
|
+
# Overall status
|
|
97
|
+
result["status"] = (
|
|
98
|
+
"healthy" if result.get("node_fn_ready") or result.get("graph_ready") else "degraded"
|
|
99
|
+
)
|
|
100
|
+
return result
|