From 0c05a9e52d18b3e9ec2c700e3869e5f98cfb983d Mon Sep 17 00:00:00 2001 From: Chan01mc Date: Fri, 16 Jan 2026 01:09:28 +0800 Subject: [PATCH 1/6] Add files via upload --- docs/did_public/did_a.json | 25 +++++++++++++++++++++++++ docs/did_public/did_b.json | 25 +++++++++++++++++++++++++ docs/did_public/private_a.pem | 3 +++ docs/did_public/private_b.pem | 3 +++ 4 files changed, 56 insertions(+) create mode 100644 docs/did_public/did_a.json create mode 100644 docs/did_public/did_b.json create mode 100644 docs/did_public/private_a.pem create mode 100644 docs/did_public/private_b.pem diff --git a/docs/did_public/did_a.json b/docs/did_public/did_a.json new file mode 100644 index 0000000..0ab504b --- /dev/null +++ b/docs/did_public/did_a.json @@ -0,0 +1,25 @@ +{ + "@context": [ + "https://www.w3.org/ns/did/v1", + "https://w3id.org/security/suites/jws-2020/v1", + "https://w3id.org/security/suites/secp256k1-2019/v1" + ], + "id": "did:wba:didhost.cc:chata", + "verificationMethod": [ + { + "id": "did:wba:didhost.cc:chata#key-1", + "type": "EcdsaSecp256k1VerificationKey2019", + "controller": "did:wba:didhost.cc:chata", + "publicKeyJwk": { + "kty": "EC", + "crv": "secp256k1", + "x": "f8K7HqZVXyLmQnRtYsUvWxZaBcDeFgHiJkLmNoPqRsTu", + "y": "A1B2C3D4E5F6G7H8I9J0K1L2M3N4O5P6Q7R8S9T0U1V2", + "kid": "chata-key-2026-01" + } + } + ], + "authentication": [ + "did:wba:didhost.cc:chata#key-1" + ] +} \ No newline at end of file diff --git a/docs/did_public/did_b.json b/docs/did_public/did_b.json new file mode 100644 index 0000000..07ec232 --- /dev/null +++ b/docs/did_public/did_b.json @@ -0,0 +1,25 @@ +{ + "@context": [ + "https://www.w3.org/ns/did/v1", + "https://w3id.org/security/suites/jws-2020/v1", + "https://w3id.org/security/suites/secp256k1-2019/v1" + ], + "id": "did:wba:didhost.cc:chatb", + "verificationMethod": [ + { + "id": "did:wba:didhost.cc:chatb#key-1", + "type": "EcdsaSecp256k1VerificationKey2019", + "controller": "did:wba:didhost.cc:chatb", + "publicKeyJwk": { + "kty": "EC", + "crv": "secp256k1", + "x": "qRsTuVwXyZaBcDeFgHiJkLmNoPqRsTuVwXyZaBcDeFgH", + "y": "Z9Y8X7W6V5U4T3S2R1Q0P9O8N7M6L5K4J3H2G1F0E9D8", + "kid": "chatb-main-key-2026" + } + } + ], + "authentication": [ + "did:wba:didhost.cc:chatb#key-1" + ] +} \ No newline at end of file diff --git a/docs/did_public/private_a.pem b/docs/did_public/private_a.pem new file mode 100644 index 0000000..489fa3d --- /dev/null +++ b/docs/did_public/private_a.pem @@ -0,0 +1,3 @@ +-----BEGIN PRIVATE KEY----- +MC4CAQAwBQYDK2VwBCIEIDj4yYy4H3DjZ1jXmZ9nY6vXlHq6Nz4vLx9uXKJ5pH0s +-----END PRIVATE KEY----- \ No newline at end of file diff --git a/docs/did_public/private_b.pem b/docs/did_public/private_b.pem new file mode 100644 index 0000000..129de91 --- /dev/null +++ b/docs/did_public/private_b.pem @@ -0,0 +1,3 @@ +-----BEGIN PRIVATE KEY----- +MC4CAQAwBQYDK2VwBCIEIHv4yYy4H3DjZ1jXmZ9nY6vXlHq6Nz4vLx9uXKJ5pH0t +-----END PRIVATE KEY----- \ No newline at end of file From 47df50adcc1fb25d82e6abb9e08f83afb57c89de Mon Sep 17 00:00:00 2001 From: Chan01mc Date: Fri, 16 Jan 2026 01:10:42 +0800 Subject: [PATCH 2/6] Add files via upload --- examples/python/openanp_examples/ChatA.py | 403 ++++++++++++++++++++++ examples/python/openanp_examples/ChatB.py | 397 +++++++++++++++++++++ 2 files changed, 800 insertions(+) create mode 100644 examples/python/openanp_examples/ChatA.py create mode 100644 examples/python/openanp_examples/ChatB.py diff --git a/examples/python/openanp_examples/ChatA.py b/examples/python/openanp_examples/ChatA.py new file mode 100644 index 0000000..63526af --- /dev/null +++ b/examples/python/openanp_examples/ChatA.py @@ -0,0 +1,403 @@ +import time +import asyncio +import uuid +from openai import OpenAI +import os +from typing import Optional, Any +from contextlib import asynccontextmanager +from anp.openanp import anp_agent, interface, AgentConfig, RemoteAgent +from anp.authentication import DIDWbaAuthHeader +from cryptography.hazmat.primitives.asymmetric import ec +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey +from fastapi import FastAPI, Body +import uvicorn + +AGENT_A_DID = "did:wba:example.com:chata" +AGENT_A_NAME = "ChatA" + +PEER_AD_URL = os.getenv("CHAT_PEER_AD_URL", "http://localhost:8001/b/ad.json") + +AUTO_DISCOVER = (os.getenv("CHAT_AUTO_DISCOVER", "1").strip().lower() not in {"0", "false", "no"}) +AUTO_DISCOVER_MAX_TRIES = int(os.getenv("CHAT_AUTO_DISCOVER_MAX_TRIES", "30").strip() or "30") +AUTO_DISCOVER_INTERVAL_SEC = float(os.getenv("CHAT_AUTO_DISCOVER_INTERVAL_SEC", "1").strip() or "1") + +AUTO_START_CHAT = (os.getenv("CHAT_AUTO_START", "1").strip().lower() not in {"0", "false", "no"}) +AUTO_CHAT_TURNS = int(os.getenv("CHAT_AUTO_TURNS", "4").strip() or "4") + +DISCOVER_TIE_TOLERANCE_SEC = float(os.getenv("CHAT_DISCOVER_TIE_TOLERANCE_SEC", "0.5").strip() or "0.5") + +API_KEY = os.getenv("OPENAI_KEY") +BASE_URL = os.getenv("OPENAI_API_BASE") +MODEL_NAME = "deepseek-chat" + +_client = None + + +def _get_client(): + global _client + if _client is not None: + return _client + if not API_KEY: + raise RuntimeError("missing OPENAI_KEY") + if BASE_URL: + _client = OpenAI(base_url=BASE_URL, api_key=API_KEY) + else: + _client = OpenAI(api_key=API_KEY) + return _client + +try: + from anp.authentication.did_wba_authenticator import DIDWbaAuthHeader as _LibDIDWbaAuthHeader + + def _load_private_key_compat(self): + key_path = self.private_key_path + with open(key_path, 'rb') as f: + private_key_data = f.read() + + try: + return serialization.load_pem_private_key(private_key_data, password=None) + except Exception: + return serialization.load_ssh_private_key(private_key_data, password=None) + + def _sign_callback_compat(self, content: bytes, method_fragment: str) -> bytes: + private_key = self._load_private_key() + if isinstance(private_key, Ed25519PrivateKey): + return private_key.sign(content) + return private_key.sign(content, ec.ECDSA(hashes.SHA256())) + + _LibDIDWbaAuthHeader._load_private_key = _load_private_key_compat + _LibDIDWbaAuthHeader._sign_callback = _sign_callback_compat +except Exception: + pass + + +auth = DIDWbaAuthHeader( + did_document_path="./did_a.json", + private_key_path="./private_a.pem" +) + +@anp_agent(AgentConfig( + name=AGENT_A_NAME, + did=AGENT_A_DID, + prefix="/a", +)) +class ChatAgentA: + def __init__(self, auth: DIDWbaAuthHeader): + self.auth = auth + self.message_count = 0 + self.sent_count = 0 + self.connected_agents = set() + self.peer: Any = None + self.peer_name: Optional[str] = None + self.first_discover_ts: Optional[float] = None + self._chat_lock = asyncio.Lock() + self._active_session_id: Optional[str] = None + self._chat_task: Optional[asyncio.Task] = None + self._auto_start_attempted = False + print("Intialized ChatAgentA") + + def _log_connected_once(self, agent_name: str) -> None: + name = (agent_name or "").strip() or "Unknown" + if name == "Unknown": + return + if name in self.connected_agents: + return + self.connected_agents.add(name) + print(f"\nChatA: 成功连接 {name}") + + @interface + async def notify_connected(self, agent: str) -> dict: + """ANP Interface: 由对端在 discover/连接后主动通知,用于日志展示""" + agent_name = (agent or "").strip() or "Unknown" + self._log_connected_once(agent_name) + if agent_name and agent_name != "Unknown": + self.peer_name = agent_name + return {"ok": True, "agent": "ChatA", "connected": agent_name} + + def _llm_generate(self, prompt: str) -> str: + if not API_KEY: + return "你好,我们开始聊天吧。" + + system_prompt = ( + "你是智能体 ChatA。你的任务是与对端智能体进行简短对话。" + "每次只输出一句要发给对端的中文消息,简洁自然。" + "不要输出解释、不要带前缀。" + ) + + resp = _get_client().chat.completions.create( + model=MODEL_NAME, + messages=[ + {"role": "system", "content": system_prompt}, + {"role": "user", "content": prompt}, + ], + temperature=0.8, + ) + content = resp.choices[0].message.content + return (content or "").strip() or "(空消息)" + + async def _run_chat_as_initiator(self, turns: int): + if self.peer is None: + return + peer_label = self.peer_name or getattr(self.peer, "name", None) or "Peer" + remaining_turns = int(turns) + next_message = self._llm_generate("请你主动向对端打个招呼并开启对话。") + + while remaining_turns > 0: + print(f"\nChatA -> {peer_label}: {next_message}") + self.sent_count += 1 + + try: + response = await self.peer.receive_message(message=next_message, remaining_turns=remaining_turns) + except Exception as e: + print(f"ChatA: 调用对端失败: {str(e)}") + return + + peer_reply = (response or {}).get("reply", "") + if peer_reply: + print(f"{peer_label} -> ChatA: {peer_reply}") + else: + print(f"ChatA: 未收到对端 reply,原始响应: {response}") + + remaining_turns = int((response or {}).get("remaining_turns", 0)) + if remaining_turns <= 0: + print("\nChatA: 对话结束") + return + + next_message = self._llm_generate(f"对端说:{peer_reply}\n你回复对端一句话。") + + @interface + async def propose_chat(self, initiator_did: str, initiator_discover_ts: float, session_id: str, turns: int = 4) -> dict: + """ANP Interface: 对端请求由其发起聊天。""" + initiator = (initiator_did or "").strip() + sid = (session_id or "").strip() + if not initiator or not sid: + return {"accepted": False, "reason": "missing_params"} + + async with self._chat_lock: + if self._active_session_id is not None: + return {"accepted": False, "reason": "already_active", "session_id": self._active_session_id} + + local_ts = self.first_discover_ts + if local_ts is not None: + diff = float(initiator_discover_ts) - float(local_ts) + if diff > DISCOVER_TIE_TOLERANCE_SEC: + # 我方明显更早 discover:拒绝,让我方发起 + return {"accepted": False, "reason": "i_discovered_first", "winner": AGENT_A_DID} + if abs(diff) <= DISCOVER_TIE_TOLERANCE_SEC: + # 近似同时:用 DID 做确定性裁决 + if AGENT_A_DID < initiator: + return {"accepted": False, "reason": "tie_break", "winner": AGENT_A_DID} + + self._active_session_id = sid + return {"accepted": True, "session_id": sid, "turns": int(turns)} + + async def maybe_start_chat_if_discovered_first(self, turns: int) -> None: + if not AUTO_START_CHAT: + return + if self.peer is None or self.first_discover_ts is None: + return + + async with self._chat_lock: + if self._active_session_id is not None: + return + if self._chat_task is not None and not self._chat_task.done(): + return + + sid = str(uuid.uuid4()) + peer_label = self.peer_name or getattr(self.peer, "name", None) or "Peer" + try: + resp = await self.peer.propose_chat( + initiator_did=AGENT_A_DID, + initiator_discover_ts=float(self.first_discover_ts), + session_id=sid, + turns=int(turns), + ) + except Exception as e: + print(f"ChatA: 向 {peer_label} 发起聊天失败: {str(e)}") + return + + if not (resp or {}).get("accepted"): + return + + async with self._chat_lock: + if self._active_session_id is None: + self._active_session_id = sid + if self._chat_task is None or self._chat_task.done(): + self._chat_task = asyncio.create_task(self._run_chat_as_initiator(turns=int(turns))) + + async def ensure_peer_connection(self, peer_ad_url: Optional[str] = None) -> bool: + """discover 对端(ChatB)并缓存 RemoteAgent""" + if self.peer is not None: + return True + + url = (peer_ad_url or "").strip() or PEER_AD_URL + try: + self.peer = await RemoteAgent.discover(url, self.auth) + self.peer_name = getattr(self.peer, "name", None) or self.peer_name + if self.first_discover_ts is None: + self.first_discover_ts = time.time() + self._log_connected_once(self.peer_name or "Unknown") + try: + await self.peer.notify_connected(agent=AGENT_A_NAME) + except Exception as e: + print(f" ChatA: 已连接但通知对端失败: {str(e)}") + return True + except Exception as e: + print(f" ChatA: discover 对端失败: {str(e)}") + self.peer = None + return False + + def _llm_reply(self, user_message: str) -> str: + if not API_KEY: + return "(ChatA 未配置 OPENAI_KEY,无法调用模型)" + + system_prompt = ( + "你是一个通过 ANP 接口对话的智能体 ChatA。" + "你需要用中文、简洁、自然地回复对方的消息。" + "不要输出多余的元信息。" + ) + + resp = _get_client().chat.completions.create( + model=MODEL_NAME, + messages=[ + {"role": "system", "content": system_prompt}, + {"role": "user", "content": user_message}, + ], + temperature=0.7, + ) + content = resp.choices[0].message.content + return (content or "").strip() or "(空回复)" + + @interface + async def status(self) -> dict: + return { + "agent": "ChatA", + "did": AGENT_A_DID, + "messages_received": self.message_count, + "messages_sent": self.sent_count, + "peer_connected": self.peer is not None, + "peer_name": self.peer_name, + } + + @interface + async def receive_message(self, message: str, remaining_turns: int) -> dict: + """ANP Interface: 接收消息并用模型回复""" + self.message_count += 1 + sender = self.peer_name or getattr(self.peer, "name", None) or "Peer" + print(f"\n{sender} -> ChatA: {message}") + + try: + reply = self._llm_reply(message) + except Exception as e: + reply = f"(ChatA 调用模型失败:{str(e)})" + + print(f"ChatA -> ChatB: {reply}") + + new_remaining_turns = max(0, int(remaining_turns) - 1) + if new_remaining_turns <= 0: + print("\nChatA: 对话结束") + + return { + "agent": "ChatA", + "reply": reply, + "remaining_turns": new_remaining_turns, + } + + async def send_message(self, message: str, remaining_turns: int = 4, peer_ad_url: Optional[str] = None) -> dict: + """主动向对端发送消息""" + ok = await self.ensure_peer_connection(peer_ad_url=peer_ad_url) + if not ok or self.peer is None: + return {"ok": False, "error": "peer_not_connected"} + + self.sent_count += 1 + try: + return await self.peer.receive_message(message=message, remaining_turns=int(remaining_turns)) + except Exception as e: + return {"ok": False, "error": f"call_peer_failed: {str(e)}"} + +@asynccontextmanager +async def lifespan(app: FastAPI): + app.state.start_time = time.time() + print("\n" + "=" * 60) + print("启动 Chat Agent A (端口 8000)") + print(" • 访问 http://localhost:8000 查看状态") + print(" • 访问 http://localhost:8000/a/ad.json 查看广告") + print(" • 访问 http://localhost:8000/health 进行健康检查") + print(" • 访问 http://localhost:8000/p2p/discover 进行 P2P discover") + print(" • 访问 http://localhost:8000/p2p/send 主动发送消息") + print("=" * 60 + "\n") + + if AUTO_DISCOVER: + async def _auto_discover_loop(): + for _ in range(max(1, AUTO_DISCOVER_MAX_TRIES)): + if await chat_agent_a.ensure_peer_connection(): + if not chat_agent_a._auto_start_attempted: + chat_agent_a._auto_start_attempted = True + await chat_agent_a.maybe_start_chat_if_discovered_first(turns=AUTO_CHAT_TURNS) + return + await asyncio.sleep(max(0.1, AUTO_DISCOVER_INTERVAL_SEC)) + + asyncio.create_task(_auto_discover_loop()) + + yield + + +app = FastAPI(title="ChatAgentA", description="Chat Agent A - 端口 8000", lifespan=lifespan) + +chat_agent_a = ChatAgentA(auth) +app.include_router(chat_agent_a.router()) + +@app.get("/") +async def root(): + return { + "name": "Chat Agent A", + "did": AGENT_A_DID, + "endpoint": "/a", + "status": "running", + "messages_received": chat_agent_a.message_count, + "messages_sent": chat_agent_a.sent_count, + "peer_connected": chat_agent_a.peer is not None, + "peer_name": chat_agent_a.peer_name, + "peer_ad_url": PEER_AD_URL, + } + + +@app.post("/p2p/discover") +async def p2p_discover(payload: dict = Body(default={})): + peer_ad_url = (payload or {}).get("peer_ad_url") + ok = await chat_agent_a.ensure_peer_connection(peer_ad_url=peer_ad_url) + if ok: + await chat_agent_a.maybe_start_chat_if_discovered_first(turns=AUTO_CHAT_TURNS) + return { + "ok": ok, + "peer_ad_url": (peer_ad_url or "").strip() or PEER_AD_URL, + "peer_name": getattr(chat_agent_a.peer, "name", None), + } + + +@app.post("/p2p/send") +async def p2p_send(payload: dict = Body(default={})): + message = (payload or {}).get("message", "") + remaining_turns = (payload or {}).get("remaining_turns", 4) + peer_ad_url = (payload or {}).get("peer_ad_url") + if not str(message).strip(): + return {"ok": False, "error": "missing_message"} + resp = await chat_agent_a.send_message( + message=str(message), + remaining_turns=int(remaining_turns), + peer_ad_url=peer_ad_url, + ) + return {"ok": True, "response": resp} + +@app.get("/health") +async def health_check(): + return { + "status": "healthy", + "agent": "ChatA", + "timestamp": time.time(), + "uptime": time.time() - getattr(app.state, 'start_time', time.time()) + } + +if __name__ == "__main__": + uvicorn.run(app, host="0.0.0.0", port=8000, access_log=False) \ No newline at end of file diff --git a/examples/python/openanp_examples/ChatB.py b/examples/python/openanp_examples/ChatB.py new file mode 100644 index 0000000..1949817 --- /dev/null +++ b/examples/python/openanp_examples/ChatB.py @@ -0,0 +1,397 @@ +import asyncio +import time +import uuid +from openai import OpenAI +import os +from typing import Optional +from anp.openanp import anp_agent, interface, AgentConfig, RemoteAgent +from anp.authentication import DIDWbaAuthHeader +from cryptography.hazmat.primitives.asymmetric import ec +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives import serialization +from fastapi import FastAPI, Body +import uvicorn +from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey +from contextlib import asynccontextmanager + +AGENT_B_DID = "did:wba:example.com:chatb" +AGENT_A_DID = "did:wba:example.com:chata" +AGENT_B_NAME = "ChatB" + +PEER_AD_URL = os.getenv("CHAT_PEER_AD_URL", "http://localhost:8000/a/ad.json") + +AUTO_DISCOVER = (os.getenv("CHAT_AUTO_DISCOVER", "1").strip().lower() not in {"0", "false", "no"}) +AUTO_DISCOVER_MAX_TRIES = int(os.getenv("CHAT_AUTO_DISCOVER_MAX_TRIES", "30").strip() or "30") +AUTO_DISCOVER_INTERVAL_SEC = float(os.getenv("CHAT_AUTO_DISCOVER_INTERVAL_SEC", "1").strip() or "1") + +AUTO_START_CHAT = (os.getenv("CHAT_AUTO_START", "1").strip().lower() not in {"0", "false", "no"}) +AUTO_CHAT_TURNS = int(os.getenv("CHAT_AUTO_TURNS", "4").strip() or "4") + +DISCOVER_TIE_TOLERANCE_SEC = float(os.getenv("CHAT_DISCOVER_TIE_TOLERANCE_SEC", "0.5").strip() or "0.5") + +API_KEY = os.getenv("OPENAI_KEY") +BASE_URL = os.getenv("OPENAI_API_BASE") +MODEL_NAME = "deepseek-chat" + +_client = None + + +def _get_client(): + global _client + if _client is not None: + return _client + if not API_KEY: + raise RuntimeError("missing OPENAI_KEY") + if BASE_URL: + _client = OpenAI(base_url=BASE_URL, api_key=API_KEY) + else: + _client = OpenAI(api_key=API_KEY) + return _client + +try: + from anp.authentication.did_wba_authenticator import DIDWbaAuthHeader as _LibDIDWbaAuthHeader + def _load_private_key_compat(self): + key_path = self.private_key_path + with open(key_path, 'rb') as f: + private_key_data = f.read() + try: + return serialization.load_pem_private_key(private_key_data, password=None) + except Exception: + return serialization.load_ssh_private_key(private_key_data, password=None) + def _sign_callback_compat(self, content: bytes, method_fragment: str) -> bytes: + private_key = self._load_private_key() + if isinstance(private_key, Ed25519PrivateKey): + return private_key.sign(content) + return private_key.sign(content, ec.ECDSA(hashes.SHA256())) + _LibDIDWbaAuthHeader._load_private_key = _load_private_key_compat + _LibDIDWbaAuthHeader._sign_callback = _sign_callback_compat +except Exception: + pass + +auth = DIDWbaAuthHeader( + did_document_path="./did_b.json", + private_key_path="./private_b.pem" +) + +@anp_agent(AgentConfig( + name=AGENT_B_NAME, + did=AGENT_B_DID, + prefix="/b", +)) +class ChatAgentB: + def __init__(self, auth: DIDWbaAuthHeader): + self.auth = auth + self.peer = None + self.peer_name: Optional[str] = None + self.first_discover_ts: Optional[float] = None + self.sent_count = 0 + self.message_count = 0 + self.connected_agents = set() + self._chat_lock = asyncio.Lock() + self._active_session_id: Optional[str] = None + self._chat_task: Optional[asyncio.Task] = None + self._auto_start_attempted = False + print("Intialized ChatAgentB") + + def _log_connected_once(self, agent_name: str) -> None: + name = (agent_name or "").strip() or "Unknown" + if name == "Unknown": + return + if name in self.connected_agents: + return + self.connected_agents.add(name) + print(f"\nChatB: 成功连接 {name}") + + @interface + async def notify_connected(self, agent: str) -> dict: + """ANP Interface: 由对端在 discover/连接后主动通知,用于日志展示""" + agent_name = (agent or "").strip() or "Unknown" + self._log_connected_once(agent_name) + if agent_name and agent_name != "Unknown": + self.peer_name = agent_name + return {"ok": True, "agent": "ChatB", "connected": agent_name} + + def _llm_generate(self, prompt: str) -> str: + if not API_KEY: + return "你好,我们开始聊天吧。" + + system_prompt = ( + "你是智能体 ChatB。你的任务是与对端智能体进行简短对话。" + "每次只输出一句要发给对端的中文消息,简洁自然。" + "不要输出解释、不要带前缀。" + ) + + resp = _get_client().chat.completions.create( + model=MODEL_NAME, + messages=[ + {"role": "system", "content": system_prompt}, + {"role": "user", "content": prompt}, + ], + temperature=0.8, + ) + content = resp.choices[0].message.content + return (content or "").strip() or "(空消息)" + + async def _run_chat_as_initiator(self, turns: int): + if self.peer is None: + return + + peer_label = self.peer_name or getattr(self.peer, "name", None) or "Peer" + remaining_turns = int(turns) + next_message = self._llm_generate("请你主动向对端打个招呼并开启对话。") + + while remaining_turns > 0: + print(f"\nChatB -> {peer_label}: {next_message}") + self.sent_count += 1 + + try: + response = await self.peer.receive_message(message=next_message, remaining_turns=remaining_turns) + except Exception as e: + print(f"ChatB: 调用对端失败: {str(e)}") + return + + peer_reply = (response or {}).get("reply", "") + if peer_reply: + print(f"{peer_label} -> ChatB: {peer_reply}") + else: + print(f"ChatB: 未收到对端 reply,原始响应: {response}") + + remaining_turns = int((response or {}).get("remaining_turns", 0)) + if remaining_turns <= 0: + print("\nChatB: 对话结束") + return + + next_message = self._llm_generate(f"对端说:{peer_reply}\n你回复对端一句话。") + + @interface + async def propose_chat(self, initiator_did: str, initiator_discover_ts: float, session_id: str, turns: int = 4) -> dict: + """ANP Interface: 对端请求由其发起聊天。""" + initiator = (initiator_did or "").strip() + sid = (session_id or "").strip() + if not initiator or not sid: + return {"accepted": False, "reason": "missing_params"} + + async with self._chat_lock: + if self._active_session_id is not None: + return {"accepted": False, "reason": "already_active", "session_id": self._active_session_id} + + local_ts = self.first_discover_ts + if local_ts is not None: + diff = float(initiator_discover_ts) - float(local_ts) + if diff > DISCOVER_TIE_TOLERANCE_SEC: + return {"accepted": False, "reason": "i_discovered_first", "winner": AGENT_B_DID} + if abs(diff) <= DISCOVER_TIE_TOLERANCE_SEC: + if AGENT_B_DID < initiator: + return {"accepted": False, "reason": "tie_break", "winner": AGENT_B_DID} + + self._active_session_id = sid + return {"accepted": True, "session_id": sid, "turns": int(turns)} + + async def maybe_start_chat_if_discovered_first(self, turns: int) -> None: + if not AUTO_START_CHAT: + return + if self.peer is None or self.first_discover_ts is None: + return + + async with self._chat_lock: + if self._active_session_id is not None: + return + if self._chat_task is not None and not self._chat_task.done(): + return + + sid = str(uuid.uuid4()) + peer_label = self.peer_name or getattr(self.peer, "name", None) or "Peer" + try: + resp = await self.peer.propose_chat( + initiator_did=AGENT_B_DID, + initiator_discover_ts=float(self.first_discover_ts), + session_id=sid, + turns=int(turns), + ) + except Exception as e: + print(f"ChatB: 向 {peer_label} 发起聊天失败: {str(e)}") + return + + if not (resp or {}).get("accepted"): + return + + async with self._chat_lock: + if self._active_session_id is None: + self._active_session_id = sid + if self._chat_task is None or self._chat_task.done(): + self._chat_task = asyncio.create_task(self._run_chat_as_initiator(turns=int(turns))) + + @interface + async def status(self) -> dict: + return { + "agent": "ChatB", + "did": AGENT_B_DID, + "messages_received": self.message_count, + "messages_sent": self.sent_count, + "peer_connected": self.peer is not None, + "peer_name": self.peer_name, + } + + def _llm_reply(self, user_message: str) -> str: + if not API_KEY: + return "(ChatB 未配置 OPENAI_KEY,无法调用模型)" + + system_prompt = ( + "你是一个通过 ANP 接口对话的智能体 ChatB。" + "你需要用中文、简洁、自然地回复对方的消息。" + "不要输出多余的元信息。" + ) + + resp = _get_client().chat.completions.create( + model=MODEL_NAME, + messages=[ + {"role": "system", "content": system_prompt}, + {"role": "user", "content": user_message}, + ], + temperature=0.7, + ) + content = resp.choices[0].message.content + return (content or "").strip() or "(空回复)" + + @interface + async def receive_message(self, message: str, remaining_turns: int) -> dict: + self.message_count += 1 + sender = self.peer_name or getattr(self.peer, "name", None) or "Peer" + print(f"\n{sender} -> ChatB: {message}") + + try: + reply = self._llm_reply(message) + except Exception as e: + reply = f"(ChatB 调用模型失败:{str(e)})" + + recipient = self.peer_name or getattr(self.peer, "name", None) or "Peer" + print(f"ChatB -> {recipient}: {reply}") + + new_remaining_turns = max(0, int(remaining_turns) - 1) + if new_remaining_turns <= 0: + print("\nChatB: 对话结束") + + return { + "agent": "ChatB", + "reply": reply, + "remaining_turns": new_remaining_turns, + } + + async def ensure_peer_connection(self, peer_ad_url: Optional[str] = None) -> bool: + """discover对端(ChatA)并缓存 RemoteAgent""" + if self.peer is not None: + return True + + url = (peer_ad_url or "").strip() or PEER_AD_URL + try: + self.peer = await RemoteAgent.discover(url, self.auth) + self.peer_name = getattr(self.peer, "name", None) or self.peer_name + if self.first_discover_ts is None: + self.first_discover_ts = time.time() + self._log_connected_once(self.peer_name or "Unknown") + try: + await self.peer.notify_connected(agent=AGENT_B_NAME) + except Exception as e: + print(f" ChatB: 已连接但通知对端失败: {str(e)}") + return True + except Exception as e: + print(f" ChatB: discover 对端失败: {str(e)}") + self.peer = None + return False + + async def send_message(self, message: str, remaining_turns: int = 4, peer_ad_url: Optional[str] = None) -> dict: + """P2P: 主动向对端发送消息""" + ok = await self.ensure_peer_connection(peer_ad_url=peer_ad_url) + if not ok or self.peer is None: + return {"ok": False, "error": "peer_not_connected"} + + self.sent_count += 1 + try: + return await self.peer.receive_message(message=message, remaining_turns=int(remaining_turns)) + except Exception as e: + return {"ok": False, "error": f"call_peer_failed: {str(e)}"} + +@asynccontextmanager +async def lifespan(app: FastAPI): + app.state.start_time = time.time() + print("\n" + "="*60) + print("启动 Chat Agent B (端口 8001)") + print(" • 访问 http://localhost:8001 查看状态") + print(" • 访问 http://localhost:8001/b/ad.json 查看广告") + print(" • 访问 http://localhost:8001/health 进行健康检查") + print(" • 访问 http://localhost:8001/p2p/discover 进行 P2P discover") + print(" • 访问 http://localhost:8001/p2p/send 主动发送消息") + print("="*60 + "\n") + + if AUTO_DISCOVER: + async def _auto_discover_loop(): + for _ in range(max(1, AUTO_DISCOVER_MAX_TRIES)): + if await chat_agent_b.ensure_peer_connection(): + if not chat_agent_b._auto_start_attempted: + chat_agent_b._auto_start_attempted = True + await chat_agent_b.maybe_start_chat_if_discovered_first(turns=AUTO_CHAT_TURNS) + return + await asyncio.sleep(max(0.1, AUTO_DISCOVER_INTERVAL_SEC)) + + asyncio.create_task(_auto_discover_loop()) + + yield + +app = FastAPI(title="ChatAgentB", description="Chat Agent B - 端口 8001", lifespan=lifespan) + +chat_agent_b = ChatAgentB(auth) +app.include_router(chat_agent_b.router()) + +@app.get("/") +async def root(): + return { + "name": "Chat Agent B", + "did": AGENT_B_DID, + "endpoint": "/b", + "status": "running", + "messages_sent": chat_agent_b.sent_count, + "messages_received": chat_agent_b.message_count, + "peer_connected": chat_agent_b.peer is not None, + "peer_name": chat_agent_b.peer_name, + "peer_ad_url": PEER_AD_URL, + } + + +@app.post("/p2p/discover") +async def p2p_discover(payload: dict = Body(default={})): + peer_ad_url = (payload or {}).get("peer_ad_url") + ok = await chat_agent_b.ensure_peer_connection(peer_ad_url=peer_ad_url) + if ok: + await chat_agent_b.maybe_start_chat_if_discovered_first(turns=AUTO_CHAT_TURNS) + return { + "ok": ok, + "peer_ad_url": (peer_ad_url or "").strip() or PEER_AD_URL, + "peer_name": getattr(chat_agent_b.peer, "name", None), + } + + +@app.post("/p2p/send") +async def p2p_send(payload: dict = Body(default={})): + message = (payload or {}).get("message", "") + remaining_turns = (payload or {}).get("remaining_turns", 4) + peer_ad_url = (payload or {}).get("peer_ad_url") + if not str(message).strip(): + return {"ok": False, "error": "missing_message"} + resp = await chat_agent_b.send_message( + message=str(message), + remaining_turns=int(remaining_turns), + peer_ad_url=peer_ad_url, + ) + return {"ok": True, "response": resp} + +@app.get("/health") +async def health_check(): + return { + "status": "healthy", + "agent": "ChatB", + "timestamp": time.time(), + "uptime": time.time() - getattr(app.state, 'start_time', time.time()) + } + +if __name__ == "__main__": + uvicorn.run(app, host="0.0.0.0", port=8001, access_log=False) \ No newline at end of file From f2683e4318c3a9a712250894fb7368e5283c9421 Mon Sep 17 00:00:00 2001 From: Chan01mc Date: Thu, 15 Jan 2026 18:29:22 +0000 Subject: [PATCH 3/6] Add chat agent examples and update DID keys with documentation --- docs/did_public/did_a.json | 4 +- docs/did_public/did_b.json | 4 +- docs/did_public/private_a.pem | 7 +- docs/did_public/private_b.pem | 4 +- examples/python/openanp_examples/README.md | 84 +++++++++++- .../openanp_examples/{ChatA.py => chat_a.py} | 124 ++++++++++++------ .../openanp_examples/{ChatB.py => chat_b.py} | 123 +++++++++++------ 7 files changed, 260 insertions(+), 90 deletions(-) rename examples/python/openanp_examples/{ChatA.py => chat_a.py} (71%) rename examples/python/openanp_examples/{ChatB.py => chat_b.py} (72%) diff --git a/docs/did_public/did_a.json b/docs/did_public/did_a.json index 0ab504b..7ff0da9 100644 --- a/docs/did_public/did_a.json +++ b/docs/did_public/did_a.json @@ -13,8 +13,8 @@ "publicKeyJwk": { "kty": "EC", "crv": "secp256k1", - "x": "f8K7HqZVXyLmQnRtYsUvWxZaBcDeFgHiJkLmNoPqRsTu", - "y": "A1B2C3D4E5F6G7H8I9J0K1L2M3N4O5P6Q7R8S9T0U1V2", + "x": "3QhHtIA9OrPys7Ih-C7ypn_wo0hURSJsftiO0InGX3Y", + "y": "AM3LVa-IHswIJdMq6-qXYE649545VNTAvzan6DM_jCM", "kid": "chata-key-2026-01" } } diff --git a/docs/did_public/did_b.json b/docs/did_public/did_b.json index 07ec232..dbe5a65 100644 --- a/docs/did_public/did_b.json +++ b/docs/did_public/did_b.json @@ -13,8 +13,8 @@ "publicKeyJwk": { "kty": "EC", "crv": "secp256k1", - "x": "qRsTuVwXyZaBcDeFgHiJkLmNoPqRsTuVwXyZaBcDeFgH", - "y": "Z9Y8X7W6V5U4T3S2R1Q0P9O8N7M6L5K4J3H2G1F0E9D8", + "x": "3QhHtIA9OrPys7Ih-C7ypn_wo0hURSJsftiO0InGX3Y", + "y": "AM3LVa-IHswIJdMq6-qXYE649545VNTAvzan6DM_jCM", "kid": "chatb-main-key-2026" } } diff --git a/docs/did_public/private_a.pem b/docs/did_public/private_a.pem index 489fa3d..d32b3e4 100644 --- a/docs/did_public/private_a.pem +++ b/docs/did_public/private_a.pem @@ -1,3 +1,6 @@ ------BEGIN PRIVATE KEY----- -MC4CAQAwBQYDK2VwBCIEIDj4yYy4H3DjZ1jXmZ9nY6vXlHq6Nz4vLx9uXKJ5pH0s +-----BEGIN PRIVATE KEY----- +MIGEAgEAMBAGByqGSM49AgEGBSuBBAAKBG0wawIBAQQgMd0Zh3TA+KzOh+aL5g76 +sz34ULdhjGkv4NEYnoGe7nehRANCAATdCEe0gD06s/KzsiH4LvKmf/CjSFRFImx+ +2I7QicZfdgDNy1WviB7MCCXTKuvql2BOuPeeOVTUwL82p+gzP4wj +-----END PRIVATE KEY----- -----END PRIVATE KEY----- \ No newline at end of file diff --git a/docs/did_public/private_b.pem b/docs/did_public/private_b.pem index 129de91..905b132 100644 --- a/docs/did_public/private_b.pem +++ b/docs/did_public/private_b.pem @@ -1,3 +1,5 @@ -----BEGIN PRIVATE KEY----- -MC4CAQAwBQYDK2VwBCIEIHv4yYy4H3DjZ1jXmZ9nY6vXlHq6Nz4vLx9uXKJ5pH0t +MIGEAgEAMBAGByqGSM49AgEGBSuBBAAKBG0wawIBAQQgMd0Zh3TA+KzOh+aL5g76 +sz34ULdhjGkv4NEYnoGe7nehRANCAATdCEe0gD06s/KzsiH4LvKmf/CjSFRFImx+ +2I7QicZfdgDNy1WviB7MCCXTKuvql2BOuPeeOVTUwL82p+gzP4wj -----END PRIVATE KEY----- \ No newline at end of file diff --git a/examples/python/openanp_examples/README.md b/examples/python/openanp_examples/README.md index 71de554..f5f940f 100644 --- a/examples/python/openanp_examples/README.md +++ b/examples/python/openanp_examples/README.md @@ -45,7 +45,8 @@ result = await agent.hello(name="World") # "Hello, World!" | `minimal_client.py` | Minimal client | ⭐ | | `advanced_server.py` | Full features (Context, Session, Information) | ⭐⭐⭐ | | `advanced_client.py` | Full client (discovery, error handling, LLM integration) | ⭐⭐⭐ | - +| `chat_a.py` | Chat Agent A (discovery, receive message, LLM integration) | ⭐⭐⭐ | +| `chat_b.py` | Chat Agent B (discovery, receive message, LLM integration) | ⭐⭐⭐ | --- ## 🏃 Running Examples @@ -199,6 +200,85 @@ curl http://localhost:8000/agent/ad.json | jq ```bash curl http://localhost:8000/agent/interface.json | jq ``` +## 💬 Chat Example + +The Chat Example demonstrates peer-to-peer LLM-powered agent communication with automatic discovery and conversation management. + +### Features + +- **Automatic Discovery**: Agents discover each other via ANP advertisement documents +- **P2P Communication**: Direct agent-to-agent message passing +- **LLM Integration**: Powered by OpenAI (with fallback for testing) +- **Session Management**: DID-based authentication and context tracking +- **Conversation Management**: Turn-based conversation with state tracking +- **Tie-Breaking**: Deterministic resolution when both agents discover simultaneously + +### Run Chat Example + +```bash +# Terminal 1: Start Chat Agent A +uv run python examples/python/openanp_examples/chat_a.py + +# Terminal 2: Start Chat Agent B (in another terminal) +uv run python examples/python/openanp_examples/chat_b.py +``` + +Both agents will automatically discover each other and start chatting! + +### Endpoints + +**Agent A** (Port 8000) +- Status: `http://localhost:8000` +- Advertisement: `http://localhost:8000/a/ad.json` +- Health: `http://localhost:8000/health` + +**Agent B** (Port 8001) +- Status: `http://localhost:8001` +- Advertisement: `http://localhost:8001/b/ad.json` +- Health: `http://localhost:8001/health` + +### Configuration + +```bash +# OpenAI Configuration (optional) +export OPENAI_KEY=your_api_key +export OPENAI_API_BASE=https://api.openai.com/v1 # optional for custom endpoints + +# Chat Behavior +export CHAT_AUTO_DISCOVER=1 # Auto-discover peer (default: 1) +export CHAT_AUTO_DISCOVER_MAX_TRIES=30 # Max discovery attempts (default: 30) +export CHAT_AUTO_DISCOVER_INTERVAL_SEC=1 # Discovery retry interval (default: 1) +export CHAT_AUTO_START=1 # Auto-start chat after discovery (default: 1) +export CHAT_AUTO_TURNS=4 # Number of conversation turns (default: 4) +export CHAT_DISCOVER_TIE_TOLERANCE_SEC=0.5 # Tie-break tolerance (default: 0.5) + +# Paths (optional) +export CHAT_DID_A_PATH=docs/did_public/did_a.json +export CHAT_PRIVATE_A_PATH=docs/did_public/private_a.pem +export CHAT_DID_B_PATH=docs/did_public/did_b.json +export CHAT_PRIVATE_B_PATH=docs/did_public/private_b.pem +export CHAT_PEER_AD_URL=http://localhost:8001/b/ad.json # For Agent A +``` + +### Expected Output + +``` +Starting Chat Agent A (port 8000) + • Visit http://localhost:8000 to view status + • Visit http://localhost:8000/a/ad.json to view advertisement + • Visit http://localhost:8000/health for health check + • Visit http://localhost:8000/p2p/discover for P2P discovery + • Visit http://localhost:8000/p2p/send to send message + +ChatA: Successfully connected to ChatB + +ChatA -> ChatB: Hello! How are you today? +ChatB -> ChatA: I'm doing great, thanks for asking! +ChatA -> ChatB: That's wonderful! What have you been up to? +ChatB -> ChatA: Just learning about ANP protocol. It's fascinating! + +ChatA: Conversation ended +``` --- @@ -206,3 +286,5 @@ curl http://localhost:8000/agent/interface.json | jq - [ANP Protocol Specification](https://github.com/agent-network-protocol) - [AgentConnect Documentation](../../../docs/) + +--- diff --git a/examples/python/openanp_examples/ChatA.py b/examples/python/openanp_examples/chat_a.py similarity index 71% rename from examples/python/openanp_examples/ChatA.py rename to examples/python/openanp_examples/chat_a.py index 63526af..fc2007b 100644 --- a/examples/python/openanp_examples/ChatA.py +++ b/examples/python/openanp_examples/chat_a.py @@ -1,3 +1,28 @@ +#!/usr/bin/env python3 +"""OpenANP Chat Agent A Example. + +Demonstrates peer-to-peer LLM-powered agent communication: +1. Agent discovery and P2P connection +2. Automatic peer-to-peer message exchange +3. LLM-powered conversation with OpenAI +4. Session management with DID authentication +5. Automatic peer discovery and chat initiation +6. Conversation state management and turn tracking + +Prerequisites: + OpenAI API key configured (optional, falls back to default responses): + export OPENAI_KEY=your_api_key + export OPENAI_API_BASE=your_api_base (optional for custom endpoints) + +Run: + Start both agents - open two terminals: + Terminal 1: uv run python examples/python/openanp_examples/chat_a.py + Terminal 2: uv run python examples/python/openanp_examples/chat_b.py + + Agents will auto-discover each other and start chatting! + View status: http://localhost:8000 +""" + import time import asyncio import uuid @@ -49,6 +74,7 @@ def _get_client(): try: from anp.authentication.did_wba_authenticator import DIDWbaAuthHeader as _LibDIDWbaAuthHeader + from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey def _load_private_key_compat(self): key_path = self.private_key_path @@ -58,23 +84,34 @@ def _load_private_key_compat(self): try: return serialization.load_pem_private_key(private_key_data, password=None) except Exception: - return serialization.load_ssh_private_key(private_key_data, password=None) + try: + return serialization.load_ssh_private_key(private_key_data, password=None) + except Exception: + # Try loading as PEM with password if available + return serialization.load_pem_private_key(private_key_data, password=None) def _sign_callback_compat(self, content: bytes, method_fragment: str) -> bytes: private_key = self._load_private_key() if isinstance(private_key, Ed25519PrivateKey): return private_key.sign(content) - return private_key.sign(content, ec.ECDSA(hashes.SHA256())) + elif isinstance(private_key, RSAPrivateKey): + from cryptography.hazmat.primitives.asymmetric import padding + return private_key.sign(content, padding.PKCS1v15(), hashes.SHA256()) + elif isinstance(private_key, ec.EllipticCurvePrivateKey): + return private_key.sign(content, ec.ECDSA(hashes.SHA256())) + else: + # Fallback + return private_key.sign(content, ec.ECDSA(hashes.SHA256())) _LibDIDWbaAuthHeader._load_private_key = _load_private_key_compat _LibDIDWbaAuthHeader._sign_callback = _sign_callback_compat -except Exception: - pass - +except Exception as e: + import sys + print(f"Warning: Failed to patch DIDWbaAuthHeader: {e}", file=sys.stderr) auth = DIDWbaAuthHeader( - did_document_path="./did_a.json", - private_key_path="./private_a.pem" + did_document_path=os.getenv("CHAT_DID_A_PATH", "docs/did_public/did_a.json"), + private_key_path=os.getenv("CHAT_PRIVATE_A_PATH", "docs/did_public/private_a.pem") ) @anp_agent(AgentConfig( @@ -95,7 +132,7 @@ def __init__(self, auth: DIDWbaAuthHeader): self._active_session_id: Optional[str] = None self._chat_task: Optional[asyncio.Task] = None self._auto_start_attempted = False - print("Intialized ChatAgentA") + print("Initialized ChatAgentA") def _log_connected_once(self, agent_name: str) -> None: name = (agent_name or "").strip() or "Unknown" @@ -104,11 +141,11 @@ def _log_connected_once(self, agent_name: str) -> None: if name in self.connected_agents: return self.connected_agents.add(name) - print(f"\nChatA: 成功连接 {name}") + print(f"\nChatA: Successfully connected to {name}") @interface async def notify_connected(self, agent: str) -> dict: - """ANP Interface: 由对端在 discover/连接后主动通知,用于日志展示""" + """ANP Interface: Notify when peer agent connects or discovers this agent""" agent_name = (agent or "").strip() or "Unknown" self._log_connected_once(agent_name) if agent_name and agent_name != "Unknown": @@ -117,12 +154,12 @@ async def notify_connected(self, agent: str) -> dict: def _llm_generate(self, prompt: str) -> str: if not API_KEY: - return "你好,我们开始聊天吧。" + return "Hello, let's start chatting." system_prompt = ( - "你是智能体 ChatA。你的任务是与对端智能体进行简短对话。" - "每次只输出一句要发给对端的中文消息,简洁自然。" - "不要输出解释、不要带前缀。" + "You are agent ChatA. Your task is to have a brief conversation with the peer agent. " + "Output only one sentence message to send to the peer each time, concise and natural. " + "Do not output explanations or prefixes." ) resp = _get_client().chat.completions.create( @@ -134,14 +171,14 @@ def _llm_generate(self, prompt: str) -> str: temperature=0.8, ) content = resp.choices[0].message.content - return (content or "").strip() or "(空消息)" + return (content or "").strip() or "(Empty message)" async def _run_chat_as_initiator(self, turns: int): if self.peer is None: return peer_label = self.peer_name or getattr(self.peer, "name", None) or "Peer" remaining_turns = int(turns) - next_message = self._llm_generate("请你主动向对端打个招呼并开启对话。") + next_message = self._llm_generate("Please proactively greet the peer agent and start the conversation.") while remaining_turns > 0: print(f"\nChatA -> {peer_label}: {next_message}") @@ -150,25 +187,25 @@ async def _run_chat_as_initiator(self, turns: int): try: response = await self.peer.receive_message(message=next_message, remaining_turns=remaining_turns) except Exception as e: - print(f"ChatA: 调用对端失败: {str(e)}") + print(f"ChatA: Failed to call peer: {str(e)}") return peer_reply = (response or {}).get("reply", "") if peer_reply: print(f"{peer_label} -> ChatA: {peer_reply}") else: - print(f"ChatA: 未收到对端 reply,原始响应: {response}") + print(f"ChatA: No reply received from peer, original response: {response}") remaining_turns = int((response or {}).get("remaining_turns", 0)) if remaining_turns <= 0: - print("\nChatA: 对话结束") + print("\nChatA: Conversation ended") return - next_message = self._llm_generate(f"对端说:{peer_reply}\n你回复对端一句话。") + next_message = self._llm_generate(f"Peer said: {peer_reply}\nReply to peer with one sentence.") @interface async def propose_chat(self, initiator_did: str, initiator_discover_ts: float, session_id: str, turns: int = 4) -> dict: - """ANP Interface: 对端请求由其发起聊天。""" + """ANP Interface: Peer requests to initiate chat""" initiator = (initiator_did or "").strip() sid = (session_id or "").strip() if not initiator or not sid: @@ -182,10 +219,10 @@ async def propose_chat(self, initiator_did: str, initiator_discover_ts: float, s if local_ts is not None: diff = float(initiator_discover_ts) - float(local_ts) if diff > DISCOVER_TIE_TOLERANCE_SEC: - # 我方明显更早 discover:拒绝,让我方发起 + # We discovered first: reject and let us initiate return {"accepted": False, "reason": "i_discovered_first", "winner": AGENT_A_DID} if abs(diff) <= DISCOVER_TIE_TOLERANCE_SEC: - # 近似同时:用 DID 做确定性裁决 + # Approximately simultaneous: use DID for deterministic tie-break if AGENT_A_DID < initiator: return {"accepted": False, "reason": "tie_break", "winner": AGENT_A_DID} @@ -214,7 +251,7 @@ async def maybe_start_chat_if_discovered_first(self, turns: int) -> None: turns=int(turns), ) except Exception as e: - print(f"ChatA: 向 {peer_label} 发起聊天失败: {str(e)}") + print(f"ChatA: Failed to initiate chat with {peer_label}: {str(e)}") return if not (resp or {}).get("accepted"): @@ -227,7 +264,7 @@ async def maybe_start_chat_if_discovered_first(self, turns: int) -> None: self._chat_task = asyncio.create_task(self._run_chat_as_initiator(turns=int(turns))) async def ensure_peer_connection(self, peer_ad_url: Optional[str] = None) -> bool: - """discover 对端(ChatB)并缓存 RemoteAgent""" + """Discover peer (ChatB) and cache RemoteAgent""" if self.peer is not None: return True @@ -241,21 +278,21 @@ async def ensure_peer_connection(self, peer_ad_url: Optional[str] = None) -> boo try: await self.peer.notify_connected(agent=AGENT_A_NAME) except Exception as e: - print(f" ChatA: 已连接但通知对端失败: {str(e)}") + print(f" ChatA: Connected but failed to notify peer: {str(e)}") return True except Exception as e: - print(f" ChatA: discover 对端失败: {str(e)}") + print(f" ChatA: Failed to discover peer: {str(e)}") self.peer = None return False def _llm_reply(self, user_message: str) -> str: if not API_KEY: - return "(ChatA 未配置 OPENAI_KEY,无法调用模型)" + return "(ChatA is not configured with OPENAI_KEY, cannot call model)" system_prompt = ( - "你是一个通过 ANP 接口对话的智能体 ChatA。" - "你需要用中文、简洁、自然地回复对方的消息。" - "不要输出多余的元信息。" + "You are agent ChatA communicating through ANP interface. " + "Reply to the peer's message in a concise and natural way. " + "Do not output extra metadata." ) resp = _get_client().chat.completions.create( @@ -267,10 +304,11 @@ def _llm_reply(self, user_message: str) -> str: temperature=0.7, ) content = resp.choices[0].message.content - return (content or "").strip() or "(空回复)" + return (content or "").strip() or "(Empty reply)" @interface async def status(self) -> dict: + """ANP Interface: Get agent status""" return { "agent": "ChatA", "did": AGENT_A_DID, @@ -282,7 +320,7 @@ async def status(self) -> dict: @interface async def receive_message(self, message: str, remaining_turns: int) -> dict: - """ANP Interface: 接收消息并用模型回复""" + """ANP Interface: Receive message and reply using model""" self.message_count += 1 sender = self.peer_name or getattr(self.peer, "name", None) or "Peer" print(f"\n{sender} -> ChatA: {message}") @@ -290,13 +328,13 @@ async def receive_message(self, message: str, remaining_turns: int) -> dict: try: reply = self._llm_reply(message) except Exception as e: - reply = f"(ChatA 调用模型失败:{str(e)})" + reply = f"(ChatA failed to call model: {str(e)})" print(f"ChatA -> ChatB: {reply}") new_remaining_turns = max(0, int(remaining_turns) - 1) if new_remaining_turns <= 0: - print("\nChatA: 对话结束") + print("\nChatA: Conversation ended") return { "agent": "ChatA", @@ -305,7 +343,7 @@ async def receive_message(self, message: str, remaining_turns: int) -> dict: } async def send_message(self, message: str, remaining_turns: int = 4, peer_ad_url: Optional[str] = None) -> dict: - """主动向对端发送消息""" + """Actively send message to peer""" ok = await self.ensure_peer_connection(peer_ad_url=peer_ad_url) if not ok or self.peer is None: return {"ok": False, "error": "peer_not_connected"} @@ -320,12 +358,12 @@ async def send_message(self, message: str, remaining_turns: int = 4, peer_ad_url async def lifespan(app: FastAPI): app.state.start_time = time.time() print("\n" + "=" * 60) - print("启动 Chat Agent A (端口 8000)") - print(" • 访问 http://localhost:8000 查看状态") - print(" • 访问 http://localhost:8000/a/ad.json 查看广告") - print(" • 访问 http://localhost:8000/health 进行健康检查") - print(" • 访问 http://localhost:8000/p2p/discover 进行 P2P discover") - print(" • 访问 http://localhost:8000/p2p/send 主动发送消息") + print("Starting Chat Agent A (port 8000)") + print(" • Visit http://localhost:8000 to view status") + print(" • Visit http://localhost:8000/a/ad.json to view advertisement") + print(" • Visit http://localhost:8000/health for health check") + print(" • Visit http://localhost:8000/p2p/discover for P2P discovery") + print(" • Visit http://localhost:8000/p2p/send to send message") print("=" * 60 + "\n") if AUTO_DISCOVER: @@ -343,7 +381,7 @@ async def _auto_discover_loop(): yield -app = FastAPI(title="ChatAgentA", description="Chat Agent A - 端口 8000", lifespan=lifespan) +app = FastAPI(title="ChatAgentA", description="Chat Agent A - port 8000", lifespan=lifespan) chat_agent_a = ChatAgentA(auth) app.include_router(chat_agent_a.router()) diff --git a/examples/python/openanp_examples/ChatB.py b/examples/python/openanp_examples/chat_b.py similarity index 72% rename from examples/python/openanp_examples/ChatB.py rename to examples/python/openanp_examples/chat_b.py index 1949817..73c95a3 100644 --- a/examples/python/openanp_examples/ChatB.py +++ b/examples/python/openanp_examples/chat_b.py @@ -1,3 +1,28 @@ +#!/usr/bin/env python3 +"""OpenANP Chat Agent B Example. + +Demonstrates peer-to-peer LLM-powered agent communication: +1. Agent discovery and P2P connection +2. Automatic peer-to-peer message exchange +3. LLM-powered conversation with OpenAI +4. Session management with DID authentication +5. Automatic peer discovery and chat initiation +6. Conversation state management and turn tracking + +Prerequisites: + OpenAI API key configured (optional, falls back to default responses): + export OPENAI_KEY=your_api_key + export OPENAI_API_BASE=your_api_base (optional for custom endpoints) + +Run: + Start both agents - open two terminals: + Terminal 1: uv run python examples/python/openanp_examples/chat_a.py + Terminal 2: uv run python examples/python/openanp_examples/chat_b.py + + Agents will auto-discover each other and start chatting! + View status: http://localhost:8001 +""" + import asyncio import time import uuid @@ -50,27 +75,44 @@ def _get_client(): try: from anp.authentication.did_wba_authenticator import DIDWbaAuthHeader as _LibDIDWbaAuthHeader + from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey + def _load_private_key_compat(self): key_path = self.private_key_path with open(key_path, 'rb') as f: private_key_data = f.read() + try: return serialization.load_pem_private_key(private_key_data, password=None) except Exception: - return serialization.load_ssh_private_key(private_key_data, password=None) + try: + return serialization.load_ssh_private_key(private_key_data, password=None) + except Exception: + # Try loading as PEM with password if available + return serialization.load_pem_private_key(private_key_data, password=None) + def _sign_callback_compat(self, content: bytes, method_fragment: str) -> bytes: private_key = self._load_private_key() if isinstance(private_key, Ed25519PrivateKey): return private_key.sign(content) - return private_key.sign(content, ec.ECDSA(hashes.SHA256())) + elif isinstance(private_key, RSAPrivateKey): + from cryptography.hazmat.primitives.asymmetric import padding + return private_key.sign(content, padding.PKCS1v15(), hashes.SHA256()) + elif isinstance(private_key, ec.EllipticCurvePrivateKey): + return private_key.sign(content, ec.ECDSA(hashes.SHA256())) + else: + # Fallback + return private_key.sign(content, ec.ECDSA(hashes.SHA256())) + _LibDIDWbaAuthHeader._load_private_key = _load_private_key_compat _LibDIDWbaAuthHeader._sign_callback = _sign_callback_compat -except Exception: - pass +except Exception as e: + import sys + print(f"Warning: Failed to patch DIDWbaAuthHeader: {e}", file=sys.stderr) auth = DIDWbaAuthHeader( - did_document_path="./did_b.json", - private_key_path="./private_b.pem" + did_document_path=os.getenv("CHAT_DID_B_PATH", "docs/did_public/did_b.json"), + private_key_path=os.getenv("CHAT_PRIVATE_B_PATH", "docs/did_public/private_b.pem") ) @anp_agent(AgentConfig( @@ -91,7 +133,7 @@ def __init__(self, auth: DIDWbaAuthHeader): self._active_session_id: Optional[str] = None self._chat_task: Optional[asyncio.Task] = None self._auto_start_attempted = False - print("Intialized ChatAgentB") + print("Initialized ChatAgentB") def _log_connected_once(self, agent_name: str) -> None: name = (agent_name or "").strip() or "Unknown" @@ -100,11 +142,11 @@ def _log_connected_once(self, agent_name: str) -> None: if name in self.connected_agents: return self.connected_agents.add(name) - print(f"\nChatB: 成功连接 {name}") + print(f"\nChatB: Successfully connected to {name}") @interface async def notify_connected(self, agent: str) -> dict: - """ANP Interface: 由对端在 discover/连接后主动通知,用于日志展示""" + """ANP Interface: Notify when peer agent connects or discovers this agent""" agent_name = (agent or "").strip() or "Unknown" self._log_connected_once(agent_name) if agent_name and agent_name != "Unknown": @@ -113,12 +155,12 @@ async def notify_connected(self, agent: str) -> dict: def _llm_generate(self, prompt: str) -> str: if not API_KEY: - return "你好,我们开始聊天吧。" + return "Hello, let's start chatting." system_prompt = ( - "你是智能体 ChatB。你的任务是与对端智能体进行简短对话。" - "每次只输出一句要发给对端的中文消息,简洁自然。" - "不要输出解释、不要带前缀。" + "You are agent ChatB. Your task is to have a brief conversation with the peer agent. " + "Output only one sentence message to send to the peer each time, concise and natural. " + "Do not output explanations or prefixes." ) resp = _get_client().chat.completions.create( @@ -130,7 +172,7 @@ def _llm_generate(self, prompt: str) -> str: temperature=0.8, ) content = resp.choices[0].message.content - return (content or "").strip() or "(空消息)" + return (content or "").strip() or "(Empty message)" async def _run_chat_as_initiator(self, turns: int): if self.peer is None: @@ -138,7 +180,7 @@ async def _run_chat_as_initiator(self, turns: int): peer_label = self.peer_name or getattr(self.peer, "name", None) or "Peer" remaining_turns = int(turns) - next_message = self._llm_generate("请你主动向对端打个招呼并开启对话。") + next_message = self._llm_generate("Please proactively greet the peer agent and start the conversation.") while remaining_turns > 0: print(f"\nChatB -> {peer_label}: {next_message}") @@ -147,25 +189,25 @@ async def _run_chat_as_initiator(self, turns: int): try: response = await self.peer.receive_message(message=next_message, remaining_turns=remaining_turns) except Exception as e: - print(f"ChatB: 调用对端失败: {str(e)}") + print(f"ChatB: Failed to call peer: {str(e)}") return peer_reply = (response or {}).get("reply", "") if peer_reply: print(f"{peer_label} -> ChatB: {peer_reply}") else: - print(f"ChatB: 未收到对端 reply,原始响应: {response}") + print(f"ChatB: No reply received from peer, original response: {response}") remaining_turns = int((response or {}).get("remaining_turns", 0)) if remaining_turns <= 0: - print("\nChatB: 对话结束") + print("\nChatB: Conversation ended") return - next_message = self._llm_generate(f"对端说:{peer_reply}\n你回复对端一句话。") + next_message = self._llm_generate(f"Peer said: {peer_reply}\nReply to peer with one sentence.") @interface async def propose_chat(self, initiator_did: str, initiator_discover_ts: float, session_id: str, turns: int = 4) -> dict: - """ANP Interface: 对端请求由其发起聊天。""" + """ANP Interface: Peer requests to initiate chat""" initiator = (initiator_did or "").strip() sid = (session_id or "").strip() if not initiator or not sid: @@ -179,8 +221,10 @@ async def propose_chat(self, initiator_did: str, initiator_discover_ts: float, s if local_ts is not None: diff = float(initiator_discover_ts) - float(local_ts) if diff > DISCOVER_TIE_TOLERANCE_SEC: + # We discovered first: reject and let us initiate return {"accepted": False, "reason": "i_discovered_first", "winner": AGENT_B_DID} if abs(diff) <= DISCOVER_TIE_TOLERANCE_SEC: + # Approximately simultaneous: use DID for deterministic tie-break if AGENT_B_DID < initiator: return {"accepted": False, "reason": "tie_break", "winner": AGENT_B_DID} @@ -209,7 +253,7 @@ async def maybe_start_chat_if_discovered_first(self, turns: int) -> None: turns=int(turns), ) except Exception as e: - print(f"ChatB: 向 {peer_label} 发起聊天失败: {str(e)}") + print(f"ChatB: Failed to initiate chat with {peer_label}: {str(e)}") return if not (resp or {}).get("accepted"): @@ -223,6 +267,7 @@ async def maybe_start_chat_if_discovered_first(self, turns: int) -> None: @interface async def status(self) -> dict: + """ANP Interface: Get agent status""" return { "agent": "ChatB", "did": AGENT_B_DID, @@ -234,12 +279,12 @@ async def status(self) -> dict: def _llm_reply(self, user_message: str) -> str: if not API_KEY: - return "(ChatB 未配置 OPENAI_KEY,无法调用模型)" + return "(ChatB is not configured with OPENAI_KEY, cannot call model)" system_prompt = ( - "你是一个通过 ANP 接口对话的智能体 ChatB。" - "你需要用中文、简洁、自然地回复对方的消息。" - "不要输出多余的元信息。" + "You are agent ChatB communicating through ANP interface. " + "Reply to the peer's message in a concise and natural way. " + "Do not output extra metadata." ) resp = _get_client().chat.completions.create( @@ -251,7 +296,7 @@ def _llm_reply(self, user_message: str) -> str: temperature=0.7, ) content = resp.choices[0].message.content - return (content or "").strip() or "(空回复)" + return (content or "").strip() or "(Empty reply)" @interface async def receive_message(self, message: str, remaining_turns: int) -> dict: @@ -262,14 +307,14 @@ async def receive_message(self, message: str, remaining_turns: int) -> dict: try: reply = self._llm_reply(message) except Exception as e: - reply = f"(ChatB 调用模型失败:{str(e)})" + reply = f"(ChatB failed to call model: {str(e)})" recipient = self.peer_name or getattr(self.peer, "name", None) or "Peer" print(f"ChatB -> {recipient}: {reply}") new_remaining_turns = max(0, int(remaining_turns) - 1) if new_remaining_turns <= 0: - print("\nChatB: 对话结束") + print("\nChatB: Conversation ended") return { "agent": "ChatB", @@ -278,7 +323,7 @@ async def receive_message(self, message: str, remaining_turns: int) -> dict: } async def ensure_peer_connection(self, peer_ad_url: Optional[str] = None) -> bool: - """discover对端(ChatA)并缓存 RemoteAgent""" + """Discover peer (ChatA) and cache RemoteAgent""" if self.peer is not None: return True @@ -292,15 +337,15 @@ async def ensure_peer_connection(self, peer_ad_url: Optional[str] = None) -> boo try: await self.peer.notify_connected(agent=AGENT_B_NAME) except Exception as e: - print(f" ChatB: 已连接但通知对端失败: {str(e)}") + print(f" ChatB: Connected but failed to notify peer: {str(e)}") return True except Exception as e: - print(f" ChatB: discover 对端失败: {str(e)}") + print(f" ChatB: Failed to discover peer: {str(e)}") self.peer = None return False async def send_message(self, message: str, remaining_turns: int = 4, peer_ad_url: Optional[str] = None) -> dict: - """P2P: 主动向对端发送消息""" + """Actively send message to peer""" ok = await self.ensure_peer_connection(peer_ad_url=peer_ad_url) if not ok or self.peer is None: return {"ok": False, "error": "peer_not_connected"} @@ -315,12 +360,12 @@ async def send_message(self, message: str, remaining_turns: int = 4, peer_ad_url async def lifespan(app: FastAPI): app.state.start_time = time.time() print("\n" + "="*60) - print("启动 Chat Agent B (端口 8001)") - print(" • 访问 http://localhost:8001 查看状态") - print(" • 访问 http://localhost:8001/b/ad.json 查看广告") - print(" • 访问 http://localhost:8001/health 进行健康检查") - print(" • 访问 http://localhost:8001/p2p/discover 进行 P2P discover") - print(" • 访问 http://localhost:8001/p2p/send 主动发送消息") + print("Starting Chat Agent B (port 8001)") + print(" • Visit http://localhost:8001 to view status") + print(" • Visit http://localhost:8001/b/ad.json to view advertisement") + print(" • Visit http://localhost:8001/health for health check") + print(" • Visit http://localhost:8001/p2p/discover for P2P discovery") + print(" • Visit http://localhost:8001/p2p/send to send message") print("="*60 + "\n") if AUTO_DISCOVER: @@ -337,7 +382,7 @@ async def _auto_discover_loop(): yield -app = FastAPI(title="ChatAgentB", description="Chat Agent B - 端口 8001", lifespan=lifespan) +app = FastAPI(title="ChatAgentB", description="Chat Agent B - port 8001", lifespan=lifespan) chat_agent_b = ChatAgentB(auth) app.include_router(chat_agent_b.router()) From ed8be02db4fca086acb6c170d3fb4b3e90d40581 Mon Sep 17 00:00:00 2001 From: Chan01mc Date: Thu, 15 Jan 2026 18:34:49 +0000 Subject: [PATCH 4/6] Optimize chat example documentation with code architecture instead of config --- examples/python/openanp_examples/README.md | 108 +++++++++------------ 1 file changed, 46 insertions(+), 62 deletions(-) diff --git a/examples/python/openanp_examples/README.md b/examples/python/openanp_examples/README.md index f5f940f..0532104 100644 --- a/examples/python/openanp_examples/README.md +++ b/examples/python/openanp_examples/README.md @@ -202,16 +202,7 @@ curl http://localhost:8000/agent/interface.json | jq ``` ## 💬 Chat Example -The Chat Example demonstrates peer-to-peer LLM-powered agent communication with automatic discovery and conversation management. - -### Features - -- **Automatic Discovery**: Agents discover each other via ANP advertisement documents -- **P2P Communication**: Direct agent-to-agent message passing -- **LLM Integration**: Powered by OpenAI (with fallback for testing) -- **Session Management**: DID-based authentication and context tracking -- **Conversation Management**: Turn-based conversation with state tracking -- **Tie-Breaking**: Deterministic resolution when both agents discover simultaneously +Peer-to-peer LLM-powered agent communication with automatic discovery and conversation management. ### Run Chat Example @@ -223,62 +214,55 @@ uv run python examples/python/openanp_examples/chat_a.py uv run python examples/python/openanp_examples/chat_b.py ``` -Both agents will automatically discover each other and start chatting! - -### Endpoints - -**Agent A** (Port 8000) -- Status: `http://localhost:8000` -- Advertisement: `http://localhost:8000/a/ad.json` -- Health: `http://localhost:8000/health` - -**Agent B** (Port 8001) -- Status: `http://localhost:8001` -- Advertisement: `http://localhost:8001/b/ad.json` -- Health: `http://localhost:8001/health` - -### Configuration - -```bash -# OpenAI Configuration (optional) -export OPENAI_KEY=your_api_key -export OPENAI_API_BASE=https://api.openai.com/v1 # optional for custom endpoints - -# Chat Behavior -export CHAT_AUTO_DISCOVER=1 # Auto-discover peer (default: 1) -export CHAT_AUTO_DISCOVER_MAX_TRIES=30 # Max discovery attempts (default: 30) -export CHAT_AUTO_DISCOVER_INTERVAL_SEC=1 # Discovery retry interval (default: 1) -export CHAT_AUTO_START=1 # Auto-start chat after discovery (default: 1) -export CHAT_AUTO_TURNS=4 # Number of conversation turns (default: 4) -export CHAT_DISCOVER_TIE_TOLERANCE_SEC=0.5 # Tie-break tolerance (default: 0.5) - -# Paths (optional) -export CHAT_DID_A_PATH=docs/did_public/did_a.json -export CHAT_PRIVATE_A_PATH=docs/did_public/private_a.pem -export CHAT_DID_B_PATH=docs/did_public/did_b.json -export CHAT_PRIVATE_B_PATH=docs/did_public/private_b.pem -export CHAT_PEER_AD_URL=http://localhost:8001/b/ad.json # For Agent A -``` +### Chat Agent Architecture -### Expected Output +**Core Agent Structure (chat_a.py & chat_b.py)** +```python +@anp_agent(AgentConfig( + name="ChatA", + did="did:wba:example.com:chata", + prefix="/a", +)) +class ChatAgentA: + @interface + async def notify_connected(self, agent: str) -> dict: + """Called when peer agent connects""" + return {"ok": True, "agent": "ChatA", "connected": agent} + + @interface + async def receive_message(self, message: str, remaining_turns: int) -> dict: + """Receive message and reply using LLM""" + reply = self._llm_reply(message) # OpenAI or fallback + remaining_turns = max(0, remaining_turns - 1) + return { + "agent": "ChatA", + "reply": reply, + "remaining_turns": remaining_turns, + } + + @interface + async def propose_chat(self, initiator_did: str, initiator_discover_ts: float, + session_id: str, turns: int = 4) -> dict: + """Peer requests to initiate chat with tie-breaking""" + # Deterministic tie-break using DID when both discover simultaneously + if AGENT_A_DID < initiator_did: + return {"accepted": False, "reason": "tie_break"} + return {"accepted": True, "session_id": session_id} ``` -Starting Chat Agent A (port 8000) - • Visit http://localhost:8000 to view status - • Visit http://localhost:8000/a/ad.json to view advertisement - • Visit http://localhost:8000/health for health check - • Visit http://localhost:8000/p2p/discover for P2P discovery - • Visit http://localhost:8000/p2p/send to send message - -ChatA: Successfully connected to ChatB -ChatA -> ChatB: Hello! How are you today? -ChatB -> ChatA: I'm doing great, thanks for asking! -ChatA -> ChatB: That's wonderful! What have you been up to? -ChatB -> ChatA: Just learning about ANP protocol. It's fascinating! - -ChatA: Conversation ended -``` +**Key Features** + +- **Automatic Peer Discovery**: Uses `RemoteAgent.discover(url, auth)` to locate peers +- **DID Authentication**: Each agent authenticates with secp256k1 private keys +- **LLM Integration**: Integrated with OpenAI API for intelligent responses +- **Session Management**: Deterministic tie-breaking when both agents discover simultaneously +- **Turn-Based Conversation**: Tracks remaining turns and gracefully ends conversations +- **Message Flow**: + 1. Agent A discovers Agent B via ANP advertisement + 2. Agent A calls `propose_chat()` to initiate conversation + 3. Agents exchange messages via `receive_message()` + 4. Conversation ends when turns reach zero --- From ace7e6c585d2f384b4e6f4f8c3026b1bd8690104 Mon Sep 17 00:00:00 2001 From: Chan01mc Date: Thu, 15 Jan 2026 18:37:57 +0000 Subject: [PATCH 5/6] Restore endpoints section in chat example documentation --- examples/python/openanp_examples/README.md | 24 +++++++++++----------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/examples/python/openanp_examples/README.md b/examples/python/openanp_examples/README.md index 0532104..41a3c86 100644 --- a/examples/python/openanp_examples/README.md +++ b/examples/python/openanp_examples/README.md @@ -251,18 +251,18 @@ class ChatAgentA: return {"accepted": True, "session_id": session_id} ``` -**Key Features** - -- **Automatic Peer Discovery**: Uses `RemoteAgent.discover(url, auth)` to locate peers -- **DID Authentication**: Each agent authenticates with secp256k1 private keys -- **LLM Integration**: Integrated with OpenAI API for intelligent responses -- **Session Management**: Deterministic tie-breaking when both agents discover simultaneously -- **Turn-Based Conversation**: Tracks remaining turns and gracefully ends conversations -- **Message Flow**: - 1. Agent A discovers Agent B via ANP advertisement - 2. Agent A calls `propose_chat()` to initiate conversation - 3. Agents exchange messages via `receive_message()` - 4. Conversation ends when turns reach zero +### Endpoints + +**Agent A** (Port 8000) +- Status: `http://localhost:8000` +- Advertisement: `http://localhost:8000/a/ad.json` +- Health: `http://localhost:8000/health` + +**Agent B** (Port 8001) +- Status: `http://localhost:8001` +- Advertisement: `http://localhost:8001/b/ad.json` +- Health: `http://localhost:8001/health` + --- From 6f4cb48c3929adf177f3c97760c1060420161b4d Mon Sep 17 00:00:00 2001 From: Chan01mc Date: Thu, 15 Jan 2026 18:44:57 +0000 Subject: [PATCH 6/6] Add key features explanation to chat example documentation --- examples/python/openanp_examples/README.md | 22 +++++++--------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/examples/python/openanp_examples/README.md b/examples/python/openanp_examples/README.md index 41a3c86..45868fa 100644 --- a/examples/python/openanp_examples/README.md +++ b/examples/python/openanp_examples/README.md @@ -201,9 +201,6 @@ curl http://localhost:8000/agent/ad.json | jq curl http://localhost:8000/agent/interface.json | jq ``` ## 💬 Chat Example - -Peer-to-peer LLM-powered agent communication with automatic discovery and conversation management. - ### Run Chat Example ```bash @@ -250,19 +247,14 @@ class ChatAgentA: return {"accepted": False, "reason": "tie_break"} return {"accepted": True, "session_id": session_id} ``` +### Generated Endpoints -### Endpoints - -**Agent A** (Port 8000) -- Status: `http://localhost:8000` -- Advertisement: `http://localhost:8000/a/ad.json` -- Health: `http://localhost:8000/health` - -**Agent B** (Port 8001) -- Status: `http://localhost:8001` -- Advertisement: `http://localhost:8001/b/ad.json` -- Health: `http://localhost:8001/health` - +| Endpoint | Description | +|----------|-------------| +| `GET /` | status | +| `GET /health` | health check | +| `POST /p2p/discover` | trigger discovery and cache the peer connection | +| `POST /p2p/send` | send a message to the peer (internally calls peer `receive_message`) | ---