Skip to content

Commit 7a37eae

Browse files
author
Andy
committed
feat: Add a travel planner powered by a custom-configured model.
1 parent 36bfdfd commit 7a37eae

File tree

10 files changed

+1486
-0
lines changed

10 files changed

+1486
-0
lines changed

examples/travel_planner/README.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# travel planner example
2+
> This is a Python implementation that adheres to the A2A (Assume to Answer) protocol.
3+
> It is a travel assistant in line with the specifications of the OpenAI model, capable of providing you with travel planning services.
4+
5+
## Getting started
6+
7+
1. Install the dependencies
8+
```bash
9+
cd ~/a2a-python/
10+
pip install .
11+
```
12+
13+
2. Start the server
14+
```bash
15+
uv run .
16+
```
17+
18+
3. Run the test client
19+
```bash
20+
uv run loop_client.py
21+
```
22+
23+
24+
## License
25+
26+
This project is licensed under the terms of the [Apache 2.0 License](/LICENSE).
27+
28+
## Contributing
29+
30+
See [CONTRIBUTING.md](/CONTRIBUTING.md) for contribution guidelines.
31+

examples/travel_planner/__init__.py

Whitespace-only changes.
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
from agent_executor import TravelPlannerAgentExecutor
2+
3+
from a2a.server.apps import A2AStarletteApplication
4+
from a2a.server.request_handlers import DefaultRequestHandler
5+
from a2a.server.tasks import InMemoryTaskStore
6+
from a2a.types import (
7+
AgentAuthentication,
8+
AgentCapabilities,
9+
AgentCard,
10+
AgentSkill,
11+
)
12+
13+
14+
if __name__ == '__main__':
15+
16+
skill = AgentSkill(
17+
id='travel_planner',
18+
name='travel planner agent',
19+
description='travel planner',
20+
tags=['travel planner'],
21+
examples=['hello', 'nice to meet you!'],
22+
)
23+
24+
agent_card = AgentCard(
25+
name='travel planner Agent',
26+
description='travel planner',
27+
url='http://localhost:9999/',
28+
version='1.0.0',
29+
defaultInputModes=['text'],
30+
defaultOutputModes=['text'],
31+
capabilities=AgentCapabilities(streaming=True),
32+
skills=[skill],
33+
authentication=AgentAuthentication(schemes=['public']),
34+
)
35+
36+
37+
request_handler = DefaultRequestHandler(
38+
agent_executor=TravelPlannerAgentExecutor(),
39+
task_store=InMemoryTaskStore(),
40+
)
41+
42+
server = A2AStarletteApplication(
43+
agent_card=agent_card, http_handler=request_handler
44+
)
45+
import uvicorn
46+
47+
uvicorn.run(server.build(), host='0.0.0.0', port=9999)

examples/travel_planner/agent.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
from langchain_openai import ChatOpenAI
2+
from langchain_core.messages import HumanMessage, SystemMessage
3+
import json,asyncio
4+
from collections.abc import AsyncGenerator
5+
6+
class TravelPlannerAgent:
7+
""" travel planner Agent."""
8+
9+
def __init__(self):
10+
"""Initialize the travel dialogue model"""
11+
try:
12+
with open("config.json") as f:
13+
config = json.load(f)
14+
self.model = ChatOpenAI(
15+
model=config["model_name"],
16+
base_url=config["base_url"],
17+
api_key=config["api_key"],
18+
temperature=0.7 # Control the generation randomness (0-2, higher values indicate greater randomness)
19+
)
20+
except FileNotFoundError:
21+
print("Error: The configuration file config.json cannot be found.")
22+
exit()
23+
except KeyError as e:
24+
print(f"The configuration file is missing required fields: {e}")
25+
exit()
26+
27+
async def stream(self, query: str) -> AsyncGenerator[str, None]:
28+
29+
"""Stream the response of the large model back to the client. """
30+
try:
31+
# Initialize the conversation history (system messages can be added)
32+
messages = [
33+
SystemMessage(
34+
content="""
35+
You are an expert travel assistant specializing in trip planning, destination information,
36+
and travel recommendations. Your goal is to help users plan enjoyable, safe, and
37+
realistic trips based on their preferences and constraints.
38+
39+
When providing information:
40+
- Be specific and practical with your advice
41+
- Consider seasonality, budget constraints, and travel logistics
42+
- Highlight cultural experiences and authentic local activities
43+
- Include practical travel tips relevant to the destination
44+
- Format information clearly with headings and bullet points when appropriate
45+
46+
For itineraries:
47+
- Create realistic day-by-day plans that account for travel time between attractions
48+
- Balance popular tourist sites with off-the-beaten-path experiences
49+
- Include approximate timing and practical logistics
50+
- Suggest meal options highlighting local cuisine
51+
- Consider weather, local events, and opening hours in your planning
52+
53+
Always maintain a helpful, enthusiastic but realistic tone and acknowledge
54+
any limitations in your knowledge when appropriate.
55+
"""
56+
)
57+
]
58+
59+
# Add the user message to the history.
60+
messages.append(HumanMessage(content=query))
61+
62+
# Invoke the model in streaming mode to generate a response.
63+
for chunk in self.model.stream(messages):
64+
# Return the text content block.
65+
if hasattr(chunk, 'content') and chunk.content:
66+
yield {'content': chunk.content, 'done': False}
67+
i = 1
68+
while i <= 100:
69+
yield {'content': str(i), 'done': False}
70+
i += 1
71+
await asyncio.sleep(0.5)
72+
yield {'content': '\n', 'done': True}
73+
74+
except Exception as e:
75+
print(f"error:{str(e)}")
76+
yield {'content': 'Sorry, an error occurred while processing your request.', 'done': True}
77+
78+
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
from uuid import uuid4
2+
from agent import TravelPlannerAgent
3+
4+
from typing_extensions import override
5+
6+
from a2a.types import (
7+
TaskArtifactUpdateEvent,
8+
TaskStatusUpdateEvent,
9+
)
10+
from a2a.server.agent_execution import AgentExecutor, RequestContext
11+
from a2a.server.events import EventQueue
12+
from a2a.utils import new_text_artifact
13+
14+
15+
class TravelPlannerAgentExecutor(AgentExecutor):
16+
"""travel planner AgentExecutor Example."""
17+
18+
def __init__(self):
19+
self.agent = TravelPlannerAgent()
20+
21+
@override
22+
async def execute(
23+
self,
24+
context: RequestContext,
25+
event_queue: EventQueue,
26+
) -> None:
27+
query = context.get_user_input()
28+
if not context.message:
29+
raise Exception('No message provided')
30+
31+
print(f'query:{query}')
32+
print('answer:')
33+
async for event in self.agent.stream(query):
34+
print(event['content'])
35+
message = TaskArtifactUpdateEvent(
36+
contextId=context.context_id,
37+
taskId=context.task_id,
38+
artifact=new_text_artifact(
39+
name='current_result',
40+
text=event['content'],
41+
),
42+
)
43+
event_queue.enqueue_event(message)
44+
45+
@override
46+
async def cancel(
47+
self, context: RequestContext, event_queue: EventQueue
48+
) -> None:
49+
raise Exception('cancel not supported')
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"model_name":"qwen3-32b",
3+
"api_key": "sk-74e8a7c32e2741ff892844597dcc31c1",
4+
"base_url": "https://dashscope.aliyuncs.com/compatible-mode/v1"
5+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
from a2a.client import A2AClient
2+
from typing import Any
3+
import httpx
4+
from uuid import uuid4
5+
import asyncio
6+
from a2a.types import (
7+
MessageSendParams,
8+
SendStreamingMessageRequest,
9+
)
10+
11+
def print_welcome_message() -> None:
12+
print("Welcome to the generic A2A client!")
13+
print("Please enter your query (type 'exit' to quit):")
14+
15+
def get_user_query() -> str:
16+
return input("\n> ")
17+
18+
async def interact_with_server(client: A2AClient) -> None:
19+
while True:
20+
user_input = get_user_query()
21+
if user_input.lower() == 'exit':
22+
print("bye!~")
23+
break
24+
25+
send_message_payload: dict[str, Any] = {
26+
'message': {
27+
'role': 'user',
28+
'parts': [
29+
{'type': 'text', 'text': user_input}
30+
],
31+
'messageId': uuid4().hex,
32+
},
33+
}
34+
35+
try:
36+
streaming_request = SendStreamingMessageRequest(
37+
params=MessageSendParams(**send_message_payload)
38+
)
39+
stream_response = client.send_message_streaming(streaming_request)
40+
async for chunk in stream_response:
41+
print(get_response_text(chunk), end='', flush=True)
42+
await asyncio.sleep(0.1)
43+
except Exception as e:
44+
print(f"An error occurred: {e}")
45+
46+
def get_response_text(chunk):
47+
data = chunk.model_dump(mode='json', exclude_none=True)
48+
return data['result']['artifact']['parts'][0]['text']
49+
50+
51+
async def main() -> None:
52+
print_welcome_message()
53+
async with httpx.AsyncClient() as httpx_client:
54+
client = await A2AClient.get_client_from_agent_card_url(
55+
httpx_client, 'http://localhost:9999'
56+
)
57+
await interact_with_server(client)
58+
59+
60+
if __name__ == '__main__':
61+
asyncio.run(main())
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
[project]
2+
name = "travel_planner"
3+
version = "0.1.0"
4+
description = "travel planner agent example that only returns Messages"
5+
readme = "README.md"
6+
requires-python = ">=3.13"
7+
dependencies = [
8+
"a2a",
9+
"click>=8.1.8",
10+
"dotenv>=0.9.9",
11+
"httpx>=0.28.1",
12+
"pydantic>=2.11.4",
13+
"python-dotenv>=1.1.0",
14+
"langchain-core>=0.2.31",
15+
"langchain-openai>=0.1.26",
16+
"langchain>=0.1.22",
17+
]
18+
19+
[tool.hatch.build.targets.wheel]
20+
packages = ["."]
21+
22+
[tool.uv.sources]
23+
a2a-sdk = { workspace = true }
24+
25+
[build-system]
26+
requires = ["hatchling"]
27+
build-backend = "hatchling.build"

0 commit comments

Comments
 (0)