Puedes tener agentes con comportamientos que no encajan en una configuración predefinida. En esos casos, puedes construir agentes personalizados.
Todos los agentes en AgentChat heredan de la clase BaseChatAgent e implementan los siguientes métodos y atributos abstractos:
on_messages(): El método abstracto que define el comportamiento del agente en respuesta a los mensajes. Este método se llama cuando se le pide al agente que proporcione una respuesta mediante run(). Devuelve un objeto Response.
on_reset(): El método abstracto que reinicia el agente a su estado inicial. Este método se llama cuando se solicita al agente que se reinicie.
produced_message_types: La lista de tipos de mensajes BaseChatMessage que el agente puede producir en su respuesta.
Opcionalmente, puedes implementar el método on_messages_stream() para transmitir mensajes a medida que son generados por el agente. Si este método no está implementado, el agente utiliza la implementación predeterminada de on_messages_stream() que llama a on_messages() y emite todos los mensajes en la respuesta.
Agente de Cuenta Regresiva
En este ejemplo, creamos un agente simple que cuenta regresivamente desde un número dado hasta cero, y produce un flujo de mensajes con el conteo actual.
from typing import AsyncGenerator, List, Sequence
from saptiva_agents.agents import BaseChatAgent
from saptiva_agents.base import Response
from saptiva_agents.messages import BaseAgentEvent, BaseChatMessage, TextMessage
from saptiva_agents.core import CancellationToken
class CountDownAgent(BaseChatAgent):
def __init__(self, name: str, count: int = 3):
super().__init__(name, "A simple agent that counts down.")
self._count = count
@property
def produced_message_types(self) -> Sequence[type[BaseChatMessage]]:
return (TextMessage,)
async def on_messages(self, messages: Sequence[BaseChatMessage], cancellation_token: CancellationToken) -> Response:
# Llama a on_messages_stream.
response: Response | None = None
async for message in self.on_messages_stream(messages, cancellation_token):
if isinstance(message, Response):
response = message
assert response is not None
return response
async def on_messages_stream(
self, messages: Sequence[BaseChatMessage], cancellation_token: CancellationToken
) -> AsyncGenerator[BaseAgentEvent | BaseChatMessage | Response, None]:
inner_messages: List[BaseAgentEvent | BaseChatMessage] = []
for i in range(self._count, 0, -1):
msg = TextMessage(content=f"{i}...", source=self.name)
inner_messages.append(msg)
yield msg
# La respuesta se devuelve al final del stream.
# Contiene el mensaje final y todos los mensajes internos.
yield Response(chat_message=TextMessage(content="Done!", source=self.name), inner_messages=inner_messages)
async def on_reset(self, cancellation_token: CancellationToken) -> None:
pass
async def run_countdown_agent() -> None:
# Crear un agente de cuenta regresiva.
countdown_agent = CountDownAgent("countdown")
# Ejecutar el agente con una tarea dada y transmitir la respuesta.
async for message in countdown_agent.on_messages_stream([], CancellationToken()):
if isinstance(message, Response):
print(message.chat_message)
else:
print(message)
# Usar asyncio.run(run_countdown_agent()) al ejecutar en un script.
await run_countdown_agent()
3...
2...
1...
Done!
Agente Aritmético
En este ejemplo, creamos una clase de agente capaz de realizar operaciones aritméticas simples sobre un número entero dado. Luego, utilizaremos distintas instancias de esta clase de agente dentro de un SelectorGroupChat para transformar un número entero en otro mediante la aplicación de una secuencia de operaciones aritméticas.
La clase ArithmeticAgent recibe una función llamada operator_func, la cual toma un número entero y devuelve otro número entero tras aplicarle una operación aritmética. En su método on_messages, aplica la función operator_func al número recibido en el mensaje de entrada y devuelve una respuesta con el resultado.
from typing import Callable, Sequence
from saptiva_agents.agents import BaseChatAgent
from saptiva_agents.base import SaptivaAIChatCompletionClient, Response
from saptiva_agents.conditions import MaxMessageTermination
from saptiva_agents.messages import BaseChatMessage
from saptiva_agents.teams import SelectorGroupChat
from saptiva_agents.ui import Console
from saptiva_agents.core import CancellationToken
class ArithmeticAgent(BaseChatAgent):
def __init__(self, name: str, description: str, operator_func: Callable[[int], int]) -> None:
super().__init__(name, description=description)
self._operator_func = operator_func
self._message_history: List[BaseChatMessage] = []
@property
def produced_message_types(self) -> Sequence[type[BaseChatMessage]]:
return (TextMessage,)
async def on_messages(self, messages: Sequence[BaseChatMessage], cancellation_token: CancellationToken) -> Response:
# Actualizar el historial de mensajes.
# NOTA: es posible que la lista de mensajes esté vacía, lo que significa que el agente fue seleccionado previamente.
self._message_history.extend(messages)
# Extraer el número del último mensaje recibido.
assert isinstance(self._message_history[-1], TextMessage)
number = int(self._message_history[-1].content)
# Aplicar la función de operación al número.
result = self._operator_func(number)
# Crear un nuevo mensaje con el resultado.
response_message = TextMessage(content=str(result), source=self.name)
# Actualizar nuevamente el historial de mensajes.
self._message_history.append(response_message)
# Retornar la respuesta generada
return Response(chat_message=response_message)
async def on_reset(self, cancellation_token: CancellationToken) -> None:
pass
Nota
El método on_messages puede ser llamado con una lista vacía de mensajes. Esto significa que el agente fue seleccionado anteriormente y ahora es llamado nuevamente, pero sin recibir nuevos mensajes por parte del emisor. Por lo tanto, es importante mantener un historial de los mensajes recibidos previamente por el agente y utilizar dicho historial para generar la respuesta.
Ahora podemos crear un SelectorGroupChat con 5 instancias del ArithmeticAgent:
Una que suma 1 al número entero recibido.
Una que resta 1 al número entero recibido.
Una que multiplica por 2 al número entero recibido.
Una que divide entre 2 al número entero recibido y redondea hacia abajo al entero más cercano.
Una que retorna el número entero recibido sin cambios.
Luego, creamos un SelectorGroupChat con estos agentes y configuramos adecuadamente el selector:
Permitimos seleccionar consecutivamente el mismo agente, para posibilitar operaciones repetidas.
Personalizamos el mensaje del selector (selector_prompt) para adaptar la respuesta del modelo a la tarea específica.
async def run_number_agents() -> None:
# Crear agentes para operaciones numéricas.
add_agent = ArithmeticAgent("add_agent", "Suma 1 al número.", lambda x: x + 1)
multiply_agent = ArithmeticAgent("multiply_agent", "Multiplica el número por 2.", lambda x: x * 2)
subtract_agent = ArithmeticAgent("subtract_agent", "Resta 1 al número.", lambda x: x - 1)
divide_agent = ArithmeticAgent("divide_agent", "Divide el número entre 2 y redondea hacia abajo.", lambda x: x // 2)
identity_agent = ArithmeticAgent("identity_agent", "Retorna el número tal como está.", lambda x: x)
# Condición de terminación: detener después de 10 mensajes.
termination_condition = MaxMessageTermination(10)
# Crear un SelectorGroupChat con agentes específicos.
selector_group_chat = SelectorGroupChat(
[add_agent, multiply_agent, subtract_agent, divide_agent, identity_agent],
model_client=OpenAIChatCompletionClient(model="gpt-4o"),
termination_condition=termination_condition,
allow_repeated_speaker=True, # Permitir que el mismo agente pueda hablar varias veces, necesario para esta tarea.
selector_prompt=(
"Roles disponibles:\n{roles}\nDescripción de sus funciones:\n{participants}\n"
"Historial actual de la conversación:\n{history}\n"
"Selecciona el rol más apropiado para el siguiente mensaje, y retorna solo el nombre del rol."
),
)
# Ejecutar el selector group chat con una tarea específica y transmitir la respuesta.
task: List[BaseChatMessage] = [
TextMessage(content="Aplica las operaciones para convertir el número dado en 25.", source="user"),
TextMessage(content="10", source="user"),
]
stream = selector_group_chat.run_stream(task=task)
await Console(stream)
# Usa asyncio.run(run_number_agents()) cuando ejecutes desde un script.
await run_number_agents()
---------- user ----------
Aplica las operaciones para convertir el número dado en 25.
---------- user ----------
10
---------- multiply_agent ----------
20
---------- add_agent ----------
21
---------- multiply_agent ----------
42
---------- divide_agent ----------
21
---------- add_agent ----------
22
---------- add_agent ----------
23
---------- add_agent ----------
24
---------- add_agent ----------
25
---------- Summary ----------
Number of messages: 10
Finish reason: Maximum number of messages 10 reached, current message count: 10
Total prompt tokens: 0
Total completion tokens: 0
Duration: 2.40 seconds
Del resultado, podemos ver que los agentes han transformado con éxito el número entero de entrada de 10 a 25 eligiendo los agentes apropiados que aplican las operaciones aritméticas en secuencia.
Uso de Clientes de Modelos Personalizados en Agentes Personalizados
Una de las características clave del agente predefinido AssistantAgent en AgentChat es que acepta un argumento model_client y puede utilizarlo para responder a los mensajes. Sin embargo, en algunos casos podrías necesitar que tu agente utilice un cliente de modelo personalizado.
Puedes lograr esto mediante la creación de un agente personalizado que implemente tu propio cliente de modelo.
from typing import Sequence
from saptiva_agents import QWEN_MODEL
from saptiva_agents.agents import BaseChatAgent
from saptiva_agents.base import SaptivaAIChatCompletionClient, Response
from saptiva_agents.core import UnboundedChatCompletionContext, CancellationToken
from saptiva_agents.messages import TextMessage, BaseChatMessage
class QwenAssistantAgent(BaseChatAgent):
def __init__(
self,
name: str,
description: str = "Un agente que brinda asistencia con la capacidad de usar herramientas.",
model: str = QWEN_MODEL,
api_key: str = "TU_SAPTIVA_API_KEY",
system_message: str
| None = "Eres un asistente útil que puede responder a los mensajes. Responde con TERMINATE cuando se ha completado la tarea.",
):
super().__init__(name=name, description=description)
self._model_context = UnboundedChatCompletionContext()
self._model_client = self.get_model_client(api_key=api_key)
self._system_message = system_message
self._model = model
def get_model_client(self, api_key):
return SaptivaAIChatCompletionClient(
model=self._model,
api_key=api_key,
)
@property
def produced_message_types(self) -> Sequence[type[BaseChatMessage]]:
return (TextMessage,)
async def on_messages(self, messages: Sequence[BaseChatMessage], cancellation_token: CancellationToken) -> Response:
final_response = None
async for message in self.on_messages_stream(messages, cancellation_token):
if isinstance(message, Response):
final_response = message
if final_response is None:
raise AssertionError("La transmisión debería haber devuelto el resultado final.")
return final_response
async def on_reset(self, cancellation_token: CancellationToken) -> None:
"""Restablezca el asistente limpiando el contexto del modelo."""
await self._model_context.clear()
qwen_assistant = QwenAssistantAgent("qwen_assistant")
await Console(gemini_assistant.run_stream(task="What is the capital of New York?"))
---------- user ----------
What is the capital of New York?
---------- qwen_assistant ----------
Albany
TERMINATE
TaskResult(messages=[TextMessage(source='user', models_usage=None, content='What is the capital of New York?', type='TextMessage'), TextMessage(source='qwen_assistant', models_usage=RequestUsage(prompt_tokens=46, completion_tokens=5), content='Albany\nTERMINATE\n', type='TextMessage')], stop_reason=None)
En el ejemplo anterior, hemos optado por proporcionar model, api_key y system_message como argumentos. Sin embargo, tú puedes elegir proporcionar cualquier otro argumento que sea requerido por el cliente de modelo que estés utilizando o que encaje con el diseño de tu aplicación.
Ahora exploremos cómo utilizar este agente personalizado como parte de un equipo en Saptiva-Agents.
from saptiva_agents.agents import AssistantAgent
from saptiva_agents.conditions import TextMentionTermination
from saptiva_agents.teams import RoundRobinGroupChat
from saptiva_agents.ui import Console
# Crear el agente principal.
primary_agent = AssistantAgent(
"primary",
model_client=SaptivaAIChatCompletionClient(model="llama3.3:70b", api_key="TU_SAPTIVA_API_KEY"),
system_message="You are a helpful AI assistant.",
)
# Crear al agente crítico basado en nuestro nuevo QwenAssistantAgent.
qwen_critic_agent = QwenAssistantAgent(
"qwen_critic",
system_message="Provide constructive feedback. Respond with 'APPROVE' to when your feedbacks are addressed.",
)
# Defina una condición de terminación que detenga la tarea si el crítico aprueba o después de 10 mensajes.
termination = TextMentionTermination("APPROVE") | MaxMessageTermination(10)
# Cree un equipo con los agentes primarios y críticos.
team = RoundRobinGroupChat([primary_agent, qwen_critic_agent], termination_condition=termination)
await Console(team.run_stream(task="Write a Haiku poem with 4 lines about the fall season."))
---------- user ----------
Write a Haiku poem with 4 lines about the fall season.
---------- primary ----------
Crimson leaves cascade,
Whispering winds sing of change,
Chill wraps the fading,
Nature's quilt, rich and warm.
---------- qwen_critic ----------
The poem is good, but it has four lines instead of three. A haiku must have three lines with a 5-7-5 syllable structure. The content is evocative of autumn, but the form is incorrect. Please revise to adhere to the haiku's syllable structure.
---------- primary ----------
Thank you for your feedback! Here’s a revised haiku that follows the 5-7-5 syllable structure:
Crimson leaves drift down,
Chill winds whisper through the gold,
Autumn’s breath is near.
---------- qwen_critic ----------
The revised haiku is much improved. It correctly follows the 5-7-5 syllable structure and maintains the evocative imagery of autumn. APPROVE
TaskResult(messages=[TextMessage(source='user', models_usage=None, content='Write a Haiku poem with 4 lines about the fall season.', type='TextMessage'), TextMessage(source='primary', models_usage=RequestUsage(prompt_tokens=33, completion_tokens=31), content="Crimson leaves cascade, \nWhispering winds sing of change, \nChill wraps the fading, \nNature's quilt, rich and warm.", type='TextMessage'), TextMessage(source='qwen_critic', models_usage=RequestUsage(prompt_tokens=86, completion_tokens=60), content="The poem is good, but it has four lines instead of three. A haiku must have three lines with a 5-7-5 syllable structure. The content is evocative of autumn, but the form is incorrect. Please revise to adhere to the haiku's syllable structure.\n", type='TextMessage'), TextMessage(source='primary', models_usage=RequestUsage(prompt_tokens=141, completion_tokens=49), content='Thank you for your feedback! Here’s a revised haiku that follows the 5-7-5 syllable structure:\n\nCrimson leaves drift down, \nChill winds whisper through the gold, \nAutumn’s breath is near.', type='TextMessage'), TextMessage(source='qwen_critic', models_usage=RequestUsage(prompt_tokens=211, completion_tokens=32), content='The revised haiku is much improved. It correctly follows the 5-7-5 syllable structure and maintains the evocative imagery of autumn. APPROVE\n', type='TextMessage')], stop_reason="Text 'APPROVE' mentioned")
En la sección anterior mostramos varios conceptos muy importantes:
Hemos desarrollado un agente personalizado que utiliza 2 modelos para responder mensajes.
Demostramos que este agente personalizado puede utilizarse dentro del ecosistema general de Saptiva-Agents, en este caso como participante en un RoundRobinGroupChat, siempre que herede de la clase BaseChatAgent.