aigency 0.0.1rc15474277__py3-none-any.whl → 0.0.1rc163779164__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.
@@ -0,0 +1,59 @@
1
+ import httpx
2
+
3
+ from a2a.client.client import ClientConfig
4
+ from a2a.client.client_factory import ClientFactory
5
+ from a2a.types import AgentCard, Message, SendMessageResponse
6
+ #TODO: Enable when auth is implemented
7
+ #from a2a.client.auth.interceptor import AuthInterceptor
8
+
9
+
10
+ class AgentClient:
11
+ """A class to hold the connections to the remote agents.
12
+
13
+ This class manages connections to remote agents using the A2A protocol.
14
+ It provides methods for retrieving agent information and sending messages
15
+ to remote agents.
16
+
17
+ Attributes:
18
+ _httpx_client (httpx.AsyncClient): The HTTP client used for asynchronous requests.
19
+ agent_card (AgentCard): The agent card containing metadata about the remote agent.
20
+ """
21
+
22
+ def __init__(self, agent_card: AgentCard):
23
+ """Initialize a connection to a remote agent.
24
+
25
+ Args:
26
+ agent_card (AgentCard): The agent card containing metadata about the remote agent.
27
+
28
+ Raises:
29
+ None
30
+
31
+ Returns:
32
+ None
33
+ """
34
+ self._httpx_client = httpx.AsyncClient(timeout=60)
35
+ self.card = agent_card
36
+
37
+ config = ClientConfig(httpx_client=self._httpx_client)
38
+ factory = ClientFactory(config=config)
39
+ self.agent_client = factory.create(agent_card)
40
+
41
+ def get_agent(self) -> AgentCard:
42
+ """Get the agent card for this remote agent connection.
43
+
44
+ Returns:
45
+ AgentCard: The agent card containing metadata about the remote agent.
46
+ """
47
+ return self.card
48
+
49
+ async def send_message(self, message_request: Message) -> SendMessageResponse:
50
+ """Send a message to the remote agent.
51
+
52
+ Args:
53
+ message_request (Message): The message request to send to the remote agent.
54
+
55
+ Returns:
56
+ SendMessageResponse: The response from the remote agent.
57
+ """
58
+ async for response in self.agent_client.send_message(message_request):
59
+ yield response
@@ -0,0 +1,107 @@
1
+ import logging
2
+ import uuid
3
+ from typing import Any
4
+ from a2a.types import Message, Task
5
+ from google.adk.tools.tool_context import ToolContext
6
+
7
+ logging.basicConfig(level=logging.INFO)
8
+ logger = logging.getLogger(__name__)
9
+
10
+
11
+ class Communicator:
12
+ """Base class for Agent-to-Agent communication."""
13
+
14
+ def __init__(self, remote_agent_connections=None):
15
+ self.remote_agent_connections = remote_agent_connections
16
+
17
+ async def send_message(self, agent_name: str, task: str, tool_context: ToolContext):
18
+ """Delegate a task to a specified remote agent.
19
+
20
+ This method sends a message to a remote agent, requesting it to perform a task.
21
+ It handles the creation of the message payload and manages the communication
22
+ with the remote agent.
23
+
24
+ Args:
25
+ agent_name: Name of the remote agent to send the task to.
26
+ task: Detailed description of the task for the remote agent to perform.
27
+ tool_context: Context object containing state and other information.
28
+
29
+ Returns:
30
+ Task object if successful, None otherwise.
31
+
32
+ Raises:
33
+ ValueError: If the specified agent is not found in the available connections.
34
+ """
35
+ logger.info(
36
+ f"`send_message` triggered with agent_name: {agent_name}, task: {task}"
37
+ )
38
+ if agent_name not in self.remote_agent_connections:
39
+ logger.error(
40
+ f"LLM tried to call '{agent_name}' but it was not found. "
41
+ f"Available agents: {list(self.remote_agent_connections.keys())}"
42
+ )
43
+ raise ValueError(f"Agent '{agent_name}' not found.")
44
+
45
+ state = tool_context.state
46
+ client = self.remote_agent_connections[agent_name]
47
+
48
+ if "remote_agent_contexts" not in state:
49
+ state["remote_agent_contexts"] = {}
50
+
51
+ if agent_name not in state["remote_agent_contexts"]:
52
+ logger.debug(f"Creating new context for agent: {agent_name}")
53
+ state["remote_agent_contexts"][agent_name] = {
54
+ "context_id": str(uuid.uuid4())
55
+ }
56
+ context_id = state["remote_agent_contexts"][agent_name]["context_id"]
57
+ task_id = state.get("task_id", None)
58
+ message_id = state.get("input_message_metadata", {}).get(
59
+ "message_id", str(uuid.uuid4())
60
+ )
61
+
62
+ payload = self.create_send_message_payload(task, task_id, context_id)
63
+ payload["message"]["message_id"] = message_id
64
+ logger.debug("`send_message` triggered with payload: %s", payload)
65
+
66
+ send_response = None
67
+ async for resp in client.send_message(
68
+ message_request=Message(**payload.get("message"))
69
+ ):
70
+ send_response = resp
71
+
72
+ if isinstance(send_response, tuple):
73
+ send_response, _ = send_response
74
+
75
+ if not isinstance(send_response, Task):
76
+ return None
77
+ return send_response
78
+
79
+ @staticmethod
80
+ def create_send_message_payload(
81
+ text: str,
82
+ task_id: str | None = None,
83
+ context_id: str | None = None,
84
+ ) -> dict[str, Any]:
85
+ """Create a message payload for sending to a remote agent.
86
+
87
+ Args:
88
+ text: The text content of the message.
89
+ task_id: Optional task ID to associate with the message.
90
+ context_id: Optional context ID to associate with the message.
91
+
92
+ Returns:
93
+ dict: A dictionary containing the formatted message payload ready
94
+ to be sent to a remote agent.
95
+ """
96
+ payload: dict[str, Any] = {
97
+ "message": {
98
+ "role": "user",
99
+ "parts": [{"type": "text", "text": text}],
100
+ "message_id": uuid.uuid4().hex,
101
+ },
102
+ }
103
+ if task_id:
104
+ payload["message"]["task_id"] = task_id
105
+ if context_id:
106
+ payload["message"]["context_id"] = context_id
107
+ return payload
@@ -1,39 +1,57 @@
1
- """Agent generator module for creating A2A agents."""
1
+ import logging
2
+ import httpx
3
+ from google.adk.agents import Agent
4
+ from a2a.types import AgentCapabilities, AgentCard, AgentSkill
2
5
 
3
- from typing import Any, Dict, List
6
+ from aigency.agents.executor import AgentA2AExecutor
7
+ from aigency.agents.client import AgentClient
8
+ from aigency.schemas.aigency_config import AigencyConfig
9
+ from aigency.tools.generator import ToolGenerator
10
+ from aigency.utils.utils import generate_url, safe_async_run
11
+ from aigency.agents.communicator import Communicator
4
12
 
5
- from a2a.types import AgentCapabilities, AgentCard, AgentSkill
6
- from google.adk.agents import Agent
7
13
  from google.adk.artifacts import InMemoryArtifactService
8
14
  from google.adk.memory.in_memory_memory_service import InMemoryMemoryService
9
15
  from google.adk.runners import Runner
10
16
  from google.adk.sessions import InMemorySessionService
11
17
 
12
- from aigency.agents.executor import AgentA2AExecutor
13
- from aigency.models.config import AgentConfig
14
- from aigency.tools.generator import ToolGenerator
18
+ from a2a.client import A2ACardResolver
15
19
 
16
20
 
17
21
  class AgentA2AGenerator:
18
- """Generator for creating A2A agents and related components."""
19
22
 
20
23
  @staticmethod
21
- def create_agent(agent_config: AgentConfig) -> Agent:
24
+ def create_agent(agent_config: AigencyConfig) -> Agent:
25
+
26
+ tools = [
27
+ ToolGenerator.create_tool(tool_cfg) for tool_cfg in agent_config.agent.tools
28
+ ]
29
+
30
+ remote_agents = agent_config.agent.remote_agents
31
+ if remote_agents:
32
+ remote_agent_connections = AgentA2AGenerator.build_remote_agent_connections(
33
+ agent_config
34
+ )
35
+ logger.info(f"Remote agent connections: {remote_agent_connections}")
36
+ communicator = Communicator(
37
+ remote_agent_connections=remote_agent_connections
38
+ )
39
+ tools.append(communicator.send_message)
22
40
 
23
- tools = [ToolGenerator.create_tool(tool_cfg) for tool_cfg in agent_config.tools]
24
-
25
41
  return Agent(
26
- name=agent_config.name,
27
- model=agent_config.model.name,
28
- instruction=agent_config.instruction,
42
+ name=agent_config.metadata.name,
43
+ model=agent_config.agent.model.name,
44
+ instruction=agent_config.agent.instruction,
29
45
  tools=tools,
30
46
  )
31
47
 
32
48
  @staticmethod
33
- def build_agent_card(agent_config: AgentConfig) -> AgentCard:
49
+ def build_agent_card(agent_config: AigencyConfig) -> AgentCard:
34
50
 
35
51
  # TODO: Parse properly
36
- capabilities = AgentCapabilities(streaming=agent_config.capabilities.streaming)
52
+ capabilities = AgentCapabilities(
53
+ streaming=agent_config.service.capabilities.streaming
54
+ )
37
55
 
38
56
  skills = [
39
57
  AgentSkill(
@@ -43,24 +61,22 @@ class AgentA2AGenerator:
43
61
  tags=skill.tags,
44
62
  examples=skill.examples,
45
63
  )
46
- for skill in agent_config.skills
64
+ for skill in agent_config.agent.skills
47
65
  ]
48
66
 
49
67
  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,
68
+ name=agent_config.metadata.name,
69
+ description=agent_config.metadata.description,
70
+ url=agent_config.service.url,
71
+ version=agent_config.metadata.version,
72
+ default_input_modes=agent_config.service.interface.default_input_modes,
73
+ default_output_modes=agent_config.service.interface.default_output_modes,
56
74
  capabilities=capabilities,
57
75
  skills=skills,
58
76
  )
59
77
 
60
78
  @staticmethod
61
- def build_executor(
62
- agent: Agent, agent_card: AgentCard
63
- ) -> AgentA2AExecutor:
79
+ def build_executor(agent: Agent, agent_card: AgentCard) -> AgentA2AExecutor:
64
80
 
65
81
  runner = Runner(
66
82
  app_name=agent.name,
@@ -71,3 +87,48 @@ class AgentA2AGenerator:
71
87
  )
72
88
 
73
89
  return AgentA2AExecutor(runner=runner, card=agent_card)
90
+
91
+ @staticmethod
92
+ def build_remote_agent_connections(agent_config: AigencyConfig):
93
+ """Initialize connections to all remote agents asynchronously.
94
+
95
+ Tests each connection individually with detailed logging to help identify
96
+ any connection issues. It attempts to connect to each remote agent address,
97
+ retrieve its agent card, and store the connection for later use.
98
+
99
+ Raises:
100
+ No exceptions are raised, but errors are logged.
101
+ """
102
+
103
+ if not agent_config.agent.remote_agents:
104
+ return {}
105
+
106
+ remote_agent_configs = [
107
+ {"url": generate_url(host=remote_agent.host, port=remote_agent.port)}
108
+ for remote_agent in agent_config.agent.remote_agents
109
+ ]
110
+
111
+ async def _connect():
112
+ remote_agent_connections = {}
113
+ async with httpx.AsyncClient(timeout=60) as client:
114
+ for config in remote_agent_configs:
115
+ address = config.get("url")
116
+ logger.debug(f"--- Attempting connection to: {address} ---")
117
+ try:
118
+ card_resolver = A2ACardResolver(client, address)
119
+ card = await card_resolver.get_agent_card()
120
+ remote_connection = AgentClient(agent_card=card)
121
+ remote_agent_connections[card.name] = remote_connection
122
+ except Exception as e:
123
+ logger.error(
124
+ f"--- CRITICAL FAILURE for address: {address} ---",
125
+ exc_info=True,
126
+ )
127
+ raise e
128
+ return remote_agent_connections
129
+
130
+ try:
131
+ return safe_async_run(_connect())
132
+ except Exception as e:
133
+ logger.error("--- CRITICAL FAILURE", exc_info=True)
134
+ raise e
@@ -0,0 +1,16 @@
1
+ from pydantic import BaseModel
2
+ from typing import List, Optional
3
+ from aigency.schemas.agent.model import AgentModel
4
+ from aigency.schemas.agent.skills import Skill
5
+ from aigency.schemas.agent.tools import Tool
6
+ from aigency.schemas.agent.remote_agent import RemoteAgent
7
+
8
+
9
+ class Agent(BaseModel):
10
+ """El 'cerebro' del agente: su lógica, modelo y capacidades."""
11
+
12
+ model: AgentModel
13
+ instruction: str
14
+ skills: List[Skill]
15
+ tools: Optional[List[Tool]] = []
16
+ remote_agents: Optional[List[RemoteAgent]] = []
@@ -0,0 +1,12 @@
1
+ from typing import Optional
2
+ from pydantic import BaseModel
3
+
4
+ class ProviderConfig(BaseModel):
5
+ """Configuration for AI model provider."""
6
+ name: str
7
+ endpoint: Optional[str] = None
8
+
9
+ class AgentModel(BaseModel):
10
+ """Configuration for AI model."""
11
+ name: str
12
+ provider: Optional[ProviderConfig] = None
@@ -0,0 +1,9 @@
1
+ from pydantic import BaseModel, Field
2
+
3
+
4
+ class RemoteAgent(BaseModel):
5
+ """Remote agent configuration."""
6
+
7
+ name: str
8
+ host: str
9
+ port: int = Field(..., ge=1, le=65535)
@@ -0,0 +1,10 @@
1
+ from pydantic import BaseModel
2
+ from typing import List
3
+
4
+ class Skill(BaseModel):
5
+ """Define una habilidad específica del agente."""
6
+ id: str
7
+ name: str
8
+ description: str
9
+ tags: List[str]
10
+ examples: List[str]
@@ -1,39 +1,53 @@
1
1
  """Tool models for agent configuration."""
2
2
 
3
3
  from enum import Enum
4
- from typing import Dict, List, Optional
4
+ from typing import Dict, List, Optional, TypeAlias
5
5
 
6
6
  from pydantic import BaseModel
7
7
 
8
8
 
9
9
  class ToolType(str, Enum):
10
10
  """Enum for tool types."""
11
+
11
12
  MCP = "mcp"
12
13
  FUNCTION = "function"
13
14
 
14
- class Tool(BaseModel):
15
+
16
+ class BaseTool(BaseModel):
15
17
  """Define an external tool that the agent can use."""
18
+
16
19
  type: ToolType
17
20
  name: str
18
21
  description: str
19
22
 
20
- class FunctionTool(Tool):
23
+
24
+ class FunctionTool(BaseTool):
21
25
  """Configuration for function-based tools."""
26
+
22
27
  module_path: str
23
28
  function_name: str
24
29
 
30
+
25
31
  class McpTypeStreamable(BaseModel):
26
32
  """Model for streamable tool type."""
33
+
27
34
  url: str
28
35
  port: int
29
36
  path: str = "/"
30
37
 
38
+
31
39
  class McpTypeStdio(BaseModel):
32
40
  """Model for stdio tool type."""
41
+
33
42
  command: str
34
43
  args: List[str]
35
44
  env: Optional[Dict[str, str]] = None
36
45
 
37
- class McpTool(Tool):
46
+
47
+ class McpTool(BaseTool):
38
48
  """Configuration for MCP-based tools."""
39
- mcp_config: McpTypeStreamable | McpTypeStdio
49
+
50
+ mcp_config: McpTypeStreamable | McpTypeStdio
51
+
52
+
53
+ Tool: TypeAlias = FunctionTool | McpTool
@@ -0,0 +1,14 @@
1
+ from pydantic import BaseModel
2
+ from typing import Optional
3
+ from aigency.schemas.observability.observability import Observability
4
+ from aigency.schemas.metadata.metadata import Metadata
5
+ from aigency.schemas.agent.agent import Agent
6
+ from aigency.schemas.service.service import Service
7
+
8
+ class AigencyConfig(BaseModel):
9
+ """Root Pydantic model for complete agent configuration."""
10
+
11
+ metadata: Metadata
12
+ service: Service
13
+ agent: Agent
14
+ observability: Optional[Observability] = None
@@ -0,0 +1,8 @@
1
+ from pydantic import BaseModel
2
+
3
+
4
+ class Metadata(BaseModel):
5
+ """Metadatos descriptivos del agente."""
6
+ name: str
7
+ version: str
8
+ description: str
@@ -0,0 +1,10 @@
1
+ from pydantic import BaseModel
2
+ from aigency.schemas.observability.phoenix import Phoenix
3
+
4
+ class Monitoring(BaseModel):
5
+ """Configuración de las herramientas de monitoreo."""
6
+ phoenix: Phoenix
7
+
8
+ class Observability(BaseModel):
9
+ """Agrupa todas las configuraciones de observabilidad."""
10
+ monitoring: Monitoring
@@ -0,0 +1,6 @@
1
+ from pydantic import BaseModel
2
+
3
+ class Phoenix(BaseModel):
4
+ """Configuración del monitor Phoenix."""
5
+ host: str
6
+ port: int
@@ -0,0 +1,5 @@
1
+ from pydantic import BaseModel
2
+
3
+ class Capabilities(BaseModel):
4
+ """Capacidades técnicas del servicio del agente."""
5
+ streaming: bool
@@ -0,0 +1,7 @@
1
+ from typing import List
2
+ from pydantic import BaseModel
3
+
4
+ class Interface(BaseModel):
5
+ """Define los modos de comunicación del agente."""
6
+ default_input_modes: List[str]
7
+ default_output_modes: List[str]
@@ -0,0 +1,9 @@
1
+ from pydantic import BaseModel
2
+ from aigency.schemas.service.interface import Interface
3
+ from aigency.schemas.service.capabilities import Capabilities
4
+
5
+ class Service(BaseModel):
6
+ """Configuración de red y comunicación del agente."""
7
+ url: str
8
+ interface: Interface
9
+ capabilities: Capabilities
@@ -14,7 +14,7 @@ from google.adk.tools.mcp_tool.mcp_toolset import (
14
14
  StreamableHTTPConnectionParams,
15
15
  )
16
16
 
17
- from aigency.models.tools import (
17
+ from aigency.schemas.agent.tools import (
18
18
  FunctionTool,
19
19
  McpTool,
20
20
  McpTypeStdio,
@@ -43,11 +43,7 @@ class ToolGenerator:
43
43
 
44
44
  if isinstance(config.mcp_config, McpTypeStreamable):
45
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
- )
46
+ return MCPToolset(connection_params=StreamableHTTPConnectionParams(url=url))
51
47
  elif isinstance(config.mcp_config, McpTypeStdio):
52
48
  command = config.mcp_config.command
53
49
  args = config.mcp_config.args
@@ -65,7 +61,7 @@ class ToolGenerator:
65
61
  ToolType.MCP: load_mcp_tool,
66
62
  ToolType.FUNCTION: load_function_tool,
67
63
  }
68
-
64
+
69
65
  @staticmethod
70
66
  def create_tool(tool: Tool) -> Optional[Any]:
71
67
  """Create a tool based on its configuration.
@@ -6,36 +6,46 @@ from typing import Any, Dict, Optional
6
6
 
7
7
  import yaml
8
8
 
9
- from aigency.models.config import AgentConfig
9
+ from aigency.schemas.aigency_config import AigencyConfig
10
10
  from aigency.utils.logger import get_logger
11
11
 
12
12
 
13
13
  logger = get_logger()
14
14
 
15
+
15
16
  class ConfigService:
16
17
  """Service for loading and managing agent configurations."""
18
+
17
19
  def __init__(self, config_file: str, environment: Optional[str] = None):
18
20
  self.config_file = config_file
19
- self.environment = environment or os.getenv('ENVIRONMENT', None)
21
+ self.environment = environment or os.getenv("ENVIRONMENT", None)
20
22
  self.config = self._load_and_parse()
21
23
 
22
- def _load_and_parse(self) -> AgentConfig:
23
- """Carga los YAMLs, los mergea y parsea según AgentConfig."""
24
+ def _load_and_parse(self) -> AigencyConfig:
25
+ """Carga los YAMLs, los mergea y parsea según AigencyConfig."""
24
26
 
25
27
  logger.info(f"Loading configuration from {self.config_file}")
26
28
  config = self._load_yaml(self.config_file)
27
29
 
28
30
  if self.environment is not None:
29
- logger.info(f"Environment '{self.environment}' detected, loading environment-specific configuration")
31
+ logger.info(
32
+ f"Environment '{self.environment}' detected, loading environment-specific configuration"
33
+ )
30
34
  env_config = self._load_env_config()
31
35
  if env_config:
32
- logger.info(f"Successfully loaded environment configuration with {len(env_config)} keys: {list(env_config.keys())}")
36
+ logger.info(
37
+ f"Successfully loaded environment configuration with {len(env_config)} keys: {list(env_config.keys())}"
38
+ )
33
39
  config = self._merge_configs(config, env_config)
34
- logger.debug(f"Configuration merged successfully for environment '{self.environment}'")
40
+ logger.debug(
41
+ f"Configuration merged successfully for environment '{self.environment}'"
42
+ )
35
43
  else:
36
- logger.warning(f"No environment-specific configuration found for '{self.environment}', using base configuration only")
37
-
38
- return AgentConfig(**config)
44
+ logger.warning(
45
+ f"No environment-specific configuration found for '{self.environment}', using base configuration only"
46
+ )
47
+
48
+ return AigencyConfig(**config)
39
49
 
40
50
  def _load_yaml(self, file_path: str) -> Dict[str, Any]:
41
51
  """Carga un archivo YAML."""
@@ -43,26 +53,37 @@ class ConfigService:
43
53
  with open(file_path, "r", encoding="utf-8") as file:
44
54
  return yaml.safe_load(file) or {}
45
55
  except FileNotFoundError:
46
- raise FileNotFoundError(f"Archivo de configuración no encontrado: {file_path}")
56
+ raise FileNotFoundError(
57
+ f"Archivo de configuración no encontrado: {file_path}"
58
+ )
47
59
  except yaml.YAMLError as e:
48
60
  raise ValueError(f"Error al parsear YAML {file_path}: {e}")
49
61
 
50
62
  def _load_env_config(self) -> Optional[Dict[str, Any]]:
51
63
  """Carga configuración específica del entorno."""
52
64
  config_path = Path(self.config_file)
53
- env_file = config_path.parent / f"{config_path.stem}.{self.environment}{config_path.suffix}"
54
-
65
+ env_file = (
66
+ config_path.parent
67
+ / f"{config_path.stem}.{self.environment}{config_path.suffix}"
68
+ )
69
+
55
70
  return self._load_yaml(str(env_file)) if env_file.exists() else None
56
71
 
57
- def _merge_configs(self, base: Dict[str, Any], env: Optional[Dict[str, Any]]) -> Dict[str, Any]:
72
+ def _merge_configs(
73
+ self, base: Dict[str, Any], env: Optional[Dict[str, Any]]
74
+ ) -> Dict[str, Any]:
58
75
  """Mergea configuración base con configuración de entorno."""
59
76
  if not env:
60
77
  return base
61
-
78
+
62
79
  result = base.copy()
63
80
  for key, value in env.items():
64
- if key in result and isinstance(result[key], dict) and isinstance(value, dict):
81
+ if (
82
+ key in result
83
+ and isinstance(result[key], dict)
84
+ and isinstance(value, dict)
85
+ ):
65
86
  result[key] = self._merge_configs(result[key], value)
66
87
  else:
67
88
  result[key] = value
68
- return result
89
+ return result