Skip to content

Commit 6c41c71

Browse files
authored
feat: add echo agent for testing. (#62)
* feat: add echo agent for testing. Signed-off-by: Jeff Napper <[email protected]> * fix: fixed typo in manifest. Hat tip, Marco. Signed-off-by: Jeff Napper <[email protected]> * chore: update for workflow server type mismatch. Signed-off-by: Jeff Napper <[email protected]> * chore: add code in main to imitate workflow server. Signed-off-by: Jeff Napper <[email protected]> * feat: added test client that can use file describing API operations to test against an ACP client. Signed-off-by: Jeff Napper <[email protected]> --------- Signed-off-by: Jeff Napper <[email protected]>
1 parent 34a5ca7 commit 6c41c71

File tree

19 files changed

+6800
-1
lines changed

19 files changed

+6800
-1
lines changed

agntcy_acp/__init__.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
# SPDX-License-Identifier: Apache-2.0
33
import json
44
from os import getenv
5+
from pydantic_core import CoreSchema, core_schema
6+
from pydantic import GetCoreSchemaHandler, BaseModel
57
from typing import Optional, Any, Dict, Union
68

79
from .acp_v0.sync_client.api_client import ApiClient
@@ -73,7 +75,7 @@ def _get_envvar_param(prefix: str, varname: str) -> Optional[str]:
7375
env_varname = prefix + varname.upper()
7476
return getenv(env_varname.translate(__ENV_VAR_SPECIAL_CHAR_TABLE), None)
7577

76-
class ApiClientConfiguration(Configuration):
78+
class ApiClientConfiguration(Configuration,BaseModel):
7779
"""This class contains various settings of the API client.
7880
7981
:param host: Base url.
@@ -212,6 +214,7 @@ def fromEnvPrefix(
212214
debug=debug,
213215
)
214216

217+
215218
__all__ = [
216219
"ACPClient",
217220
"AsyncACPClient",

examples/echo-agent/README.md

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# Echo Agent
2+
3+
The echo agent simply repeats input to output adding a new
4+
"assistant" message with a limited number of supported
5+
transformations of the last "human" type message:
6+
7+
* to-upper: convert text to upper case
8+
* to-lower: convert text to lower case
9+
10+
The agent is intended to be used for simple test cases where
11+
access to an LLM is either unsupported or undesired. The agent
12+
outputs a deterministic value based on the input to better
13+
support testable outcomes.
14+
15+
## Prerequisites
16+
17+
Before running the application, ensure you have the following:
18+
19+
- **Python 3.9 or higher**
20+
- [Poetry](https://python-poetry.org/)
21+
22+
## Running the Echo Agent directly
23+
24+
* Install dependencies
25+
```
26+
poetry install
27+
```
28+
29+
* Run the agent CLI using poetry
30+
```
31+
export TO_LOWER=true ; poetry run echo_agent --human "What is actually up?"
32+
{
33+
"messages": [
34+
{
35+
"type": "human",
36+
"content": "What is actually up?"
37+
},
38+
{
39+
"type": "assistant",
40+
"content": "what is actually up?"
41+
}
42+
]
43+
}
44+
```
45+
46+
## Running the Echo Agent using Agent Workflow Server
47+
48+
* Create an environment variable file if desired based on the example
49+
50+
* Make sure that the [workflow server manager](https://docs.agntcy.org/pages/agws/workflow_server_manager.html#getting-started) cli (wfsm) is added to your path
51+
52+
* Start the workflow server. Note the existing manifest
53+
assumes you are deploying from the directory with the `pyproject.toml`
54+
file for the echo agent. Adjust as needed.
55+
56+
```
57+
wfsm deploy --manifestPath deploy/echo-agent.json --envFilePath deploy/echo_agent_example.yaml
58+
```
59+
60+
Using the output of the logs to get the values for the
61+
`API_KEY`, `AGENT_ID`, and `WORKFLOW_SERVER_PORT`, we can
62+
make a request to the agent using `curl`:
63+
64+
```
65+
curl -s -H 'content-type: application/json' -H "x-api-key: ${API_KEY}" -d '{"agent_id": "'${AGENT_ID}'", "input": { "echo_input": { "messages": [ { "type": "human", "content": "What is up, Dude?" } ] } }, "config": { "configurable": { "to_upper": true } } }' http://127.0.0.1:${WORKFLOW_SERVER_PORT}/runs/wait
66+
```
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
{
2+
"metadata": {
3+
"ref": {
4+
"name": "org.agntcy.echo_agent",
5+
"version": "0.0.1"
6+
},
7+
"description": "Provides a test interface to echo input to output."
8+
},
9+
"specs": {
10+
"capabilities": {
11+
"threads": false,
12+
"interrupts": false,
13+
"callbacks": false
14+
},
15+
"input": {
16+
"$defs": {
17+
"Message": {
18+
"properties": {
19+
"type": {
20+
"$ref": "#/$defs/Type",
21+
"description": "indicates the originator of the message, a human or an assistant"
22+
},
23+
"content": {
24+
"description": "the content of the message",
25+
"title": "Content",
26+
"type": "string"
27+
}
28+
},
29+
"required": [
30+
"type",
31+
"content"
32+
],
33+
"title": "Message",
34+
"type": "object"
35+
},
36+
"Type": {
37+
"enum": [
38+
"human",
39+
"assistant",
40+
"ai"
41+
],
42+
"title": "Type",
43+
"type": "string"
44+
}
45+
},
46+
"properties": {
47+
"messages": {
48+
"anyOf": [
49+
{
50+
"items": {
51+
"$ref": "#/$defs/Message"
52+
},
53+
"type": "array"
54+
},
55+
{
56+
"type": "null"
57+
}
58+
],
59+
"default": null,
60+
"title": "Messages"
61+
}
62+
},
63+
"title": "InputState",
64+
"type": "object"
65+
},
66+
"output": {
67+
"$defs": {
68+
"Message": {
69+
"properties": {
70+
"type": {
71+
"$ref": "#/$defs/Type",
72+
"description": "indicates the originator of the message, a human or an assistant"
73+
},
74+
"content": {
75+
"description": "the content of the message",
76+
"title": "Content",
77+
"type": "string"
78+
}
79+
},
80+
"required": [
81+
"type",
82+
"content"
83+
],
84+
"title": "Message",
85+
"type": "object"
86+
},
87+
"Type": {
88+
"enum": [
89+
"human",
90+
"assistant",
91+
"ai"
92+
],
93+
"title": "Type",
94+
"type": "string"
95+
}
96+
},
97+
"properties": {
98+
"messages": {
99+
"anyOf": [
100+
{
101+
"items": {
102+
"$ref": "#/$defs/Message"
103+
},
104+
"type": "array"
105+
},
106+
{
107+
"type": "null"
108+
}
109+
],
110+
"default": null,
111+
"title": "Messages"
112+
}
113+
},
114+
"title": "OutputState",
115+
"type": "object"
116+
},
117+
"config": {
118+
"properties": {
119+
"to_upper": {
120+
"title": "to_upper",
121+
"description": "If true, convert text to upper before echoing.",
122+
"type": "boolean",
123+
"default": false
124+
},
125+
"to_lower": {
126+
"title": "to_lower",
127+
"description": "If true, convert text to lower before echoing.",
128+
"type": "boolean",
129+
"default": false
130+
}
131+
},
132+
"title": "ConfigSchema",
133+
"type": "object"
134+
}
135+
},
136+
"deployment": {
137+
"deployment_options": [
138+
{
139+
"type": "source_code",
140+
"name": "source_code_local",
141+
"url": ".",
142+
"framework_config": {
143+
"framework_type": "langgraph",
144+
"graph": "echo_agent.langgraph:AGENT_GRAPH"
145+
}
146+
}
147+
],
148+
"env_vars": [
149+
{
150+
"desc": "If true-ish, convert text to lower before echoing. Superseded by config.",
151+
"name": "TO_LOWER"
152+
},
153+
{
154+
"desc": "If true-ish, convert text to upper before echoing. Superseded by config.",
155+
"name": "TO_UPPER"
156+
}
157+
],
158+
"dependencies": []
159+
}
160+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
values:
2+
TO_UPPER: true
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
# Copyright AGNTCY Contributors (https://github.com/agntcy)
2+
# SPDX-License-Identifier: Apache-2.0
3+
import itertools
4+
import logging
5+
6+
import click
7+
from langchain_core.runnables import RunnableConfig
8+
9+
from .langgraph import AGENT_GRAPH
10+
from .state import AgentState, ConfigSchema, InputState, Message, MsgType
11+
12+
logger = logging.getLogger(__name__)
13+
14+
15+
class ParamMessage(click.ParamType):
16+
name = "message"
17+
18+
def __init__(self, **kwargs):
19+
self.msg_type = kwargs.pop("msg_type", MsgType.human)
20+
super().__init__(**kwargs)
21+
22+
def convert(self, value, param, ctx):
23+
try:
24+
return Message(type=self.msg_type, content=value)
25+
except ValueError:
26+
self.fail(f"{value!r} is not valid message content", param, ctx)
27+
28+
29+
@click.command(short_help="Validate agent ACP descriptor")
30+
@click.option(
31+
"--to-upper",
32+
envvar="TO_UPPER",
33+
is_flag=True,
34+
show_envvar=True,
35+
help="Convert input to upper case.",
36+
)
37+
@click.option(
38+
"--to-lower",
39+
envvar="TO_LOWER",
40+
is_flag=True,
41+
show_envvar=True,
42+
help="Convert input to lower case.",
43+
)
44+
@click.option(
45+
"--log-level",
46+
type=click.Choice(
47+
["critical", "error", "warning", "info", "debug"], case_sensitive=False
48+
),
49+
default="info",
50+
help="Set logging level.",
51+
)
52+
@click.option(
53+
"--human",
54+
type=ParamMessage(msg_type=MsgType.human),
55+
multiple=True,
56+
help="Add a human message.",
57+
)
58+
@click.option(
59+
"--assistant",
60+
type=ParamMessage(msg_type=MsgType.assistant),
61+
multiple=True,
62+
help="Add a human message.",
63+
)
64+
def echo_server_agent(
65+
to_upper,
66+
to_lower,
67+
human,
68+
assistant,
69+
log_level,
70+
):
71+
""" """
72+
logging.basicConfig(level=log_level.upper())
73+
74+
config = ConfigSchema()
75+
if to_upper is not None:
76+
config["to_upper"] = to_upper
77+
if to_lower is not None:
78+
config["to_lower"] = to_lower
79+
if human is not None and assistant is not None:
80+
# Interleave list starting with human. Stops at shortest list.
81+
messages = list(itertools.chain(*zip(human, assistant)))
82+
# Append rest of list.
83+
if len(human) > len(assistant):
84+
messages += human[len(assistant) :]
85+
elif len(assistant) > len(human):
86+
messages += assistant[len(human) :]
87+
elif human is not None:
88+
messages = human
89+
elif assistant is not None:
90+
messages = assistant
91+
else:
92+
messages = []
93+
94+
echo_input = InputState(messages=messages)
95+
logger.debug(f"input messages: {echo_input.model_dump_json()}")
96+
97+
# Imitate input from ACP API
98+
input_api_object = AgentState(echo_input=echo_input).model_dump(mode="json")
99+
100+
output_state = AGENT_GRAPH.invoke(
101+
AGENT_GRAPH.builder.schema.model_validate(input_api_object),
102+
config=RunnableConfig(configurable=config),
103+
)
104+
105+
logger.debug(f"output messages: {output_state}")
106+
print(output_state["echo_output"].model_dump_json(indent=2))
107+
108+
109+
if __name__ == "__main__":
110+
echo_server_agent() # type: ignore

0 commit comments

Comments
 (0)