pico-agent 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.
pico_agent/__init__.py ADDED
@@ -0,0 +1,31 @@
1
+ from .config import AgentConfig, AgentType, AgentCapability, LLMConfig
2
+ from .decorators import agent
3
+ from .registry import AgentConfigService, ToolRegistry
4
+ from .interfaces import CentralConfigClient, LLMFactory, LLM
5
+ from .scanner import AgentScanner
6
+ from .validation import AgentValidator, ValidationReport, ValidationIssue, Severity
7
+ from .exceptions import AgentError, AgentDisabledError, AgentConfigurationError
8
+
9
+ PICO_SCANNERS = [AgentScanner()]
10
+
11
+ __all__ = [
12
+ "AgentConfig",
13
+ "LLMConfig",
14
+ "AgentType",
15
+ "AgentCapability",
16
+ "agent",
17
+ "AgentConfigService",
18
+ "ToolRegistry",
19
+ "CentralConfigClient",
20
+ "LLMFactory",
21
+ "LLM",
22
+ "AgentScanner",
23
+ "PICO_SCANNERS",
24
+ "AgentValidator",
25
+ "ValidationReport",
26
+ "ValidationIssue",
27
+ "Severity",
28
+ "AgentError",
29
+ "AgentDisabledError",
30
+ "AgentConfigurationError"
31
+ ]
pico_agent/_version.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = '0.1.0'
pico_agent/config.py ADDED
@@ -0,0 +1,36 @@
1
+ from dataclasses import dataclass, field
2
+ from typing import List, Optional, Dict
3
+ from enum import Enum
4
+
5
+ class AgentType(str, Enum):
6
+ ONE_SHOT = "one_shot"
7
+ REACT = "react"
8
+
9
+ class AgentCapability:
10
+ FAST = "fast"
11
+ SMART = "smart"
12
+ REASONING = "reasoning"
13
+ VISION = "vision"
14
+ CODING = "coding"
15
+
16
+ @dataclass
17
+ class AgentConfig:
18
+ name: str
19
+ system_prompt: str
20
+ user_prompt_template: str
21
+ capability: str = AgentCapability.SMART
22
+ enabled: bool = True
23
+ agent_type: AgentType = AgentType.ONE_SHOT
24
+ max_iterations: int = 5
25
+ tools: List[str] = field(default_factory=list)
26
+ agents: List[str] = field(default_factory=list)
27
+ tags: List[str] = field(default_factory=list)
28
+ tracing_enabled: bool = True
29
+ temperature: float = 0.7
30
+ max_tokens: Optional[int] = None
31
+ llm_profile: Optional[str] = None
32
+
33
+ @dataclass
34
+ class LLMConfig:
35
+ api_keys: Dict[str, str] = field(default_factory=dict)
36
+ base_urls: Dict[str, str] = field(default_factory=dict)
@@ -0,0 +1,42 @@
1
+ from typing import List, Optional, Callable, Type
2
+ from .config import AgentConfig, AgentType, AgentCapability
3
+
4
+ AGENT_META_KEY = "_pico_agent_meta"
5
+ IS_AGENT_INTERFACE = "_pico_is_agent_interface"
6
+
7
+ def agent(
8
+ name: str,
9
+ capability: str = AgentCapability.SMART,
10
+ system_prompt: str = "",
11
+ user_prompt_template: str = "{input}",
12
+ agent_type: AgentType = AgentType.ONE_SHOT,
13
+ max_iterations: int = 5,
14
+ tools: Optional[List[str]] = None,
15
+ agents: Optional[List[str]] = None,
16
+ tags: Optional[List[str]] = None,
17
+ tracing_enabled: bool = True,
18
+ temperature: float = 0.7,
19
+ llm_profile: Optional[str] = None
20
+ ) -> Callable[[Type], Type]:
21
+
22
+ def decorator(cls_or_proto: Type) -> Type:
23
+ default_config = AgentConfig(
24
+ name=name,
25
+ capability=capability,
26
+ system_prompt=system_prompt,
27
+ user_prompt_template=user_prompt_template,
28
+ agent_type=agent_type,
29
+ max_iterations=max_iterations,
30
+ tools=tools or [],
31
+ agents=agents or [],
32
+ tags=tags or [],
33
+ tracing_enabled=tracing_enabled,
34
+ temperature=temperature,
35
+ llm_profile=llm_profile
36
+ )
37
+
38
+ setattr(cls_or_proto, AGENT_META_KEY, default_config)
39
+ setattr(cls_or_proto, IS_AGENT_INTERFACE, True)
40
+ return cls_or_proto
41
+
42
+ return decorator
@@ -0,0 +1,9 @@
1
+ class AgentError(Exception):
2
+ pass
3
+
4
+ class AgentDisabledError(AgentError):
5
+ def __init__(self, agent_name: str):
6
+ super().__init__(f"Agent '{agent_name}' is disabled via configuration.")
7
+
8
+ class AgentConfigurationError(AgentError):
9
+ pass
pico_agent/factory.py ADDED
@@ -0,0 +1,65 @@
1
+ from typing import Optional, Any
2
+ from pico_ioc import factory, provides, component
3
+ from .interfaces import CentralConfigClient, LLMFactory
4
+ from .config import AgentConfig, LLMConfig
5
+ from .registry import AgentConfigService, ToolRegistry, LocalAgentRegistry
6
+ from .proxy import DynamicAgentProxy
7
+ from .router import ModelRouter
8
+ from .providers import LangChainLLMFactory
9
+
10
+ class NoOpCentralClient(CentralConfigClient):
11
+ def get_agent_config(self, name: str) -> Optional[AgentConfig]: return None
12
+ def upsert_agent_config(self, config: AgentConfig) -> None: pass
13
+
14
+ @factory
15
+ class AgentInfrastructureFactory:
16
+ @provides(CentralConfigClient, scope="singleton")
17
+ def provide_central_config(self) -> CentralConfigClient:
18
+ return NoOpCentralClient()
19
+
20
+ @provides(LLMConfig, scope="singleton")
21
+ def provide_llm_config(self) -> LLMConfig:
22
+ return LLMConfig()
23
+
24
+ @provides(LLMFactory, scope="singleton")
25
+ def provide_llm_factory(self, config: LLMConfig) -> LLMFactory:
26
+ return LangChainLLMFactory(config)
27
+
28
+ @component
29
+ class DynamicProxyFactory:
30
+ def __init__(
31
+ self,
32
+ config_service: AgentConfigService,
33
+ tool_registry: ToolRegistry,
34
+ llm_factory: LLMFactory,
35
+ local_registry: LocalAgentRegistry,
36
+ model_router: ModelRouter
37
+ ):
38
+ self.config_service = config_service
39
+ self.tool_registry = tool_registry
40
+ self.llm_factory = llm_factory
41
+ self.local_registry = local_registry
42
+ self.model_router = model_router
43
+
44
+ def get_agent(self, name: str) -> Optional[Any]:
45
+ protocol = self.local_registry.get_protocol(name)
46
+ if protocol:
47
+ return self.create_proxy(protocol)
48
+ return None
49
+
50
+ def create_proxy(self, protocol: type) -> Any:
51
+ from .decorators import AGENT_META_KEY
52
+ config = getattr(protocol, AGENT_META_KEY)
53
+
54
+ self.local_registry.register_default(config.name, config)
55
+ self.local_registry.register_protocol(config.name, protocol)
56
+
57
+ return DynamicAgentProxy(
58
+ agent_name=config.name,
59
+ protocol_cls=protocol,
60
+ config_service=self.config_service,
61
+ tool_registry=self.tool_registry,
62
+ llm_factory=self.llm_factory,
63
+ model_router=self.model_router,
64
+ agent_resolver=self
65
+ )
@@ -0,0 +1,25 @@
1
+ from typing import Any, Callable
2
+ from pico_ioc import component, MethodInterceptor, MethodCtx
3
+ from .proxy import TracedAgentProxy
4
+ from .decorators import AGENT_META_KEY
5
+
6
+ @component
7
+ class AgentInterceptor(MethodInterceptor):
8
+ def __init__(self, proxy: TracedAgentProxy):
9
+ self.proxy = proxy
10
+
11
+ def invoke(self, ctx: MethodCtx, call_next: Callable[[MethodCtx], Any]) -> Any:
12
+ config = getattr(ctx.cls, AGENT_META_KEY, None)
13
+
14
+ if not config or ctx.name != "invoke":
15
+ return call_next(ctx)
16
+
17
+ user_input = ""
18
+ if ctx.args:
19
+ user_input = str(ctx.args[0])
20
+ elif "input" in ctx.kwargs:
21
+ user_input = str(ctx.kwargs["input"])
22
+ elif "message" in ctx.kwargs:
23
+ user_input = str(ctx.kwargs["message"])
24
+
25
+ return self.proxy.execute_agent(config.name, user_input)
@@ -0,0 +1,14 @@
1
+ from typing import Protocol, Any, List, Dict, Optional, Type
2
+ from .config import AgentConfig
3
+
4
+ class CentralConfigClient(Protocol):
5
+ def get_agent_config(self, name: str) -> Optional[AgentConfig]: ...
6
+ def upsert_agent_config(self, config: AgentConfig) -> None: ...
7
+
8
+ class LLM(Protocol):
9
+ def invoke(self, messages: List[Dict[str, str]], tools: List[Any]) -> str: ...
10
+ def invoke_structured(self, messages: List[Dict[str, str]], tools: List[Any], output_schema: Type[Any]) -> Any: ...
11
+ def invoke_agent_loop(self, messages: List[Dict[str, str]], tools: List[Any], max_iterations: int, output_schema: Optional[Type[Any]] = None) -> Any: ...
12
+
13
+ class LLMFactory(Protocol):
14
+ def create(self, model_name: str, temperature: float, max_tokens: Optional[int], llm_profile: Optional[str] = None) -> LLM: ...
@@ -0,0 +1,210 @@
1
+ import os
2
+ from typing import List, Dict, Any, Type, Optional
3
+ from langchain_core.messages import SystemMessage, HumanMessage, BaseMessage, AIMessage
4
+ from langchain_core.language_models.chat_models import BaseChatModel
5
+ from .interfaces import LLM, LLMFactory
6
+ from .config import LLMConfig
7
+
8
+ class LangChainAdapter(LLM):
9
+ def __init__(self, chat_model: BaseChatModel):
10
+ self.model = chat_model
11
+
12
+ def _convert_messages(self, messages: List[Dict[str, str]]) -> List[BaseMessage]:
13
+ lc_messages = []
14
+ for msg in messages:
15
+ if msg["role"] == "system":
16
+ lc_messages.append(SystemMessage(content=msg["content"]))
17
+ elif msg["role"] == "user":
18
+ lc_messages.append(HumanMessage(content=msg["content"]))
19
+ elif msg["role"] == "assistant":
20
+ lc_messages.append(AIMessage(content=msg["content"]))
21
+ return lc_messages
22
+
23
+ def invoke(self, messages: List[Dict[str, str]], tools: List[Any]) -> str:
24
+ lc_messages = self._convert_messages(messages)
25
+ model_with_tools = self.model
26
+
27
+ if tools:
28
+ model_with_tools = self.model.bind_tools(tools)
29
+
30
+ response = model_with_tools.invoke(lc_messages)
31
+ return str(response.content)
32
+
33
+ def invoke_structured(
34
+ self,
35
+ messages: List[Dict[str, str]],
36
+ tools: List[Any],
37
+ output_schema: Type[Any]
38
+ ) -> Any:
39
+ lc_messages = self._convert_messages(messages)
40
+ structured_model = self.model.with_structured_output(output_schema)
41
+ return structured_model.invoke(lc_messages)
42
+
43
+ def invoke_agent_loop(
44
+ self,
45
+ messages: List[Dict[str, str]],
46
+ tools: List[Any],
47
+ max_iterations: int,
48
+ output_schema: Optional[Type[Any]] = None
49
+ ) -> Any:
50
+ from langgraph.prebuilt import create_react_agent
51
+
52
+ lc_messages = self._convert_messages(messages)
53
+ agent_executor = create_react_agent(self.model, tools=tools)
54
+
55
+ inputs = {"messages": lc_messages}
56
+ result = agent_executor.invoke(inputs, config={"recursion_limit": max_iterations})
57
+
58
+ final_message = result["messages"][-1]
59
+
60
+ if output_schema:
61
+ structured_model = self.model.with_structured_output(output_schema)
62
+ return structured_model.invoke([HumanMessage(content=str(final_message.content))])
63
+
64
+ return str(final_message.content)
65
+
66
+ class LangChainLLMFactory(LLMFactory):
67
+ def __init__(self, config: LLMConfig):
68
+ self.config = config
69
+
70
+ def create(self, model_name: str, temperature: float, max_tokens: Optional[int], llm_profile: Optional[str] = None) -> LLM:
71
+ final_provider = None
72
+ real_model_name = model_name
73
+
74
+ if ":" in model_name:
75
+ parts = model_name.split(":", 1)
76
+ final_provider = parts[0]
77
+ real_model_name = parts[1]
78
+
79
+ if not final_provider:
80
+ final_provider = self._detect_provider(real_model_name)
81
+
82
+ chat_model = self.create_chat_model(final_provider, real_model_name, llm_profile)
83
+
84
+ if temperature is not None:
85
+ try:
86
+ chat_model.temperature = temperature
87
+ except AttributeError:
88
+ pass
89
+
90
+ if max_tokens is not None:
91
+ try:
92
+ chat_model.max_tokens = max_tokens
93
+ except AttributeError:
94
+ pass
95
+
96
+ return LangChainAdapter(chat_model)
97
+
98
+ def _get_api_key(self, provider: str, profile: Optional[str]) -> Optional[str]:
99
+ if profile and profile in self.config.api_keys:
100
+ return self.config.api_keys[profile]
101
+ return self.config.api_keys.get(provider)
102
+
103
+ def _get_base_url(self, provider: str, default: Optional[str], profile: Optional[str]) -> Optional[str]:
104
+ if profile and profile in self.config.base_urls:
105
+ return self.config.base_urls[profile]
106
+ return self.config.base_urls.get(provider, default)
107
+
108
+ def _detect_provider(self, model_name: str) -> str:
109
+ name_lower = model_name.lower()
110
+ if "gemini" in name_lower: return "gemini"
111
+ elif "claude" in name_lower or "anthropic" in name_lower: return "claude"
112
+ elif "deepseek" in name_lower: return "deepseek"
113
+ elif "qwen" in name_lower: return "qwen"
114
+ elif "azure" in name_lower: return "azure"
115
+ return "openai"
116
+
117
+ def create_chat_model(self, provider: str, model_name: str, profile: Optional[str]) -> BaseChatModel:
118
+ provider_lower = provider.lower()
119
+ timeout = 60
120
+
121
+ def require_key(p_name, key_val):
122
+ if not key_val:
123
+ raise ValueError(
124
+ f"API Key not found for provider '{p_name}' (Profile: '{profile}'). "
125
+ "Please configure it via LLMConfig."
126
+ )
127
+ return key_val
128
+
129
+ if provider_lower == "openai":
130
+ try:
131
+ from langchain_openai import ChatOpenAI
132
+ api_key = require_key("openai", self._get_api_key("openai", profile))
133
+ return ChatOpenAI(model=model_name, api_key=api_key, request_timeout=timeout)
134
+ except ImportError:
135
+ raise ImportError("Please install 'pico-agent[openai]' to use this provider.")
136
+
137
+ elif provider_lower == "azure":
138
+ try:
139
+ from langchain_openai import AzureChatOpenAI
140
+ import os
141
+ api_key = require_key("azure", self._get_api_key("azure", profile))
142
+ return AzureChatOpenAI(
143
+ azure_deployment=model_name,
144
+ openai_api_version=os.getenv("AZURE_OPENAI_API_VERSION"),
145
+ api_key=api_key,
146
+ request_timeout=timeout,
147
+ )
148
+ except ImportError:
149
+ raise ImportError("Please install 'pico-agent[openai]' to use Azure OpenAI.")
150
+
151
+ elif provider_lower == "gemini" or provider_lower == "google":
152
+ try:
153
+ from langchain_google_genai import ChatGoogleGenerativeAI
154
+ api_key = require_key("google", self._get_api_key("google", profile))
155
+ return ChatGoogleGenerativeAI(
156
+ model=model_name,
157
+ google_api_key=api_key,
158
+ temperature=0.0,
159
+ request_timeout=timeout,
160
+ )
161
+ except ImportError:
162
+ raise ImportError("Please install 'pico-agent[google]' to use Gemini.")
163
+
164
+ elif provider_lower == "claude" or provider_lower == "anthropic":
165
+ try:
166
+ from langchain_anthropic import ChatAnthropic
167
+ api_key = require_key("anthropic", self._get_api_key("anthropic", profile))
168
+ base_url = self._get_base_url("anthropic", None, profile)
169
+ return ChatAnthropic(
170
+ model=model_name,
171
+ api_key=api_key,
172
+ base_url=base_url,
173
+ temperature=0.0,
174
+ default_request_timeout=timeout
175
+ )
176
+ except ImportError:
177
+ raise ImportError("Please install 'pico-agent[anthropic]' to use Claude.")
178
+
179
+ elif provider_lower == "deepseek":
180
+ try:
181
+ from langchain_openai import ChatOpenAI
182
+ base_url = self._get_base_url("deepseek", "https://api.deepseek.com/v1", profile)
183
+ api_key = require_key("deepseek", self._get_api_key("deepseek", profile))
184
+ return ChatOpenAI(
185
+ model=model_name,
186
+ base_url=base_url,
187
+ api_key=api_key,
188
+ temperature=0.0,
189
+ request_timeout=timeout,
190
+ )
191
+ except ImportError:
192
+ raise ImportError("Please install 'pico-agent[openai]' to use DeepSeek.")
193
+
194
+ elif provider_lower == "qwen":
195
+ try:
196
+ from langchain_openai import ChatOpenAI
197
+ base_url = self._get_base_url("qwen", "https://dashscope.aliyuncs.com/compatible-mode/v1", profile)
198
+ api_key = require_key("qwen", self._get_api_key("qwen", profile))
199
+ return ChatOpenAI(
200
+ model=model_name,
201
+ base_url=base_url,
202
+ api_key=api_key,
203
+ temperature=0.0,
204
+ request_timeout=timeout,
205
+ )
206
+ except ImportError:
207
+ raise ImportError("Please install 'pico-agent[openai]' to use Qwen.")
208
+
209
+ else:
210
+ raise ValueError(f"Unknown LLM Provider: {provider}")
pico_agent/proxy.py ADDED
@@ -0,0 +1,234 @@
1
+ import inspect
2
+ from typing import Any, List, Dict, Type, get_type_hints, Optional
3
+ from pydantic import BaseModel
4
+ from pico_ioc import component
5
+ from .config import AgentConfig, AgentType
6
+ from .registry import AgentConfigService, ToolRegistry
7
+ from .interfaces import LLMFactory
8
+ from .router import ModelRouter
9
+ from .exceptions import AgentDisabledError
10
+ from .tools import AgentAsTool
11
+
12
+ @component
13
+ class TracedAgentProxy:
14
+ def __init__(
15
+ self,
16
+ config_service: AgentConfigService,
17
+ tool_registry: ToolRegistry,
18
+ llm_factory: LLMFactory,
19
+ model_router: ModelRouter
20
+ ):
21
+ self.config_service = config_service
22
+ self.tool_registry = tool_registry
23
+ self.llm_factory = llm_factory
24
+ self.model_router = model_router
25
+
26
+ def execute_agent(self, agent_name: str, user_input: str) -> Any:
27
+ config = self.config_service.get_config(agent_name)
28
+
29
+ if not config.enabled:
30
+ raise AgentDisabledError(agent_name)
31
+
32
+ final_model_name = self.model_router.resolve_model(
33
+ capability=config.capability,
34
+ runtime_override=None
35
+ )
36
+
37
+ llm = self.llm_factory.create(
38
+ model_name=final_model_name,
39
+ temperature=config.temperature,
40
+ max_tokens=config.max_tokens,
41
+ llm_profile=config.llm_profile
42
+ )
43
+
44
+ resolved_tools = []
45
+ for tool_name in config.tools:
46
+ t = self.tool_registry.get_tool(tool_name)
47
+ if t:
48
+ resolved_tools.append(t)
49
+
50
+ dynamic = self.tool_registry.get_dynamic_tools(config.tags)
51
+ for dt in dynamic:
52
+ if dt not in resolved_tools:
53
+ resolved_tools.append(dt)
54
+
55
+ messages = []
56
+ if config.system_prompt:
57
+ messages.append({"role": "system", "content": config.system_prompt})
58
+
59
+ messages.append({"role": "user", "content": user_input})
60
+
61
+ if config.agent_type == AgentType.REACT:
62
+ return llm.invoke_agent_loop(
63
+ messages,
64
+ resolved_tools,
65
+ config.max_iterations
66
+ )
67
+ else:
68
+ return llm.invoke(messages, resolved_tools)
69
+
70
+
71
+ class DynamicAgentProxy:
72
+ def __init__(
73
+ self,
74
+ agent_name: str,
75
+ protocol_cls: Type,
76
+ config_service: AgentConfigService,
77
+ tool_registry: ToolRegistry,
78
+ llm_factory: LLMFactory,
79
+ model_router: ModelRouter,
80
+ agent_resolver: Any = None
81
+ ):
82
+ self.agent_name = agent_name
83
+ self.protocol_cls = protocol_cls
84
+ self.config_service = config_service
85
+ self.tool_registry = tool_registry
86
+ self.llm_factory = llm_factory
87
+ self.model_router = model_router
88
+ self.agent_resolver = agent_resolver
89
+
90
+ def __getattr__(self, name: str) -> Any:
91
+ if name.startswith("_"):
92
+ raise AttributeError(f"'{type(self).__name__}' object has no attribute '{name}'")
93
+
94
+ if not hasattr(self.protocol_cls, name):
95
+ raise AttributeError(f"Agent {self.agent_name} has no method {name}")
96
+
97
+ method_ref = getattr(self.protocol_cls, name)
98
+
99
+ if not callable(method_ref):
100
+ return method_ref
101
+
102
+ method_sig = inspect.signature(method_ref)
103
+
104
+ params = list(method_sig.parameters.values())
105
+ if params and params[0].name == "self":
106
+ new_params = params[1:]
107
+ method_sig = method_sig.replace(parameters=new_params)
108
+
109
+ type_hints = get_type_hints(method_ref)
110
+ return_type = type_hints.get("return", str)
111
+
112
+ def method_wrapper(*args, **kwargs):
113
+ input_context = self._extract_input_context(method_sig, args, kwargs)
114
+ runtime_model = kwargs.pop("model", kwargs.pop("_model", None))
115
+ return self._execute(input_context, return_type, runtime_model)
116
+
117
+ return method_wrapper
118
+
119
+ def _extract_input_context(self, sig: inspect.Signature, args: tuple, kwargs: dict) -> Dict[str, Any]:
120
+ bound = sig.bind(*args, **kwargs)
121
+ bound.apply_defaults()
122
+ context = {}
123
+ for name, val in bound.arguments.items():
124
+ if name in ("model", "_model", "self"):
125
+ continue
126
+ context[name] = str(val)
127
+ return context
128
+
129
+ def _execute(self, input_context: Dict[str, Any], return_type: Type, runtime_model: Optional[str]) -> Any:
130
+ config = self.config_service.get_config(self.agent_name)
131
+
132
+ if not config.enabled:
133
+ raise AgentDisabledError(self.agent_name)
134
+
135
+ final_model_name = self.model_router.resolve_model(
136
+ capability=config.capability,
137
+ runtime_override=runtime_model
138
+ )
139
+
140
+ llm = self.llm_factory.create(
141
+ model_name=final_model_name,
142
+ temperature=config.temperature,
143
+ max_tokens=config.max_tokens,
144
+ llm_profile=config.llm_profile
145
+ )
146
+
147
+ resolved_tools = self._resolve_dependencies(config)
148
+ messages = self._build_messages(config, input_context)
149
+
150
+ target_schema = return_type if self._is_pydantic_model(return_type) else None
151
+
152
+ if config.agent_type == AgentType.REACT:
153
+ return llm.invoke_agent_loop(
154
+ messages,
155
+ resolved_tools,
156
+ config.max_iterations,
157
+ output_schema=target_schema
158
+ )
159
+ else:
160
+ if target_schema:
161
+ return llm.invoke_structured(messages, resolved_tools, target_schema)
162
+ return llm.invoke(messages, resolved_tools)
163
+
164
+ def _resolve_dependencies(self, config: AgentConfig) -> List[Any]:
165
+ final_tools = []
166
+
167
+ for tool_name in config.tools:
168
+ t = self.tool_registry.get_tool(tool_name)
169
+ if t:
170
+ final_tools.append(t)
171
+
172
+ if self.agent_resolver:
173
+ for agent_name in config.agents:
174
+ try:
175
+ child_agent = self.agent_resolver.get_agent(agent_name)
176
+ if child_agent:
177
+ child_config = self.config_service.get_config(agent_name)
178
+ if not child_config.enabled:
179
+ continue
180
+
181
+ method_name = "invoke"
182
+ protocol = child_agent.protocol_cls
183
+ candidates = [
184
+ n for n, m in inspect.getmembers(protocol)
185
+ if not n.startswith("_") and (inspect.isfunction(m) or inspect.ismethod(m))
186
+ ]
187
+
188
+ if "invoke" in candidates:
189
+ method_name = "invoke"
190
+ elif candidates:
191
+ method_name = candidates[0]
192
+
193
+ adapter = AgentAsTool(
194
+ agent_proxy=child_agent,
195
+ method_name=method_name,
196
+ description=f"Delegates to agent: {agent_name}"
197
+ )
198
+ final_tools.append(adapter)
199
+ except Exception:
200
+ pass
201
+
202
+ dynamic = self.tool_registry.get_dynamic_tools(config.tags)
203
+ for dt in dynamic:
204
+ if dt not in final_tools:
205
+ final_tools.append(dt)
206
+
207
+ return final_tools
208
+
209
+ def _build_messages(self, config: AgentConfig, input_context: Dict[str, Any]) -> List[Dict[str, str]]:
210
+ messages = []
211
+ if config.system_prompt:
212
+ try:
213
+ sys_content = config.system_prompt.format(**input_context)
214
+ except KeyError:
215
+ sys_content = config.system_prompt
216
+ messages.append({"role": "system", "content": sys_content})
217
+
218
+ user_content = ""
219
+ if config.user_prompt_template:
220
+ try:
221
+ user_content = config.user_prompt_template.format(**input_context)
222
+ except KeyError:
223
+ user_content = " ".join(input_context.values())
224
+ else:
225
+ user_content = " ".join(input_context.values())
226
+
227
+ messages.append({"role": "user", "content": user_content})
228
+ return messages
229
+
230
+ def _is_pydantic_model(self, cls: Type) -> bool:
231
+ try:
232
+ return issubclass(cls, BaseModel)
233
+ except TypeError:
234
+ return False
pico_agent/registry.py ADDED
@@ -0,0 +1,77 @@
1
+ from typing import Dict, Any, Optional, List
2
+ from pico_ioc import component
3
+ from .config import AgentConfig
4
+ from .interfaces import CentralConfigClient
5
+
6
+ @component
7
+ class ToolRegistry:
8
+ def __init__(self):
9
+ self._tools: Dict[str, Any] = {}
10
+ self._tag_map: Dict[str, List[str]] = {}
11
+
12
+ def register(self, name: str, tool: Any, tags: List[str] = []) -> None:
13
+ self._tools[name] = tool
14
+ for tag in tags:
15
+ if tag not in self._tag_map:
16
+ self._tag_map[tag] = []
17
+ self._tag_map[tag].append(name)
18
+
19
+ def get_tool(self, name: str) -> Optional[Any]:
20
+ return self._tools.get(name)
21
+
22
+ def get_dynamic_tools(self, agent_tags: List[str]) -> List[Any]:
23
+ found_tools = []
24
+ for tag in agent_tags:
25
+ tool_names = self._tag_map.get(tag, [])
26
+ for name in tool_names:
27
+ t = self._tools.get(name)
28
+ if t and t not in found_tools:
29
+ found_tools.append(t)
30
+
31
+ global_names = self._tag_map.get("global", [])
32
+ for name in global_names:
33
+ t = self._tools.get(name)
34
+ if t and t not in found_tools:
35
+ found_tools.append(t)
36
+ return found_tools
37
+
38
+ @component
39
+ class LocalAgentRegistry:
40
+ def __init__(self):
41
+ self._defaults: Dict[str, AgentConfig] = {}
42
+ self._protocols: Dict[str, type] = {}
43
+
44
+ def register_default(self, name: str, config: AgentConfig) -> None:
45
+ self._defaults[name] = config
46
+
47
+ def register_protocol(self, name: str, protocol: type) -> None:
48
+ self._protocols[name] = protocol
49
+
50
+ def get_default(self, name: str) -> Optional[AgentConfig]:
51
+ return self._defaults.get(name)
52
+
53
+ def get_protocol(self, name: str) -> Optional[type]:
54
+ return self._protocols.get(name)
55
+
56
+ @component
57
+ class AgentConfigService:
58
+ def __init__(self, central_client: CentralConfigClient, local_registry: LocalAgentRegistry):
59
+ self.central_client = central_client
60
+ self.local_registry = local_registry
61
+ self.auto_register = True
62
+
63
+ def get_config(self, name: str) -> AgentConfig:
64
+ remote_config = self.central_client.get_agent_config(name)
65
+ if remote_config:
66
+ return remote_config
67
+
68
+ local_config = self.local_registry.get_default(name)
69
+ if not local_config:
70
+ raise ValueError(f"No configuration found for agent: {name}")
71
+
72
+ if self.auto_register:
73
+ try:
74
+ self.central_client.upsert_agent_config(local_config)
75
+ except Exception:
76
+ pass
77
+ return local_config
pico_agent/router.py ADDED
@@ -0,0 +1,23 @@
1
+ from typing import Dict, Optional
2
+ from pico_ioc import component
3
+ from .config import AgentCapability
4
+
5
+ @component(scope="singleton")
6
+ class ModelRouter:
7
+ def __init__(self):
8
+ self._capability_map: Dict[str, str] = {
9
+ AgentCapability.FAST: "gpt-5-mini",
10
+ AgentCapability.SMART: "gpt-5.1",
11
+ AgentCapability.REASONING: "gemini-3-pro",
12
+ AgentCapability.VISION: "gpt-4o",
13
+ AgentCapability.CODING: "claude-3-5-sonnet"
14
+ }
15
+
16
+ def resolve_model(self, capability: str, runtime_override: Optional[str] = None) -> str:
17
+ if runtime_override:
18
+ return runtime_override
19
+
20
+ return self._capability_map.get(capability, "gpt-5.1")
21
+
22
+ def update_mapping(self, capability: str, model: str) -> None:
23
+ self._capability_map[capability] = model
pico_agent/scanner.py ADDED
@@ -0,0 +1,37 @@
1
+ from typing import Any, Optional, Tuple, Callable
2
+ from pico_ioc.factory import DeferredProvider, ProviderMetadata
3
+ from .decorators import IS_AGENT_INTERFACE, AGENT_META_KEY
4
+
5
+ class AgentScanner:
6
+ def should_scan(self, obj: Any) -> bool:
7
+ return isinstance(obj, type) and getattr(obj, IS_AGENT_INTERFACE, False)
8
+
9
+ def scan(self, obj: Any) -> Optional[Tuple[Any, Callable[[], Any], ProviderMetadata]]:
10
+ if not self.should_scan(obj):
11
+ return None
12
+
13
+ config = getattr(obj, AGENT_META_KEY)
14
+
15
+ def builder(container, locator):
16
+ from .factory import DynamicProxyFactory
17
+ factory = container.get(DynamicProxyFactory)
18
+ return factory.create_proxy(obj)
19
+
20
+ provider = DeferredProvider(builder)
21
+
22
+ metadata = ProviderMetadata(
23
+ key=obj,
24
+ provided_type=obj,
25
+ concrete_class=None,
26
+ factory_class=None,
27
+ factory_method=None,
28
+ qualifiers=set(),
29
+ primary=True,
30
+ lazy=True,
31
+ infra="pico_agent",
32
+ pico_name=config.name,
33
+ scope="singleton",
34
+ dependencies=()
35
+ )
36
+
37
+ return (obj, provider, metadata)
pico_agent/tools.py ADDED
@@ -0,0 +1,30 @@
1
+ import inspect
2
+ from typing import Any, Type, get_type_hints
3
+ from pydantic import BaseModel, create_model
4
+
5
+ class AgentAsTool:
6
+ def __init__(self, agent_proxy: Any, method_name: str = "invoke", description: str = ""):
7
+ self.proxy = agent_proxy
8
+ self.method_name = method_name
9
+ self._func = getattr(agent_proxy, method_name)
10
+ self.name = getattr(agent_proxy, "agent_name", "agent_tool")
11
+ self.description = description or f"Agent {self.name}"
12
+ self.args_schema = self._create_schema_from_sig()
13
+
14
+ def _create_schema_from_sig(self) -> Type[BaseModel]:
15
+ protocol_cls = self.proxy.protocol_cls
16
+ real_method = getattr(protocol_cls, self.method_name)
17
+ sig = inspect.signature(real_method)
18
+ type_hints = get_type_hints(real_method)
19
+
20
+ fields = {}
21
+ for param_name, param in sig.parameters.items():
22
+ if param_name == "self": continue
23
+ annotation = type_hints.get(param_name, str)
24
+ default = param.default if param.default is not inspect.Parameter.empty else ...
25
+ fields[param_name] = (annotation, default)
26
+
27
+ return create_model(f"{self.name}Input", **fields)
28
+
29
+ def __call__(self, **kwargs):
30
+ return self._func(**kwargs)
@@ -0,0 +1,43 @@
1
+ from dataclasses import dataclass, field
2
+ from typing import List
3
+ from enum import Enum
4
+ from .config import AgentConfig
5
+
6
+ class Severity(str, Enum):
7
+ WARNING = "warning"
8
+ ERROR = "error"
9
+
10
+ @dataclass
11
+ class ValidationIssue:
12
+ field: str
13
+ message: str
14
+ severity: Severity
15
+
16
+ @dataclass
17
+ class ValidationReport:
18
+ valid: bool
19
+ issues: List[ValidationIssue] = field(default_factory=list)
20
+
21
+ @property
22
+ def has_errors(self) -> bool:
23
+ return any(i.severity == Severity.ERROR for i in self.issues)
24
+
25
+ class AgentValidator:
26
+ def validate(self, config: AgentConfig) -> ValidationReport:
27
+ issues = []
28
+
29
+ if not config.name or len(config.name.strip()) == 0:
30
+ issues.append(ValidationIssue("name", "Agent name cannot be empty", Severity.ERROR))
31
+
32
+ if not config.capability:
33
+ issues.append(ValidationIssue("capability", "Agent capability must be defined", Severity.ERROR))
34
+
35
+ if not (0.0 <= config.temperature <= 2.0):
36
+ issues.append(ValidationIssue("temperature", "Temperature must be between 0.0 and 2.0", Severity.ERROR))
37
+ elif config.temperature > 1.0:
38
+ issues.append(ValidationIssue("temperature", "High temperature (>1.0) may cause hallucinations", Severity.WARNING))
39
+
40
+ if not config.system_prompt:
41
+ issues.append(ValidationIssue("system_prompt", "System prompt is empty", Severity.WARNING))
42
+
43
+ return ValidationReport(valid=not any(i.severity == Severity.ERROR for i in issues), issues=issues)
@@ -0,0 +1,63 @@
1
+ Metadata-Version: 2.4
2
+ Name: pico-agent
3
+ Version: 0.1.0
4
+ Summary: A pico agent framework with LangChain integration.
5
+ Author-email: David Perez Cabrera <dperezcabrera@gmail.com>
6
+ License: MIT License
7
+
8
+ Copyright (c) 2025 David Pérez Cabrera
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Project-URL: Homepage, https://github.com/dperezcabrera/pico-agent
29
+ Project-URL: Repository, https://github.com/dperezcabrera/pico-agent
30
+ Project-URL: Issue Tracker, https://github.com/dperezcabrera/pico-agent/issues
31
+ Keywords: ioc,di,dependency injection,agent,llm,langchain,ai
32
+ Classifier: Development Status :: 4 - Beta
33
+ Classifier: Framework :: AsyncIO
34
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
35
+ Classifier: Programming Language :: Python :: 3
36
+ Classifier: Programming Language :: Python :: 3.10
37
+ Classifier: Programming Language :: Python :: 3.11
38
+ Classifier: Programming Language :: Python :: 3.12
39
+ Classifier: License :: OSI Approved :: MIT License
40
+ Classifier: Operating System :: OS Independent
41
+ Requires-Python: >=3.10
42
+ Description-Content-Type: text/markdown
43
+ License-File: LICENSE
44
+ Requires-Dist: pico-ioc>=2.2.0
45
+ Requires-Dist: pydantic>=2.0
46
+ Requires-Dist: langchain>=1.0.2
47
+ Requires-Dist: langchain-core
48
+ Requires-Dist: langchain-community
49
+ Requires-Dist: langchain-model-profiles
50
+ Provides-Extra: openai
51
+ Requires-Dist: langchain-openai; extra == "openai"
52
+ Provides-Extra: google
53
+ Requires-Dist: langchain-google-genai; extra == "google"
54
+ Provides-Extra: anthropic
55
+ Requires-Dist: langchain-anthropic; extra == "anthropic"
56
+ Provides-Extra: community
57
+ Requires-Dist: langchain-community; extra == "community"
58
+ Provides-Extra: all
59
+ Requires-Dist: langchain-openai; extra == "all"
60
+ Requires-Dist: langchain-google-genai; extra == "all"
61
+ Requires-Dist: langchain-anthropic; extra == "all"
62
+ Requires-Dist: langchain-community; extra == "all"
63
+ Dynamic: license-file
@@ -0,0 +1,21 @@
1
+ pico_agent/__init__.py,sha256=jkQz7FcUSys_qVJTVIhzGy3QT0brZ4mmIKbT5T3YPpQ,855
2
+ pico_agent/_version.py,sha256=IMjkMO3twhQzluVTo8Z6rE7Eg-9U79_LGKMcsWLKBkY,22
3
+ pico_agent/config.py,sha256=pgFd5hSjsm6OOmReNDhCLTf0GpJO5z4zEQSjMgfxc6g,992
4
+ pico_agent/decorators.py,sha256=Lv90ywOcfPrWcc4jQGrg00dWo3l_lv8ZhhwJa66p4ig,1372
5
+ pico_agent/exceptions.py,sha256=dLHWRLw2cwV1Vfd7G5_24X7VWOVoyc0bOl8gH2GdEc8,252
6
+ pico_agent/factory.py,sha256=2bv5mYahUgrxFYHbiS2ME0O2Mi938HBQWtQqypI4Kyo,2353
7
+ pico_agent/interceptor.py,sha256=R3pPVQ5w0eK7woOMecbARYuJsfGycTOgMI_Ne-ntJ3A,861
8
+ pico_agent/interfaces.py,sha256=VMpndZ0xOM9SkwRxE0sodBLw0Q3rKMSreZWhl6wu4Es,815
9
+ pico_agent/providers.py,sha256=ygwmF7BXVb6dMxgS-4zSp-AWZgP36zDdYMO3-_auAsY,8748
10
+ pico_agent/proxy.py,sha256=MvCZOp5VMMUXduHW-nb8NISpwXMS4AHI_PEzGRbC5Uo,8523
11
+ pico_agent/registry.py,sha256=VmX-VlUIMRk_qSjmKWnJ4yjMRxOi60xQAww02hLNmw0,2630
12
+ pico_agent/router.py,sha256=qkq5LpfWtVjxTyARSld8xKhehHvJ9qCx8obi53zVGqc,837
13
+ pico_agent/scanner.py,sha256=oTsMXv0qkS9Utn3D0IFP8oH7cm8XZ6pSWB1GY1Q18Iw,1209
14
+ pico_agent/tools.py,sha256=9N9vLslVK_azoBKPO6tKI0cxigMudBNazjcoiMFGxoU,1278
15
+ pico_agent/validation.py,sha256=u2VykzguXwEoyjANj_GJxAJjoLO0WEwq3JjhAKCjMc4,1530
16
+ pico_agent-0.1.0.dist-info/licenses/LICENSE,sha256=N1_nOvHTM6BobYnOTNXiQkroDqCEi6EzfGBv8lWtyZ0,1077
17
+ pico_agent-0.1.0.dist-info/METADATA,sha256=pFgiMwTtuhiP1C5xvVI04evWM53kfzHi7bvoJhmdJV8,3002
18
+ pico_agent-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
19
+ pico_agent-0.1.0.dist-info/entry_points.txt,sha256=Fs4uuKo-WNNDpU7i9MqxgjTDuZUELcsMjz-O9da4jM8,45
20
+ pico_agent-0.1.0.dist-info/top_level.txt,sha256=0t60mhJrPdkaG6tyfJwHc1FIZ83BjB_Hz_eEPpZNc3E,11
21
+ pico_agent-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [pico_stack.modules]
2
+ pico_agent = pico_agent
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 David Pérez Cabrera
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ pico_agent