aigency 0.0.1__tar.gz
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.
- aigency-0.0.1/PKG-INFO +18 -0
- aigency-0.0.1/README.md +6 -0
- aigency-0.0.1/aigency/__init__.py +0 -0
- aigency-0.0.1/aigency/agents/executor.py +141 -0
- aigency-0.0.1/aigency/agents/generator.py +73 -0
- aigency-0.0.1/aigency/models/config.py +69 -0
- aigency-0.0.1/aigency/models/core.py +28 -0
- aigency-0.0.1/aigency/models/tools.py +39 -0
- aigency-0.0.1/aigency/tools/generator.py +83 -0
- aigency-0.0.1/aigency/utils/config_service.py +68 -0
- aigency-0.0.1/aigency/utils/logger.py +101 -0
- aigency-0.0.1/aigency/utils/singleton.py +12 -0
- aigency-0.0.1/aigency/utils/utils.py +96 -0
- aigency-0.0.1/aigency.egg-info/PKG-INFO +18 -0
- aigency-0.0.1/aigency.egg-info/SOURCES.txt +18 -0
- aigency-0.0.1/aigency.egg-info/dependency_links.txt +1 -0
- aigency-0.0.1/aigency.egg-info/requires.txt +5 -0
- aigency-0.0.1/aigency.egg-info/top_level.txt +1 -0
- aigency-0.0.1/pyproject.toml +20 -0
- aigency-0.0.1/setup.cfg +4 -0
aigency-0.0.1/PKG-INFO
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
Metadata-Version: 2.4
|
2
|
+
Name: aigency
|
3
|
+
Version: 0.0.1
|
4
|
+
Summary: Add your description here
|
5
|
+
Requires-Python: >=3.12
|
6
|
+
Description-Content-Type: text/markdown
|
7
|
+
Requires-Dist: google-adk>=1.11.0
|
8
|
+
Requires-Dist: a2a-sdk==0.3.0
|
9
|
+
Requires-Dist: litellm<1.73.0,>=1.72.6
|
10
|
+
Requires-Dist: pyyaml==6.0.2
|
11
|
+
Requires-Dist: PyJWT==2.10.1
|
12
|
+
|
13
|
+
# aigency-lib
|
14
|
+
|
15
|
+
|
16
|
+
aigency-lib/examples/simple_agents/hello_world_agent
|
17
|
+
|
18
|
+
docker compose up
|
aigency-0.0.1/README.md
ADDED
File without changes
|
@@ -0,0 +1,141 @@
|
|
1
|
+
"""Agent executor module for A2A integration."""
|
2
|
+
|
3
|
+
from a2a.server.agent_execution import AgentExecutor
|
4
|
+
from a2a.server.agent_execution.context import RequestContext
|
5
|
+
from a2a.server.events.event_queue import EventQueue
|
6
|
+
from a2a.server.tasks import TaskUpdater
|
7
|
+
from a2a.types import AgentCard, TaskState, UnsupportedOperationError
|
8
|
+
from a2a.utils.errors import ServerError
|
9
|
+
from google.adk.runners import Runner
|
10
|
+
from google.genai import types
|
11
|
+
|
12
|
+
from aigency.utils.logger import get_logger
|
13
|
+
from aigency.utils.utils import (
|
14
|
+
convert_a2a_part_to_genai,
|
15
|
+
convert_genai_part_to_a2a,
|
16
|
+
)
|
17
|
+
|
18
|
+
logger = get_logger()
|
19
|
+
|
20
|
+
# TODO: This needs to be changed
|
21
|
+
DEFAULT_USER_ID = "self"
|
22
|
+
|
23
|
+
class AgentA2AExecutor(AgentExecutor):
|
24
|
+
"""Agent executor for A2A integration with Google ADK runners."""
|
25
|
+
|
26
|
+
def __init__(self, runner: Runner, card: AgentCard):
|
27
|
+
"""Initialize the BaseAgentA2AExecutor.
|
28
|
+
|
29
|
+
Args:
|
30
|
+
card (AgentCard): The agent card containing metadata about the agent.
|
31
|
+
"""
|
32
|
+
self._card = card
|
33
|
+
# Track active sessions for potential cancellation
|
34
|
+
self._active_sessions: set[str] = set()
|
35
|
+
self.runner = runner
|
36
|
+
|
37
|
+
async def _upsert_session(self, session_id: str) -> "Session":
|
38
|
+
"""Retrieve a session if it exists, otherwise create a new one.
|
39
|
+
|
40
|
+
Ensures that async session service methods are properly awaited.
|
41
|
+
|
42
|
+
Args:
|
43
|
+
session_id (str): The ID of the session to retrieve or create.
|
44
|
+
|
45
|
+
Returns:
|
46
|
+
Session: The retrieved or newly created session object.
|
47
|
+
"""
|
48
|
+
logger.info("session_id: %s", session_id)
|
49
|
+
session = await self.runner.session_service.get_session(
|
50
|
+
app_name=self.runner.app_name,
|
51
|
+
user_id=DEFAULT_USER_ID,
|
52
|
+
session_id=session_id,
|
53
|
+
)
|
54
|
+
if session is None:
|
55
|
+
session = await self.runner.session_service.create_session(
|
56
|
+
app_name=self.runner.app_name,
|
57
|
+
user_id=DEFAULT_USER_ID,
|
58
|
+
session_id=session_id,
|
59
|
+
)
|
60
|
+
return session
|
61
|
+
|
62
|
+
async def _process_request(
|
63
|
+
self,
|
64
|
+
new_message: types.Content,
|
65
|
+
session_id: str,
|
66
|
+
task_updater: TaskUpdater,
|
67
|
+
) -> None:
|
68
|
+
session_obj = await self._upsert_session(session_id)
|
69
|
+
session_id = session_obj.id
|
70
|
+
|
71
|
+
self._active_sessions.add(session_id)
|
72
|
+
|
73
|
+
try:
|
74
|
+
async for event in self.runner.run_async(
|
75
|
+
session_id=session_id,
|
76
|
+
user_id=DEFAULT_USER_ID,
|
77
|
+
new_message=new_message,
|
78
|
+
):
|
79
|
+
if event.is_final_response():
|
80
|
+
parts = []
|
81
|
+
if event.content:
|
82
|
+
parts = [
|
83
|
+
convert_genai_part_to_a2a(part)
|
84
|
+
for part in event.content.parts
|
85
|
+
if (part.text or part.file_data or part.inline_data)
|
86
|
+
]
|
87
|
+
logger.debug("Yielding final response: %s", parts)
|
88
|
+
await task_updater.add_artifact(parts)
|
89
|
+
await task_updater.update_status(TaskState.completed, final=True)
|
90
|
+
break
|
91
|
+
if not event.get_function_calls():
|
92
|
+
logger.debug("Yielding update response")
|
93
|
+
message_parts = []
|
94
|
+
if event.content:
|
95
|
+
message_parts = [
|
96
|
+
convert_genai_part_to_a2a(part)
|
97
|
+
for part in event.content.parts
|
98
|
+
if (part.text)
|
99
|
+
]
|
100
|
+
await task_updater.update_status(
|
101
|
+
TaskState.working,
|
102
|
+
message=task_updater.new_agent_message(message_parts),
|
103
|
+
)
|
104
|
+
else:
|
105
|
+
logger.debug("Skipping event")
|
106
|
+
finally:
|
107
|
+
# Remove from active sessions when done
|
108
|
+
self._active_sessions.discard(session_id)
|
109
|
+
|
110
|
+
async def execute(
|
111
|
+
self,
|
112
|
+
context: RequestContext,
|
113
|
+
event_queue: EventQueue,
|
114
|
+
):
|
115
|
+
# Run the agent until either complete or the task is suspended.
|
116
|
+
updater = TaskUpdater(event_queue, context.task_id, context.context_id)
|
117
|
+
# Immediately notify that the task is submitted.
|
118
|
+
if not context.current_task:
|
119
|
+
await updater.update_status(TaskState.submitted)
|
120
|
+
await updater.update_status(TaskState.working)
|
121
|
+
await self._process_request(
|
122
|
+
types.UserContent(
|
123
|
+
parts=[
|
124
|
+
convert_a2a_part_to_genai(part) for part in context.message.parts
|
125
|
+
],
|
126
|
+
),
|
127
|
+
context.context_id,
|
128
|
+
updater,
|
129
|
+
)
|
130
|
+
|
131
|
+
logger.debug("[ADKAgentA2AExecutor] execute exiting")
|
132
|
+
|
133
|
+
async def cancel(self, context: RequestContext, event_queue: EventQueue):
|
134
|
+
session_id = context.context_id
|
135
|
+
if session_id in self._active_sessions:
|
136
|
+
logger.info("Cancellation requested for active session: %s", session_id)
|
137
|
+
self._active_sessions.discard(session_id)
|
138
|
+
else:
|
139
|
+
logger.debug("Cancellation requested for inactive session: %s", session_id)
|
140
|
+
|
141
|
+
raise ServerError(error=UnsupportedOperationError())
|
@@ -0,0 +1,73 @@
|
|
1
|
+
"""Agent generator module for creating A2A agents."""
|
2
|
+
|
3
|
+
from typing import Any, Dict, List
|
4
|
+
|
5
|
+
from a2a.types import AgentCapabilities, AgentCard, AgentSkill
|
6
|
+
from google.adk.agents import Agent
|
7
|
+
from google.adk.artifacts import InMemoryArtifactService
|
8
|
+
from google.adk.memory.in_memory_memory_service import InMemoryMemoryService
|
9
|
+
from google.adk.runners import Runner
|
10
|
+
from google.adk.sessions import InMemorySessionService
|
11
|
+
|
12
|
+
from aigency.agents.executor import AgentA2AExecutor
|
13
|
+
from aigency.models.config import AgentConfig
|
14
|
+
from aigency.tools.generator import ToolGenerator
|
15
|
+
|
16
|
+
|
17
|
+
class AgentA2AGenerator:
|
18
|
+
"""Generator for creating A2A agents and related components."""
|
19
|
+
|
20
|
+
@staticmethod
|
21
|
+
def create_agent(agent_config: AgentConfig) -> Agent:
|
22
|
+
|
23
|
+
tools = [ToolGenerator.create_tool(tool_cfg) for tool_cfg in agent_config.tools]
|
24
|
+
|
25
|
+
return Agent(
|
26
|
+
name=agent_config.name,
|
27
|
+
model=agent_config.model.name,
|
28
|
+
instruction=agent_config.instruction,
|
29
|
+
tools=tools,
|
30
|
+
)
|
31
|
+
|
32
|
+
@staticmethod
|
33
|
+
def build_agent_card(agent_config: AgentConfig) -> AgentCard:
|
34
|
+
|
35
|
+
# TODO: Parse properly
|
36
|
+
capabilities = AgentCapabilities(streaming=agent_config.capabilities.streaming)
|
37
|
+
|
38
|
+
skills = [
|
39
|
+
AgentSkill(
|
40
|
+
id=skill.id,
|
41
|
+
name=skill.name,
|
42
|
+
description=skill.description,
|
43
|
+
tags=skill.tags,
|
44
|
+
examples=skill.examples,
|
45
|
+
)
|
46
|
+
for skill in agent_config.skills
|
47
|
+
]
|
48
|
+
|
49
|
+
return AgentCard(
|
50
|
+
name=agent_config.name,
|
51
|
+
description=agent_config.description,
|
52
|
+
url=agent_config.url,
|
53
|
+
version=agent_config.version,
|
54
|
+
default_input_modes=agent_config.default_input_modes,
|
55
|
+
default_output_modes=agent_config.default_output_modes,
|
56
|
+
capabilities=capabilities,
|
57
|
+
skills=skills,
|
58
|
+
)
|
59
|
+
|
60
|
+
@staticmethod
|
61
|
+
def build_executor(
|
62
|
+
agent: Agent, agent_card: AgentCard
|
63
|
+
) -> AgentA2AExecutor:
|
64
|
+
|
65
|
+
runner = Runner(
|
66
|
+
app_name=agent.name,
|
67
|
+
agent=agent,
|
68
|
+
artifact_service=InMemoryArtifactService(),
|
69
|
+
session_service=InMemorySessionService(),
|
70
|
+
memory_service=InMemoryMemoryService(),
|
71
|
+
)
|
72
|
+
|
73
|
+
return AgentA2AExecutor(runner=runner, card=agent_card)
|
@@ -0,0 +1,69 @@
|
|
1
|
+
"""Configuration models for agents."""
|
2
|
+
|
3
|
+
from typing import List, Optional, Union
|
4
|
+
|
5
|
+
from pydantic import BaseModel
|
6
|
+
|
7
|
+
from aigency.models.core import Capabilities, ModelConfig, Skill
|
8
|
+
from aigency.models.tools import FunctionTool, McpTool, Tool
|
9
|
+
|
10
|
+
#class SecurityScheme(BaseModel):
|
11
|
+
# """Define un esquema de seguridad individual."""
|
12
|
+
# type: str
|
13
|
+
# description: Optional[str] = None
|
14
|
+
# scheme: str
|
15
|
+
# bearerFormat: Optional[str] = None
|
16
|
+
#
|
17
|
+
#class AuthConfig(BaseModel):
|
18
|
+
# """Configuración de autenticación."""
|
19
|
+
# type: str
|
20
|
+
# securitySchemes: Optional[Dict[str, SecurityScheme]] = None
|
21
|
+
# security: Optional[List[Dict[str, List[str]]]] = None
|
22
|
+
|
23
|
+
# --- Modelos para secciones opcionales ---
|
24
|
+
|
25
|
+
#class MonitoringConfig(BaseModel):
|
26
|
+
# """Configuración de monitorización y observabilidad."""
|
27
|
+
# phoenix_host: Optional[str] = None
|
28
|
+
# phoenix_port: Optional[int] = None
|
29
|
+
|
30
|
+
#class RemoteAgent(BaseModel):
|
31
|
+
# """Configuración para la comunicación con un agente remoto."""
|
32
|
+
# name: str
|
33
|
+
# host: str
|
34
|
+
# port: int
|
35
|
+
#auth: Optional[AuthConfig] = None # Reutilizamos la configuración de Auth
|
36
|
+
|
37
|
+
# --- Clase Principal que une todo ---
|
38
|
+
|
39
|
+
class AgentConfig(BaseModel):
|
40
|
+
"""Root Pydantic model for complete agent configuration."""
|
41
|
+
# Configuración Básica
|
42
|
+
name: str
|
43
|
+
description: str
|
44
|
+
url: str
|
45
|
+
version: str
|
46
|
+
default_input_modes: Optional[List[str]] = None
|
47
|
+
default_output_modes: Optional[List[str]] = None
|
48
|
+
capabilities: Optional[Capabilities] = None
|
49
|
+
|
50
|
+
# Autenticación
|
51
|
+
#auth: Optional[AuthConfig] = None
|
52
|
+
|
53
|
+
# Configuración del Modelo
|
54
|
+
model: ModelConfig
|
55
|
+
|
56
|
+
# Comportamiento
|
57
|
+
instruction: Optional[str] = None
|
58
|
+
skills: Optional[List[Skill]] = None
|
59
|
+
|
60
|
+
# Herramientas
|
61
|
+
tools: Optional[List[Union[FunctionTool, McpTool, Tool]]] = None
|
62
|
+
|
63
|
+
# Comunicación Multi-Agente
|
64
|
+
#remote_agents_addresses: Optional[List[RemoteAgent]] = None
|
65
|
+
|
66
|
+
# Monitorización
|
67
|
+
#monitoring: Optional[MonitoringConfig] = None
|
68
|
+
|
69
|
+
|
@@ -0,0 +1,28 @@
|
|
1
|
+
"""Core models for agent configuration."""
|
2
|
+
|
3
|
+
from typing import Dict, List, Optional
|
4
|
+
|
5
|
+
from pydantic import BaseModel
|
6
|
+
|
7
|
+
|
8
|
+
class ProviderConfig(BaseModel):
|
9
|
+
"""Configuration for AI model provider."""
|
10
|
+
name: str
|
11
|
+
endpoint: Optional[str] = None
|
12
|
+
|
13
|
+
class ModelConfig(BaseModel):
|
14
|
+
"""Configuration for AI model."""
|
15
|
+
name: str
|
16
|
+
provider: Optional[ProviderConfig] = None
|
17
|
+
|
18
|
+
class Capabilities(BaseModel):
|
19
|
+
"""Agent capabilities, such as streaming."""
|
20
|
+
streaming: Optional[bool] = None
|
21
|
+
|
22
|
+
class Skill(BaseModel):
|
23
|
+
"""Define a specific agent skill."""
|
24
|
+
id: str
|
25
|
+
name: str
|
26
|
+
description: str
|
27
|
+
tags: Optional[List[str]] = None
|
28
|
+
examples: Optional[List[str]] = None
|
@@ -0,0 +1,39 @@
|
|
1
|
+
"""Tool models for agent configuration."""
|
2
|
+
|
3
|
+
from enum import Enum
|
4
|
+
from typing import Dict, List, Optional
|
5
|
+
|
6
|
+
from pydantic import BaseModel
|
7
|
+
|
8
|
+
|
9
|
+
class ToolType(str, Enum):
|
10
|
+
"""Enum for tool types."""
|
11
|
+
MCP = "mcp"
|
12
|
+
FUNCTION = "function"
|
13
|
+
|
14
|
+
class Tool(BaseModel):
|
15
|
+
"""Define an external tool that the agent can use."""
|
16
|
+
type: ToolType
|
17
|
+
name: str
|
18
|
+
description: str
|
19
|
+
|
20
|
+
class FunctionTool(Tool):
|
21
|
+
"""Configuration for function-based tools."""
|
22
|
+
module_path: str
|
23
|
+
function_name: str
|
24
|
+
|
25
|
+
class McpTypeStreamable(BaseModel):
|
26
|
+
"""Model for streamable tool type."""
|
27
|
+
url: str
|
28
|
+
port: int
|
29
|
+
path: str = "/"
|
30
|
+
|
31
|
+
class McpTypeStdio(BaseModel):
|
32
|
+
"""Model for stdio tool type."""
|
33
|
+
command: str
|
34
|
+
args: List[str]
|
35
|
+
env: Optional[Dict[str, str]] = None
|
36
|
+
|
37
|
+
class McpTool(Tool):
|
38
|
+
"""Configuration for MCP-based tools."""
|
39
|
+
mcp_config: McpTypeStreamable | McpTypeStdio
|
@@ -0,0 +1,83 @@
|
|
1
|
+
"""Tool Factory for dynamically loading and managing agent tools.
|
2
|
+
|
3
|
+
This module provides a flexible way to load different types of tools based on
|
4
|
+
configuration. It uses the Strategy pattern and Pydantic for validation.
|
5
|
+
"""
|
6
|
+
|
7
|
+
import importlib
|
8
|
+
from typing import Any, Optional
|
9
|
+
|
10
|
+
from google.adk.tools.mcp_tool.mcp_toolset import (
|
11
|
+
MCPToolset,
|
12
|
+
StdioConnectionParams,
|
13
|
+
StdioServerParameters,
|
14
|
+
StreamableHTTPConnectionParams,
|
15
|
+
)
|
16
|
+
|
17
|
+
from aigency.models.tools import (
|
18
|
+
FunctionTool,
|
19
|
+
McpTool,
|
20
|
+
McpTypeStdio,
|
21
|
+
McpTypeStreamable,
|
22
|
+
Tool,
|
23
|
+
ToolType,
|
24
|
+
)
|
25
|
+
from aigency.utils.utils import expand_env_vars
|
26
|
+
|
27
|
+
|
28
|
+
class ToolGenerator:
|
29
|
+
"""Generator for creating tools based on configuration."""
|
30
|
+
|
31
|
+
@staticmethod
|
32
|
+
def load_function_tool(config: FunctionTool) -> Any:
|
33
|
+
"""Load a function tool from configuration."""
|
34
|
+
try:
|
35
|
+
module = importlib.import_module(config.module_path)
|
36
|
+
return getattr(module, config.function_name)
|
37
|
+
except (ImportError, AttributeError) as e:
|
38
|
+
raise ValueError(f"Error loading function tool: {e}")
|
39
|
+
|
40
|
+
@staticmethod
|
41
|
+
def load_mcp_tool(config: McpTool) -> Any:
|
42
|
+
"""Load an MCP tool from configuration."""
|
43
|
+
|
44
|
+
if isinstance(config.mcp_config, McpTypeStreamable):
|
45
|
+
url = f"http://{config.mcp_config.url}:{config.mcp_config.port}{config.mcp_config.path}"
|
46
|
+
return MCPToolset(
|
47
|
+
connection_params=StreamableHTTPConnectionParams(
|
48
|
+
url=url
|
49
|
+
)
|
50
|
+
)
|
51
|
+
elif isinstance(config.mcp_config, McpTypeStdio):
|
52
|
+
command = config.mcp_config.command
|
53
|
+
args = config.mcp_config.args
|
54
|
+
env = expand_env_vars(config.mcp_config.env)
|
55
|
+
|
56
|
+
return MCPToolset(
|
57
|
+
connection_params=StdioConnectionParams(
|
58
|
+
server_params=StdioServerParameters(
|
59
|
+
command=command, args=args, env=env
|
60
|
+
)
|
61
|
+
)
|
62
|
+
)
|
63
|
+
|
64
|
+
STRATEGIES = {
|
65
|
+
ToolType.MCP: load_mcp_tool,
|
66
|
+
ToolType.FUNCTION: load_function_tool,
|
67
|
+
}
|
68
|
+
|
69
|
+
@staticmethod
|
70
|
+
def create_tool(tool: Tool) -> Optional[Any]:
|
71
|
+
"""Create a tool based on its configuration.
|
72
|
+
|
73
|
+
Args:
|
74
|
+
tool: Tool configuration
|
75
|
+
|
76
|
+
Returns:
|
77
|
+
The created tool or None if creation failed
|
78
|
+
|
79
|
+
Raises:
|
80
|
+
ValueError: If tool type is not supported or config is invalid
|
81
|
+
"""
|
82
|
+
|
83
|
+
return ToolGenerator.STRATEGIES[tool.type](tool)
|
@@ -0,0 +1,68 @@
|
|
1
|
+
"""Configuration service for loading and parsing agent configurations."""
|
2
|
+
|
3
|
+
import os
|
4
|
+
from pathlib import Path
|
5
|
+
from typing import Any, Dict, Optional
|
6
|
+
|
7
|
+
import yaml
|
8
|
+
|
9
|
+
from aigency.models.config import AgentConfig
|
10
|
+
from aigency.utils.logger import get_logger
|
11
|
+
|
12
|
+
|
13
|
+
logger = get_logger()
|
14
|
+
|
15
|
+
class ConfigService:
|
16
|
+
"""Service for loading and managing agent configurations."""
|
17
|
+
def __init__(self, config_file: str, environment: Optional[str] = None):
|
18
|
+
self.config_file = config_file
|
19
|
+
self.environment = environment or os.getenv('ENVIRONMENT', None)
|
20
|
+
self.config = self._load_and_parse()
|
21
|
+
|
22
|
+
def _load_and_parse(self) -> AgentConfig:
|
23
|
+
"""Carga los YAMLs, los mergea y parsea según AgentConfig."""
|
24
|
+
|
25
|
+
logger.info(f"Loading configuration from {self.config_file}")
|
26
|
+
config = self._load_yaml(self.config_file)
|
27
|
+
|
28
|
+
if self.environment is not None:
|
29
|
+
logger.info(f"Environment '{self.environment}' detected, loading environment-specific configuration")
|
30
|
+
env_config = self._load_env_config()
|
31
|
+
if env_config:
|
32
|
+
logger.info(f"Successfully loaded environment configuration with {len(env_config)} keys: {list(env_config.keys())}")
|
33
|
+
config = self._merge_configs(config, env_config)
|
34
|
+
logger.debug(f"Configuration merged successfully for environment '{self.environment}'")
|
35
|
+
else:
|
36
|
+
logger.warning(f"No environment-specific configuration found for '{self.environment}', using base configuration only")
|
37
|
+
|
38
|
+
return AgentConfig(**config)
|
39
|
+
|
40
|
+
def _load_yaml(self, file_path: str) -> Dict[str, Any]:
|
41
|
+
"""Carga un archivo YAML."""
|
42
|
+
try:
|
43
|
+
with open(file_path, "r", encoding="utf-8") as file:
|
44
|
+
return yaml.safe_load(file) or {}
|
45
|
+
except FileNotFoundError:
|
46
|
+
raise FileNotFoundError(f"Archivo de configuración no encontrado: {file_path}")
|
47
|
+
except yaml.YAMLError as e:
|
48
|
+
raise ValueError(f"Error al parsear YAML {file_path}: {e}")
|
49
|
+
|
50
|
+
def _load_env_config(self) -> Optional[Dict[str, Any]]:
|
51
|
+
"""Carga configuración específica del entorno."""
|
52
|
+
config_path = Path(self.config_file)
|
53
|
+
env_file = config_path.parent / f"{config_path.stem}.{self.environment}{config_path.suffix}"
|
54
|
+
|
55
|
+
return self._load_yaml(str(env_file)) if env_file.exists() else None
|
56
|
+
|
57
|
+
def _merge_configs(self, base: Dict[str, Any], env: Optional[Dict[str, Any]]) -> Dict[str, Any]:
|
58
|
+
"""Mergea configuración base con configuración de entorno."""
|
59
|
+
if not env:
|
60
|
+
return base
|
61
|
+
|
62
|
+
result = base.copy()
|
63
|
+
for key, value in env.items():
|
64
|
+
if key in result and isinstance(result[key], dict) and isinstance(value, dict):
|
65
|
+
result[key] = self._merge_configs(result[key], value)
|
66
|
+
else:
|
67
|
+
result[key] = value
|
68
|
+
return result
|
@@ -0,0 +1,101 @@
|
|
1
|
+
import logging
|
2
|
+
import sys
|
3
|
+
from typing import Optional, Dict, Any
|
4
|
+
from aigency.utils.singleton import Singleton
|
5
|
+
|
6
|
+
|
7
|
+
class Logger(Singleton):
|
8
|
+
def __init__(self, config: Optional[Dict[str, Any]] = None):
|
9
|
+
if hasattr(self, '_initialized'):
|
10
|
+
# Si ya está inicializado y se pasa nueva config, actualizar
|
11
|
+
if config and config != getattr(self, 'config', {}):
|
12
|
+
self.config.update(config)
|
13
|
+
self._setup_logger()
|
14
|
+
return
|
15
|
+
|
16
|
+
self._initialized = True
|
17
|
+
self.config = config or {}
|
18
|
+
self._logger = None
|
19
|
+
self._setup_logger()
|
20
|
+
|
21
|
+
def _setup_logger(self):
|
22
|
+
"""Configura el logger con la configuración proporcionada"""
|
23
|
+
# Obtener configuración del logger
|
24
|
+
log_level = self.config.get('log_level', 'INFO').upper()
|
25
|
+
log_format = self.config.get('log_format', '%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
26
|
+
log_file = self.config.get('log_file')
|
27
|
+
logger_name = self.config.get('logger_name', 'aigency')
|
28
|
+
|
29
|
+
# Crear logger
|
30
|
+
self._logger = logging.getLogger(logger_name)
|
31
|
+
self._logger.setLevel(getattr(logging, log_level, logging.INFO))
|
32
|
+
|
33
|
+
# Evitar duplicar handlers si ya existen
|
34
|
+
if self._logger.handlers:
|
35
|
+
return
|
36
|
+
|
37
|
+
# Crear formatter
|
38
|
+
formatter = logging.Formatter(log_format)
|
39
|
+
|
40
|
+
# Handler para consola
|
41
|
+
console_handler = logging.StreamHandler(sys.stdout)
|
42
|
+
console_handler.setLevel(getattr(logging, log_level, logging.INFO))
|
43
|
+
console_handler.setFormatter(formatter)
|
44
|
+
self._logger.addHandler(console_handler)
|
45
|
+
|
46
|
+
# Handler para archivo si se especifica
|
47
|
+
if log_file:
|
48
|
+
file_handler = logging.FileHandler(log_file)
|
49
|
+
file_handler.setLevel(getattr(logging, log_level, logging.INFO))
|
50
|
+
file_handler.setFormatter(formatter)
|
51
|
+
self._logger.addHandler(file_handler)
|
52
|
+
|
53
|
+
def debug(self, message: str, *args, **kwargs):
|
54
|
+
"""Log a debug message"""
|
55
|
+
self._logger.debug(message, *args, **kwargs)
|
56
|
+
|
57
|
+
def info(self, message: str, *args, **kwargs):
|
58
|
+
"""Log an info message"""
|
59
|
+
self._logger.info(message, *args, **kwargs)
|
60
|
+
|
61
|
+
def warning(self, message: str, *args, **kwargs):
|
62
|
+
"""Log a warning message"""
|
63
|
+
self._logger.warning(message, *args, **kwargs)
|
64
|
+
|
65
|
+
def error(self, message: str, *args, **kwargs):
|
66
|
+
"""Log an error message"""
|
67
|
+
self._logger.error(message, *args, **kwargs)
|
68
|
+
|
69
|
+
def critical(self, message: str, *args, **kwargs):
|
70
|
+
"""Log a critical message"""
|
71
|
+
self._logger.critical(message, *args, **kwargs)
|
72
|
+
|
73
|
+
def exception(self, message: str, *args, **kwargs):
|
74
|
+
"""Log an exception with traceback"""
|
75
|
+
self._logger.exception(message, *args, **kwargs)
|
76
|
+
|
77
|
+
def set_level(self, level: str):
|
78
|
+
"""Cambiar el nivel de logging dinámicamente"""
|
79
|
+
log_level = level.upper()
|
80
|
+
self._logger.setLevel(getattr(logging, log_level, logging.INFO))
|
81
|
+
for handler in self._logger.handlers:
|
82
|
+
handler.setLevel(getattr(logging, log_level, logging.INFO))
|
83
|
+
|
84
|
+
def get_logger(self):
|
85
|
+
"""Obtener la instancia del logger interno"""
|
86
|
+
return self._logger
|
87
|
+
|
88
|
+
|
89
|
+
# Función de conveniencia para obtener la instancia del logger
|
90
|
+
def get_logger(config: Optional[Dict[str, Any]] = None) -> Logger:
|
91
|
+
"""
|
92
|
+
Obtiene la instancia singleton del logger.
|
93
|
+
Si es la primera vez que se llama y se proporciona config, se usa esa configuración.
|
94
|
+
|
95
|
+
Args:
|
96
|
+
config: Configuración opcional para el logger (solo se usa en la primera llamada)
|
97
|
+
|
98
|
+
Returns:
|
99
|
+
Instancia singleton del Logger
|
100
|
+
"""
|
101
|
+
return Logger(config)
|
@@ -0,0 +1,12 @@
|
|
1
|
+
class SingletonMeta(type):
|
2
|
+
_instances = {}
|
3
|
+
|
4
|
+
def __call__(cls, *args, **kwargs):
|
5
|
+
if cls not in cls._instances:
|
6
|
+
instance = super().__call__(*args, **kwargs)
|
7
|
+
cls._instances[cls] = instance
|
8
|
+
return cls._instances[cls]
|
9
|
+
|
10
|
+
|
11
|
+
class Singleton(metaclass=SingletonMeta):
|
12
|
+
pass
|
@@ -0,0 +1,96 @@
|
|
1
|
+
"""Utility functions for type conversions and environment variable handling."""
|
2
|
+
|
3
|
+
import asyncio
|
4
|
+
import os
|
5
|
+
import threading
|
6
|
+
|
7
|
+
from a2a.types import FilePart, FileWithBytes, Part, TextPart
|
8
|
+
from google.genai import types
|
9
|
+
|
10
|
+
from aigency.utils.logger import get_logger
|
11
|
+
|
12
|
+
logger = get_logger()
|
13
|
+
|
14
|
+
def convert_a2a_part_to_genai(part: Part) -> types.Part:
|
15
|
+
"""Convert a single A2A Part type into a Google Gen AI Part type.
|
16
|
+
|
17
|
+
Args:
|
18
|
+
part (Part): The A2A Part to convert.
|
19
|
+
|
20
|
+
Returns:
|
21
|
+
types.Part: The equivalent Google Gen AI Part.
|
22
|
+
|
23
|
+
Raises:
|
24
|
+
ValueError: If the part type is not supported.
|
25
|
+
"""
|
26
|
+
part = part.root
|
27
|
+
if isinstance(part, TextPart):
|
28
|
+
return types.Part(text=part.text)
|
29
|
+
raise ValueError(f"Unsupported part type: {type(part)}")
|
30
|
+
|
31
|
+
|
32
|
+
def convert_genai_part_to_a2a(part: types.Part) -> Part:
|
33
|
+
"""Convert a single Google Gen AI Part type into an A2A Part type.
|
34
|
+
|
35
|
+
Args:
|
36
|
+
part (types.Part): The Google Gen AI Part to convert.
|
37
|
+
|
38
|
+
Returns:
|
39
|
+
Part: The equivalent A2A Part.
|
40
|
+
|
41
|
+
Raises:
|
42
|
+
ValueError: If the part type is not supported.
|
43
|
+
"""
|
44
|
+
if part.text:
|
45
|
+
return TextPart(text=part.text)
|
46
|
+
if part.inline_data:
|
47
|
+
return Part(
|
48
|
+
root=FilePart(
|
49
|
+
file=FileWithBytes(
|
50
|
+
bytes=part.inline_data.data,
|
51
|
+
mime_type=part.inline_data.mime_type,
|
52
|
+
)
|
53
|
+
)
|
54
|
+
)
|
55
|
+
raise ValueError(f"Unsupported part type: {part}")
|
56
|
+
|
57
|
+
def expand_env_vars(env_dict):
|
58
|
+
"""
|
59
|
+
Expande los valores del diccionario usando variables de entorno solo si el valor es una clave de entorno existente.
|
60
|
+
Si la variable no existe en el entorno, deja el valor literal.
|
61
|
+
"""
|
62
|
+
result = {}
|
63
|
+
for k, v in env_dict.items():
|
64
|
+
if isinstance(v, str) and v in os.environ:
|
65
|
+
result[k] = os.getenv(v)
|
66
|
+
else:
|
67
|
+
logger.warning(f"Environment variable {v} not found")
|
68
|
+
return result
|
69
|
+
|
70
|
+
def safe_async_run(coro):
|
71
|
+
"""Simple wrapper to safely run async code."""
|
72
|
+
try:
|
73
|
+
loop = asyncio.get_event_loop()
|
74
|
+
if loop.is_running():
|
75
|
+
|
76
|
+
result = None
|
77
|
+
exception = None
|
78
|
+
|
79
|
+
def run_in_thread():
|
80
|
+
nonlocal result, exception
|
81
|
+
try:
|
82
|
+
result = asyncio.run(coro)
|
83
|
+
except Exception as e:
|
84
|
+
exception = e
|
85
|
+
|
86
|
+
thread = threading.Thread(target=run_in_thread)
|
87
|
+
thread.start()
|
88
|
+
thread.join()
|
89
|
+
|
90
|
+
if exception:
|
91
|
+
raise exception
|
92
|
+
return result
|
93
|
+
else:
|
94
|
+
return loop.run_until_complete(coro)
|
95
|
+
except RuntimeError:
|
96
|
+
return asyncio.run(coro)
|
@@ -0,0 +1,18 @@
|
|
1
|
+
Metadata-Version: 2.4
|
2
|
+
Name: aigency
|
3
|
+
Version: 0.0.1
|
4
|
+
Summary: Add your description here
|
5
|
+
Requires-Python: >=3.12
|
6
|
+
Description-Content-Type: text/markdown
|
7
|
+
Requires-Dist: google-adk>=1.11.0
|
8
|
+
Requires-Dist: a2a-sdk==0.3.0
|
9
|
+
Requires-Dist: litellm<1.73.0,>=1.72.6
|
10
|
+
Requires-Dist: pyyaml==6.0.2
|
11
|
+
Requires-Dist: PyJWT==2.10.1
|
12
|
+
|
13
|
+
# aigency-lib
|
14
|
+
|
15
|
+
|
16
|
+
aigency-lib/examples/simple_agents/hello_world_agent
|
17
|
+
|
18
|
+
docker compose up
|
@@ -0,0 +1,18 @@
|
|
1
|
+
README.md
|
2
|
+
pyproject.toml
|
3
|
+
aigency/__init__.py
|
4
|
+
aigency.egg-info/PKG-INFO
|
5
|
+
aigency.egg-info/SOURCES.txt
|
6
|
+
aigency.egg-info/dependency_links.txt
|
7
|
+
aigency.egg-info/requires.txt
|
8
|
+
aigency.egg-info/top_level.txt
|
9
|
+
aigency/agents/executor.py
|
10
|
+
aigency/agents/generator.py
|
11
|
+
aigency/models/config.py
|
12
|
+
aigency/models/core.py
|
13
|
+
aigency/models/tools.py
|
14
|
+
aigency/tools/generator.py
|
15
|
+
aigency/utils/config_service.py
|
16
|
+
aigency/utils/logger.py
|
17
|
+
aigency/utils/singleton.py
|
18
|
+
aigency/utils/utils.py
|
@@ -0,0 +1 @@
|
|
1
|
+
|
@@ -0,0 +1 @@
|
|
1
|
+
aigency
|
@@ -0,0 +1,20 @@
|
|
1
|
+
[project]
|
2
|
+
name = "aigency"
|
3
|
+
version = "0.0.1"
|
4
|
+
description = "Add your description here"
|
5
|
+
readme = "README.md"
|
6
|
+
requires-python = ">=3.12"
|
7
|
+
dependencies = [
|
8
|
+
"google-adk>=1.11.0",
|
9
|
+
"a2a-sdk==0.3.0",
|
10
|
+
"litellm>=1.72.6,<1.73.0",
|
11
|
+
"pyyaml==6.0.2",
|
12
|
+
"PyJWT==2.10.1"
|
13
|
+
]
|
14
|
+
|
15
|
+
[build-system]
|
16
|
+
requires = ["setuptools>=61.0", "wheel"]
|
17
|
+
build-backend = "setuptools.build_meta"
|
18
|
+
|
19
|
+
[tool.setuptools.packages.find]
|
20
|
+
include = ["aigency*"]
|
aigency-0.0.1/setup.cfg
ADDED