Skip to content

Commit b6770a4

Browse files
authored
Merge pull request #136 from huang195/a2a-agents-by-claude-skills
3 a2a agents that were generated by claude code using skills
2 parents 55d6079 + d2a682a commit b6770a4

24 files changed

Lines changed: 3862 additions & 0 deletions

a2a/cheerup_agent/.dockerignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
__pycache__
2+
*.pyc
3+
.venv
4+
.git

a2a/cheerup_agent/Dockerfile

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim
2+
ARG RELEASE_VERSION="main"
3+
4+
WORKDIR /app
5+
COPY . .
6+
RUN uv sync --no-cache --locked --link-mode copy
7+
8+
ENV PRODUCTION_MODE=True \
9+
RELEASE_VERSION=${RELEASE_VERSION}
10+
11+
RUN chown -R 1001:1001 /app
12+
USER 1001
13+
14+
CMD ["uv", "run", "--no-sync", "server", "--host", "0.0.0.0", "--port", "8000"]

a2a/cheerup_agent/pyproject.toml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
[project]
2+
name = "cheerup-agent"
3+
version = "0.0.1"
4+
description = "A cheerful conversational agent that brightens your day."
5+
license = { text = "Apache" }
6+
requires-python = ">=3.11"
7+
dependencies = [
8+
"a2a-sdk[http-server]>=0.2.16",
9+
"openai>=1.0.0",
10+
"pydantic-settings>=2.8.1",
11+
"uvicorn>=0.30.0",
12+
"opentelemetry-exporter-otlp",
13+
]
14+
15+
[project.scripts]
16+
server = "cheerup_agent.agent:run"
17+
18+
[build-system]
19+
requires = ["hatchling"]
20+
build-backend = "hatchling.build"
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
from opentelemetry.sdk.resources import Resource
2+
from opentelemetry import trace
3+
from opentelemetry.sdk.trace import TracerProvider
4+
from opentelemetry.sdk.trace.export import BatchSpanProcessor
5+
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
6+
7+
def setup_tracer():
8+
resource = Resource.create(attributes={
9+
"service.name": "cheerup-agent",
10+
})
11+
provider = TracerProvider(resource=resource)
12+
processor = BatchSpanProcessor(OTLPSpanExporter())
13+
provider.add_span_processor(processor)
14+
trace.set_tracer_provider(provider)
15+
16+
setup_tracer()
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import logging
2+
3+
import uvicorn
4+
from starlette.requests import Request
5+
from starlette.responses import JSONResponse
6+
from starlette.routing import Route
7+
from textwrap import dedent
8+
9+
from a2a.server.agent_execution import AgentExecutor, RequestContext
10+
from a2a.server.apps import A2AStarletteApplication
11+
from a2a.server.events.event_queue import EventQueue
12+
from a2a.server.request_handlers import DefaultRequestHandler
13+
from a2a.server.tasks import InMemoryTaskStore, TaskUpdater
14+
from a2a.types import AgentCapabilities, AgentCard, AgentSkill, TaskState, TextPart
15+
from a2a.utils import new_agent_text_message, new_task
16+
17+
from cheerup_agent.cheerup_llm import chat
18+
19+
logging.basicConfig(level=logging.DEBUG)
20+
logger = logging.getLogger(__name__)
21+
22+
23+
def get_agent_card(host: str, port: int):
24+
"""Returns the Agent Card for the Cheerup Agent."""
25+
capabilities = AgentCapabilities(streaming=False)
26+
skill = AgentSkill(
27+
id="cheerup_companion",
28+
name="Cheerup Companion",
29+
description="**Cheerup Companion** - A warm, uplifting friend that brightens your day with humor, encouragement, and positive vibes.",
30+
tags=["cheerful", "motivation", "humor", "wellbeing"],
31+
examples=[
32+
"I'm having a rough day",
33+
"Tell me something to make me smile",
34+
"I need a pep talk",
35+
"Make me laugh!",
36+
],
37+
)
38+
return AgentCard(
39+
name="Cheerup Companion",
40+
description=dedent(
41+
"""\
42+
A cheerful conversational companion designed to brighten your day
43+
and put you in a great mood.
44+
45+
## What I can do
46+
- Share jokes and fun facts to make you smile
47+
- Give you a pep talk when you need encouragement
48+
- Celebrate your wins, big or small
49+
- Be a positive, supportive friend to chat with
50+
""",
51+
),
52+
url=f"http://{host}:{port}/",
53+
version="1.0.0",
54+
default_input_modes=["text"],
55+
default_output_modes=["text"],
56+
capabilities=capabilities,
57+
skills=[skill],
58+
)
59+
60+
61+
class CheerupExecutor(AgentExecutor):
62+
"""Handles cheerup agent execution for A2A."""
63+
64+
async def execute(self, context: RequestContext, event_queue: EventQueue):
65+
task = context.current_task
66+
if not task:
67+
task = new_task(context.message)
68+
await event_queue.enqueue_event(task)
69+
task_updater = TaskUpdater(event_queue, task.id, task.context_id)
70+
71+
user_input = context.get_user_input()
72+
logger.info("Cheerup agent received: %s (context=%s)", user_input, task.context_id)
73+
74+
await task_updater.update_status(
75+
TaskState.working,
76+
new_agent_text_message(
77+
"Brewing up some good vibes...",
78+
task_updater.context_id,
79+
task_updater.task_id,
80+
),
81+
)
82+
83+
try:
84+
reply = await chat(task.context_id, user_input)
85+
86+
parts = [TextPart(text=reply)]
87+
await task_updater.add_artifact(parts)
88+
await task_updater.update_status(
89+
TaskState.input_required,
90+
new_agent_text_message(
91+
reply,
92+
task_updater.context_id,
93+
task_updater.task_id,
94+
),
95+
)
96+
except Exception as e:
97+
logger.error("Cheerup agent error: %s", e)
98+
parts = [TextPart(text="Sorry, something went wrong. Please try again.")]
99+
await task_updater.add_artifact(parts)
100+
await task_updater.failed()
101+
102+
async def cancel(self, context: RequestContext, event_queue: EventQueue) -> None:
103+
raise Exception("cancel not supported")
104+
105+
106+
async def health(request: Request) -> JSONResponse:
107+
return JSONResponse({"status": "ok"})
108+
109+
110+
async def agent_card_compat(request: Request) -> JSONResponse:
111+
"""Serve agent card at /.well-known/agent-card.json for Kagenti backend compatibility."""
112+
card = get_agent_card(host="0.0.0.0", port=8000)
113+
return JSONResponse(card.model_dump(mode="json", exclude_none=True))
114+
115+
116+
def run():
117+
"""Runs the A2A Agent application."""
118+
agent_card = get_agent_card(host="0.0.0.0", port=8000)
119+
120+
request_handler = DefaultRequestHandler(
121+
agent_executor=CheerupExecutor(),
122+
task_store=InMemoryTaskStore(),
123+
)
124+
125+
server = A2AStarletteApplication(
126+
agent_card=agent_card,
127+
http_handler=request_handler,
128+
)
129+
130+
app = server.build()
131+
132+
# Add custom routes
133+
app.routes.insert(0, Route("/health", health, methods=["GET"]))
134+
app.routes.insert(0, Route("/.well-known/agent-card.json", agent_card_compat, methods=["GET"]))
135+
136+
uvicorn.run(app, host="0.0.0.0", port=8000)
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import logging
2+
from collections import defaultdict
3+
4+
from openai import AsyncOpenAI
5+
6+
from cheerup_agent.configuration import Configuration
7+
8+
logger = logging.getLogger(__name__)
9+
10+
SYSTEM_PROMPT = (
11+
"You are a warm, uplifting, and genuinely cheerful companion. Your sole "
12+
"purpose is to brighten the user's day and put them in a great mood.\n\n"
13+
"Guidelines:\n"
14+
"- Be enthusiastic, empathetic, and positive without being fake or dismissive.\n"
15+
"- If the user shares something negative, acknowledge their feelings first, "
16+
"then gently help them find a silver lining or shift perspective.\n"
17+
"- Share fun facts, lighthearted jokes, encouraging words, or playful "
18+
"observations when appropriate.\n"
19+
"- Celebrate even small wins the user mentions.\n"
20+
"- Use a conversational, friendly tone — like a supportive best friend.\n"
21+
"- Keep responses concise and punchy — aim for warmth, not walls of text.\n"
22+
"- If the user seems down, ask what might help: a joke, a compliment, "
23+
"a pep talk, or just someone to listen.\n"
24+
"- Sprinkle in creative compliments and affirmations naturally.\n"
25+
"- Remember context from earlier in the conversation to make it personal.\n"
26+
)
27+
28+
# Conversation memory keyed by context_id
29+
# NOTE: In production, add eviction or size limits to prevent unbounded memory growth.
30+
_conversations: dict[str, list[dict[str, str]]] = defaultdict(list)
31+
32+
33+
async def chat(context_id: str, user_message: str) -> str:
34+
"""Send a message and get a cheerful response, maintaining conversation history."""
35+
config = Configuration()
36+
37+
client = AsyncOpenAI(
38+
base_url=config.llm_api_base,
39+
api_key=config.llm_api_key,
40+
)
41+
42+
history = _conversations[context_id]
43+
history.append({"role": "user", "content": user_message})
44+
45+
messages = [{"role": "system", "content": SYSTEM_PROMPT}] + history
46+
47+
logger.info("Sending %d messages to LLM for context %s", len(messages), context_id)
48+
49+
response = await client.chat.completions.create(
50+
model=config.llm_model,
51+
messages=messages,
52+
)
53+
54+
assistant_message = response.choices[0].message.content
55+
history.append({"role": "assistant", "content": assistant_message})
56+
57+
logger.info("LLM response for context %s: %s", context_id, assistant_message[:200])
58+
return assistant_message
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from pydantic_settings import BaseSettings
2+
3+
class Configuration(BaseSettings):
4+
llm_model: str = "qwen3:4b"
5+
llm_api_base: str = "http://host.docker.internal:11434/v1"
6+
llm_api_key: str = "dummy"

0 commit comments

Comments
 (0)