Skip to content

Commit 6b6bfe7

Browse files
authored
feat: add storage support to claude and image generator agents (#74)
1 parent aabfd2c commit 6b6bfe7

File tree

12 files changed

+414
-66
lines changed

12 files changed

+414
-66
lines changed

6-deployed-agents/knowledge-base/claude.ai-agent/agent.py renamed to 6-deployed-agents/knowledge-base/claude-ai-agent/agent.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@
22
from enum import Enum
33
from typing import Any
44

5-
from ai import get_completion, get_structured_response
6-
from chat_proto import chat_proto
75
from uagents import Agent, Context, Model
86
from uagents.experimental.quota import AccessControlList, QuotaProtocol, RateLimit
97
from uagents_core.models import ErrorMessage
108

9+
from ai import get_structured_response, get_text_completion
10+
from chat_proto import chat_proto
11+
1112
AGENT_SEED = os.getenv("AGENT_SEED", "claude-test-agent")
1213
AGENT_NAME = os.getenv("AGENT_NAME", "Claude.ai Agent")
1314
BYPASS_RATE_LIMIT = set(
@@ -62,7 +63,7 @@ class StructuredOutputResponse(Model):
6263

6364
@proto.on_message(TextPrompt, replies={TextResponse, ErrorMessage})
6465
async def handle_request(ctx: Context, sender: str, msg: TextPrompt):
65-
response = get_completion(msg.text)
66+
response = get_text_completion(msg.text)
6667
if response is None:
6768
await ctx.send(
6869
sender,

6-deployed-agents/knowledge-base/claude.ai-agent/ai.py renamed to 6-deployed-agents/knowledge-base/claude-ai-agent/ai.py

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
raise ValueError(
1212
"You need to provide an API key: https://platform.openai.com/api-keys"
1313
)
14-
MODEL_ENGINE = os.getenv("MODEL_ENGINE", "claude-3-haiku-20240307")
14+
MODEL_ENGINE = os.getenv("MODEL_ENGINE", "claude-3-5-haiku-latest")
1515
HEADERS = {
1616
"x-api-key": ANTHROPIC_API_KEY,
1717
"anthropic-version": "2023-06-01",
@@ -25,7 +25,7 @@
2525

2626

2727
def create_structured_response_tool(
28-
response_model_schema: dict[str, Any]
28+
response_model_schema: dict[str, Any],
2929
) -> dict[str, Any]:
3030

3131
# Exclude the title from the schema (not allowed in the API)
@@ -40,13 +40,26 @@ def create_structured_response_tool(
4040
}
4141

4242

43+
def get_text_completion(prompt: str, tool: dict[str, Any] | None = None) -> str | None:
44+
content = [{"type": "text", "text": prompt}]
45+
return get_completion(content, tool)
46+
47+
4348
# Send a prompt to the AI model and return the content of the completion
44-
def get_completion(prompt: str, tool: dict[str, Any] | None = None) -> str | None:
49+
def get_completion(
50+
content: list[dict[str, Any]], tool: dict[str, Any] | None = None
51+
) -> str | None:
4552
data = {
4653
"model": MODEL_ENGINE,
4754
"max_tokens": MAX_TOKENS,
48-
"messages": [{"role": "user", "content": prompt}],
55+
"messages": [
56+
{
57+
"role": "user",
58+
"content": content,
59+
}
60+
],
4961
}
62+
5063
if tool:
5164
data["tools"] = [tool]
5265
data["tool_choice"] = {"type": "tool", "name": tool["name"]}
@@ -55,23 +68,34 @@ def get_completion(prompt: str, tool: dict[str, Any] | None = None) -> str | Non
5568
response = requests.post(
5669
CLAUDE_URL, headers=HEADERS, data=json.dumps(data), timeout=120
5770
)
71+
response.raise_for_status()
5872
except requests.exceptions.Timeout:
5973
return "The request timed out. Please try again."
6074
except requests.exceptions.RequestException as e:
6175
return f"An error occurred: {e}"
6276

77+
# Check if the response was successful
78+
response_data = response.json()
79+
80+
# Handle error responses
81+
if "error" in response_data:
82+
return f"API Error: {response_data['error'].get('message', 'Unknown error')}"
83+
6384
if tool:
64-
for item in response.json()["content"]:
85+
for item in response_data["content"]:
6586
if item["type"] == "tool_use":
6687
return item["input"]
88+
89+
messages = response_data["content"]
6790

68-
messages = response.json()["content"]
69-
message = messages[0]["text"]
70-
return message
91+
if messages:
92+
return messages[0]["text"]
93+
else:
94+
return None
7195

7296

7397
def get_structured_response(
7498
prompt: str, response_model_schema: dict[str, Any]
7599
) -> dict[str, Any] | None:
76100
tool = create_structured_response_tool(response_model_schema)
77-
return get_completion(prompt, tool)
101+
return get_text_completion(prompt, tool)
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import os
2+
from datetime import datetime
3+
from uuid import uuid4
4+
5+
from uagents import Context, Protocol
6+
from uagents_core.contrib.protocols.chat import (
7+
ChatAcknowledgement,
8+
ChatMessage,
9+
MetadataContent,
10+
ResourceContent,
11+
StartSessionContent,
12+
TextContent,
13+
chat_protocol_spec,
14+
)
15+
from uagents_core.storage import ExternalStorage
16+
17+
from ai import get_completion
18+
19+
STORAGE_URL = os.getenv("AGENTVERSE_URL", "https://agentverse.ai") + "/v1/storage"
20+
21+
22+
def create_text_chat(text: str) -> ChatMessage:
23+
return ChatMessage(
24+
timestamp=datetime.utcnow(),
25+
msg_id=uuid4(),
26+
content=[TextContent(type="text", text=text)],
27+
)
28+
29+
30+
def create_metadata(metadata: dict[str, str]) -> ChatMessage:
31+
return ChatMessage(
32+
timestamp=datetime.utcnow(),
33+
msg_id=uuid4(),
34+
content=[MetadataContent(
35+
type="metadata",
36+
metadata=metadata,
37+
)],
38+
)
39+
40+
41+
chat_proto = Protocol(spec=chat_protocol_spec)
42+
43+
44+
@chat_proto.on_message(ChatMessage)
45+
async def handle_message(ctx: Context, sender: str, msg: ChatMessage):
46+
ctx.logger.info(f"Got a message from {sender}")
47+
await ctx.send(
48+
sender,
49+
ChatAcknowledgement(
50+
timestamp=datetime.utcnow(), acknowledged_msg_id=msg.msg_id
51+
),
52+
)
53+
ctx.storage.set(str(ctx.session), sender)
54+
prompt_content = []
55+
for item in msg.content:
56+
if isinstance(item, StartSessionContent):
57+
await ctx.send(sender, create_metadata({"attachments": "true"}))
58+
elif isinstance(item, TextContent):
59+
prompt_content.append({"text": item.text, "type": "text"})
60+
elif isinstance(item, ResourceContent):
61+
try:
62+
external_storage = ExternalStorage(
63+
identity=ctx.agent.identity,
64+
storage_url=STORAGE_URL,
65+
)
66+
data = external_storage.download(str(item.resource_id))
67+
if "image" in data["mime_type"]:
68+
prompt_content.append(
69+
{
70+
"type": "image",
71+
"source": {
72+
"type": "base64",
73+
"media_type": data["mime_type"],
74+
"data": data["contents"],
75+
},
76+
}
77+
)
78+
else:
79+
ctx.logger.warning(
80+
f"Got unexpected resource type: {data['mime_type']}"
81+
)
82+
except Exception as ex:
83+
ctx.logger.error(f"Failed to download resource: {ex}")
84+
await ctx.send(sender, create_text_chat("Failed to download resource."))
85+
else:
86+
ctx.logger.warning(f"Got unexpected content from {sender}")
87+
88+
if prompt_content:
89+
response = get_completion(prompt_content)
90+
await ctx.send(sender, create_text_chat(response))
91+
92+
93+
@chat_proto.on_message(ChatAcknowledgement)
94+
async def handle_ack(ctx: Context, sender: str, msg: ChatAcknowledgement):
95+
ctx.logger.info(
96+
f"Got an acknowledgement from {sender} for {msg.acknowledged_msg_id}"
97+
)

6-deployed-agents/knowledge-base/claude.ai-agent/pyproject.toml renamed to 6-deployed-agents/knowledge-base/claude-ai-agent/pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ packages = [{ include = "claude" }]
88

99
[tool.poetry.dependencies]
1010
python = "^3.10,<3.13"
11-
uagents = "^0.22.0"
11+
uagents = "^0.22.4"
12+
uagents-core = "^0.3.1"
1213
requests = "^2.32.3"
1314

1415

6-deployed-agents/knowledge-base/claude.ai-agent/chat_proto.py

Lines changed: 0 additions & 53 deletions
This file was deleted.
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import os
2+
from enum import Enum
3+
4+
from uagents import Agent, Context, Model
5+
from uagents.experimental.quota import QuotaProtocol, RateLimit
6+
from uagents_core.models import ErrorMessage
7+
8+
from chat_proto import chat_proto
9+
from models import ImageRequest, ImageResponse, generate_image
10+
11+
AGENT_SEED = os.getenv("AGENT_SEED", "image-generator-agent")
12+
AGENT_NAME = os.getenv("AGENT_NAME", "Image Generator Agent")
13+
14+
PORT = 8000
15+
agent = Agent(
16+
name=AGENT_NAME,
17+
seed=AGENT_SEED,
18+
port=PORT,
19+
endpoint=f"http://localhost:{PORT}/submit",
20+
)
21+
22+
proto = QuotaProtocol(
23+
storage_reference=agent.storage,
24+
name="Image-Generator",
25+
version="0.1.0",
26+
default_rate_limit=RateLimit(window_size_minutes=60, max_requests=6),
27+
)
28+
29+
@proto.on_message(ImageRequest, replies={ImageResponse, ErrorMessage})
30+
async def handle_request(ctx: Context, sender: str, msg: ImageRequest):
31+
ctx.logger.info(f"Received Image request")
32+
try:
33+
image_url = generate_image(msg.image_description)
34+
35+
except Exception as err:
36+
ctx.logger.error(err)
37+
await ctx.send(
38+
sender,
39+
ErrorMessage(
40+
error="An error occurred while processing the request. Please try again later."
41+
),
42+
)
43+
return
44+
await ctx.send(sender, ImageResponse(image_url=image_url))
45+
46+
47+
agent.include(proto, publish_manifest=True)
48+
49+
50+
### Health check related code
51+
def agent_is_healthy() -> bool:
52+
"""
53+
Implement the actual health check logic here.
54+
55+
For example, check if the agent can connect to a third party API,
56+
check if the agent has enough resources, etc.
57+
"""
58+
condition = True # TODO: logic here
59+
return bool(condition)
60+
61+
62+
class HealthCheck(Model):
63+
pass
64+
65+
66+
class HealthStatus(str, Enum):
67+
HEALTHY = "healthy"
68+
UNHEALTHY = "unhealthy"
69+
70+
71+
class AgentHealth(Model):
72+
agent_name: str
73+
status: HealthStatus
74+
75+
76+
health_protocol = QuotaProtocol(
77+
storage_reference=agent.storage, name="HealthProtocol", version="0.1.0"
78+
)
79+
80+
81+
@health_protocol.on_message(HealthCheck, replies={AgentHealth})
82+
async def handle_health_check(ctx: Context, sender: str, msg: HealthCheck):
83+
status = HealthStatus.UNHEALTHY
84+
try:
85+
if agent_is_healthy():
86+
status = HealthStatus.HEALTHY
87+
except Exception as err:
88+
ctx.logger.error(err)
89+
finally:
90+
await ctx.send(sender, AgentHealth(agent_name=AGENT_NAME, status=status))
91+
92+
93+
agent.include(health_protocol, publish_manifest=True)
94+
agent.include(chat_proto, publish_manifest=True)
95+
96+
97+
if __name__ == "__main__":
98+
agent.run()

0 commit comments

Comments
 (0)