From 8e3be938727a5bea313ea1c2ee020c5c46768d17 Mon Sep 17 00:00:00 2001 From: HarshavardhanVemali Date: Tue, 26 May 2026 00:32:13 +0530 Subject: [PATCH 1/3] Fix security vulnerabilities (#136) --- .../frontend-integration/package-lock.json | 128 +++++++++++------- frontend-integration/frontend_app.py | 7 +- issue_report_formatted.md | 38 ++++++ video-to-map-agent/pdf_generator_agent.py | 8 +- video-to-map-agent/weather_monitor_agent.py | 8 +- web3/internet-computer/fetch/agent.py | 14 +- 6 files changed, 137 insertions(+), 66 deletions(-) create mode 100644 issue_report_formatted.md diff --git a/frontend-integration/frontend-integration/package-lock.json b/frontend-integration/frontend-integration/package-lock.json index a3e17f64..d992441d 100644 --- a/frontend-integration/frontend-integration/package-lock.json +++ b/frontend-integration/frontend-integration/package-lock.json @@ -97,9 +97,9 @@ } }, "node_modules/@next/env": { - "version": "14.2.33", - "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.33.tgz", - "integrity": "sha512-CgVHNZ1fRIlxkLhIX22flAZI/HmpDaZ8vwyJ/B0SDPTBuLZ1PJ+DWMjCHhqnExfmSQzA/PbZi8OAc7PAq2w9IA==", + "version": "14.2.35", + "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.35.tgz", + "integrity": "sha512-DuhvCtj4t9Gwrx80dmz2F4t/zKQ4ktN8WrMwOuVzkJfBilwAwGr6v16M5eI8yCuZ63H9TTuEU09Iu2HqkzFPVQ==", "license": "MIT" }, "node_modules/@next/swc-darwin-arm64": { @@ -434,6 +434,18 @@ "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", "license": "ISC" }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/ansi-regex": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", @@ -533,14 +545,15 @@ } }, "node_modules/axios": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz", - "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==", + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.16.1.tgz", + "integrity": "sha512-caYkukvroVPO8KrzuJEb50Hm07KwfBZPEC3VeFHTsqWHvKTsy54hjJz9BS/cdaypROE2rH6xvm9mHX4fgWkr3A==", "license": "MIT", "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.4", - "proxy-from-env": "^1.1.0" + "follow-redirects": "^1.16.0", + "form-data": "^4.0.5", + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" } }, "node_modules/bail": { @@ -584,9 +597,9 @@ } }, "node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", + "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", "dev": true, "license": "MIT", "dependencies": { @@ -1003,9 +1016,9 @@ } }, "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -1121,9 +1134,9 @@ } }, "node_modules/follow-redirects": { - "version": "1.15.11", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", - "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", "funding": [ { "type": "individual", @@ -1158,9 +1171,9 @@ } }, "node_modules/form-data": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", - "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", @@ -1249,9 +1262,10 @@ } }, "node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "dev": true, "license": "ISC", "dependencies": { @@ -1389,6 +1403,19 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/inline-style-parser": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.4.tgz", @@ -1846,9 +1873,9 @@ } }, "node_modules/mdast-util-to-hast": { - "version": "13.2.0", - "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz", - "integrity": "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==", + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", "license": "MIT", "dependencies": { "@types/hast": "^3.0.0", @@ -2509,13 +2536,13 @@ } }, "node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^2.0.2" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -2553,9 +2580,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", "funding": [ { "type": "github", @@ -2571,12 +2598,12 @@ } }, "node_modules/next": { - "version": "14.2.33", - "resolved": "https://registry.npmjs.org/next/-/next-14.2.33.tgz", - "integrity": "sha512-GiKHLsD00t4ACm1p00VgrI0rUFAC9cRDGReKyERlM57aeEZkOQGcZTpIbsGn0b562FTPJWmYfKwplfO9EaT6ng==", + "version": "14.2.35", + "resolved": "https://registry.npmjs.org/next/-/next-14.2.35.tgz", + "integrity": "sha512-KhYd2Hjt/O1/1aZVX3dCwGXM1QmOV4eNM2UTacK5gipDdPN/oHHK/4oVGy7X8GMfPMsUTUEmGlsy0EY1YGAkig==", "license": "MIT", "dependencies": { - "@next/env": "14.2.33", + "@next/env": "14.2.35", "@swc/helpers": "0.5.5", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", @@ -2768,9 +2795,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true, "license": "MIT", "engines": { @@ -2801,9 +2828,9 @@ } }, "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", "dev": true, "funding": [ { @@ -2821,7 +2848,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.11", + "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -2974,10 +3001,13 @@ } }, "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "license": "MIT" + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } }, "node_modules/queue-microtask": { "version": "1.2.3", diff --git a/frontend-integration/frontend_app.py b/frontend-integration/frontend_app.py index a909eefa..90e2d4a3 100644 --- a/frontend-integration/frontend_app.py +++ b/frontend-integration/frontend_app.py @@ -1,6 +1,7 @@ from flask import Flask, render_template, request, jsonify import requests import json +import os app = Flask(__name__) @@ -24,7 +25,7 @@ def search_products(): # Call search agent with POST request payload = {"query": query} - response = requests.post(f"{AGENTS['search']}/search", json=payload) + response = requests.post(f"{AGENTS['search']}/search", json=payload, timeout=10) response.raise_for_status() # Agent returns JSON directly @@ -65,7 +66,7 @@ def get_product_info(): # Call info agent with POST request payload = {"barcode": barcode} - response = requests.post(f"{AGENTS['info']}/product", json=payload) + response = requests.post(f"{AGENTS['info']}/product", json=payload, timeout=10) response.raise_for_status() # Agent returns JSON directly @@ -128,4 +129,4 @@ def health_check(): print("Available endpoints:") print("- Main interface: http://127.0.0.1:5000") print("- Health check: http://127.0.0.1:5000/health") - app.run(host='127.0.0.1', port=5000, debug=True) \ No newline at end of file + app.run(host='127.0.0.1', port=5000, debug=os.environ.get('FLASK_DEBUG') == '1') \ No newline at end of file diff --git a/issue_report_formatted.md b/issue_report_formatted.md new file mode 100644 index 00000000..76458ca3 --- /dev/null +++ b/issue_report_formatted.md @@ -0,0 +1,38 @@ +### Summary +A security scan (using Bandit and npm audit) has identified multiple security vulnerabilities across the repository, including a Flask app running in debug mode, vulnerable NPM packages, missing request timeouts, and silent error suppression. + +### Steps to reproduce +1. Run `bandit -r .` in the root of the repository to identify Python issues. +2. Run `npm install --package-lock-only && npm audit` inside `frontend-integration/frontend-integration` to see JavaScript dependency vulnerabilities. + +### Expected behavior +The repository code should adhere to basic security guidelines: +- Flask should not be run in debug mode in production-like environments. +- HTTP requests via the `requests` library should specify a timeout to avoid hangs. +- Node dependencies should be regularly audited and updated. +- Exceptions should be properly caught and logged rather than silently ignored. + +### Actual behavior +1. **Flask `debug=True` enabled (High Severity):** `frontend-integration/frontend_app.py:131` runs Flask with `debug=True`, which exposes an interactive debugger potentially allowing Remote Code Execution (RCE). +2. **Vulnerable NPM Dependencies (High Severity):** `frontend-integration/frontend-integration/package.json` uses outdated versions of `axios`, `next`, and other packages containing known SSRF, DoS, and Prototype Pollution vulnerabilities. +3. **Missing Request Timeouts (Medium Severity):** `web3/internet-computer/fetch/agent.py` and other agents call `requests.post()` and `requests.get()` without `timeout` parameters, risking denial of service if the endpoint hangs. +4. **Silent Error Suppression (Low Severity):** `video-to-map-agent/pdf_generator_agent.py` and `video-to-map-agent/weather_monitor_agent.py` globally suppress errors via `except Exception: pass`. + +### Affected file or folder path +`frontend-integration/frontend_app.py` +`frontend-integration/frontend-integration/package.json` +`web3/internet-computer/fetch/agent.py` +`video-to-map-agent/pdf_generator_agent.py` + +### Logs / traceback +```shell +# Bandit Finding (B201) +B201 (flask_debug_true) - frontend-integration/frontend_app.py:131 +Severity: HIGH + +# NPM Audit +9 vulnerabilities (4 moderate, 5 high) in frontend-integration/frontend-integration +``` + +### Environment +macOS, Python 3.11, Automated Scanner diff --git a/video-to-map-agent/pdf_generator_agent.py b/video-to-map-agent/pdf_generator_agent.py index 5d02d967..11864f38 100644 --- a/video-to-map-agent/pdf_generator_agent.py +++ b/video-to-map-agent/pdf_generator_agent.py @@ -110,8 +110,8 @@ def download_thumbnail(url: str): resp = requests.get(url, timeout=10) if resp.status_code == 200: return BytesIO(resp.content) - except Exception: - pass + except Exception as e: + print(f"Error downloading thumbnail: {e}") return None @@ -174,8 +174,8 @@ def _render_cover_page(pdf: "TravelPDF", msg: PDFRequest) -> None: try: pdf.image(img_data, x=55, y=thumb_y, w=100) thumb_y += 60 - except Exception: - pass + except Exception as e: + print(f"Error embedding thumbnail: {e}") # intro text pdf.set_y(thumb_y + 8) diff --git a/video-to-map-agent/weather_monitor_agent.py b/video-to-map-agent/weather_monitor_agent.py index 03caa3ea..8534ebe0 100644 --- a/video-to-map-agent/weather_monitor_agent.py +++ b/video-to-map-agent/weather_monitor_agent.py @@ -58,8 +58,8 @@ def get_daily_forecast(lat: float, lng: float, target_date: str) -> dict: ) if date_str == target_date: return day - except Exception: - pass + except Exception as e: + print(f"Error fetching daily forecast: {e}") return {} @@ -234,9 +234,9 @@ def _append_log_rows(excel_path: str, trip_date: str, per_stop_rows: list) -> No row += 1 wb.save(excel_path) - except Exception: + except Exception as e: # Never let excel bookkeeping break the actual monitoring. - pass + print(f"Error writing to excel: {e}") @agent.on_interval(period=86400.0) diff --git a/web3/internet-computer/fetch/agent.py b/web3/internet-computer/fetch/agent.py index fa148c50..6e1a5dc2 100644 --- a/web3/internet-computer/fetch/agent.py +++ b/web3/internet-computer/fetch/agent.py @@ -109,16 +109,16 @@ async def call_icp_endpoint(func_name: str, args: dict): if func_name == "get_current_fee_percentiles": url = f"{BASE_URL}/get-current-fee-percentiles" - response = requests.post(url, headers=HEADERS, json={}) + response = requests.post(url, headers=HEADERS, json={}, timeout=10) elif func_name == "get_balance": url = f"{BASE_URL}/get-balance" - response = requests.post(url, headers=HEADERS, json={"address": args["address"]}) + response = requests.post(url, headers=HEADERS, json={"address": args["address"]}, timeout=10) elif func_name == "get_utxos": url = f"{BASE_URL}/get-utxos" - response = requests.post(url, headers=HEADERS, json={"address": args["address"]}) + response = requests.post(url, headers=HEADERS, json={"address": args["address"]}, timeout=10) elif func_name == "send": url = f"{BASE_URL}/send" - response = requests.post(url, headers=HEADERS, json=args) + response = requests.post(url, headers=HEADERS, json=args, timeout=10) else: raise ValueError(f"Unsupported function call: {func_name}") response.raise_for_status() @@ -141,7 +141,8 @@ async def process_query(query: str, ctx: Context) -> str: response = requests.post( f"{ASI1_BASE_URL}/chat/completions", headers=ASI1_HEADERS, - json=payload + json=payload, + timeout=30 ) response.raise_for_status() response_json = response.json() @@ -188,7 +189,8 @@ async def process_query(query: str, ctx: Context) -> str: final_response = requests.post( f"{ASI1_BASE_URL}/chat/completions", headers=ASI1_HEADERS, - json=final_payload + json=final_payload, + timeout=30 ) final_response.raise_for_status() final_response_json = final_response.json() From f88f569ebbe3820889023848958ae396c138bddb Mon Sep 17 00:00:00 2001 From: HarshavardhanVemali Date: Tue, 26 May 2026 00:35:38 +0530 Subject: [PATCH 2/3] Add issue reports --- issue_report_formatted.md | 50 ++++++++++++--------------------------- new_issue_report.md | 24 +++++++++++++++++++ 2 files changed, 39 insertions(+), 35 deletions(-) create mode 100644 new_issue_report.md diff --git a/issue_report_formatted.md b/issue_report_formatted.md index 76458ca3..3545e5fa 100644 --- a/issue_report_formatted.md +++ b/issue_report_formatted.md @@ -1,38 +1,18 @@ -### Summary -A security scan (using Bandit and npm audit) has identified multiple security vulnerabilities across the repository, including a Flask app running in debug mode, vulnerable NPM packages, missing request timeouts, and silent error suppression. +A secondary security audit using `bandit` revealed additional vulnerabilities in this repository that should be addressed. -### Steps to reproduce -1. Run `bandit -r .` in the root of the repository to identify Python issues. -2. Run `npm install --package-lock-only && npm audit` inside `frontend-integration/frontend-integration` to see JavaScript dependency vulnerabilities. +### 1. Remote Code Execution (RCE) via `eval()` +Using `eval()` on untrusted input from an LLM or user allows arbitrary Python code execution. +- `Crewai-agents/trip_planner/tools/calculator_tools.py`: The `calculate` tool calls `eval(operation)` on raw string inputs. +- `anthropic-quickstart/03-function-calling-agent/claude_function_agent.py`: Uses `eval()` for math evaluation. While `__builtins__` is cleared, `eval` is notoriously difficult to sandbox and can still be bypassed. +**Recommendation**: Restrict the evaluation to basic math operations or use a safer alternative like `ast.literal_eval` or strict regex validation. -### Expected behavior -The repository code should adhere to basic security guidelines: -- Flask should not be run in debug mode in production-like environments. -- HTTP requests via the `requests` library should specify a timeout to avoid hangs. -- Node dependencies should be regularly audited and updated. -- Exceptions should be properly caught and logged rather than silently ignored. +### 2. Insecure Temporary Directory Usage +Hardcoding `/tmp` is flagged by security scanners as it can lead to symlink attacks or predictable paths on shared systems. +- `openclaw/fetchai-openclaw-orchestrator/connector/policy.py`: Hardcodes `"/tmp"`. +**Recommendation**: Use `tempfile.gettempdir()` instead. -### Actual behavior -1. **Flask `debug=True` enabled (High Severity):** `frontend-integration/frontend_app.py:131` runs Flask with `debug=True`, which exposes an interactive debugger potentially allowing Remote Code Execution (RCE). -2. **Vulnerable NPM Dependencies (High Severity):** `frontend-integration/frontend-integration/package.json` uses outdated versions of `axios`, `next`, and other packages containing known SSRF, DoS, and Prototype Pollution vulnerabilities. -3. **Missing Request Timeouts (Medium Severity):** `web3/internet-computer/fetch/agent.py` and other agents call `requests.post()` and `requests.get()` without `timeout` parameters, risking denial of service if the endpoint hangs. -4. **Silent Error Suppression (Low Severity):** `video-to-map-agent/pdf_generator_agent.py` and `video-to-map-agent/weather_monitor_agent.py` globally suppress errors via `except Exception: pass`. - -### Affected file or folder path -`frontend-integration/frontend_app.py` -`frontend-integration/frontend-integration/package.json` -`web3/internet-computer/fetch/agent.py` -`video-to-map-agent/pdf_generator_agent.py` - -### Logs / traceback -```shell -# Bandit Finding (B201) -B201 (flask_debug_true) - frontend-integration/frontend_app.py:131 -Severity: HIGH - -# NPM Audit -9 vulnerabilities (4 moderate, 5 high) in frontend-integration/frontend-integration -``` - -### Environment -macOS, Python 3.11, Automated Scanner +### 3. Vulnerable `urllib.urlopen` Usage +The `urllib.request.urlopen` function is prone to protocol injection vulnerabilities if the URL is somewhat dynamic. +- `openclaw/agentverse-caller/scripts/call.py` +- `Claude Agent SDK/real-estate-search-agent/sheets.py` +**Recommendation**: Use the safer `requests` library instead of `urllib`. diff --git a/new_issue_report.md b/new_issue_report.md new file mode 100644 index 00000000..289414e7 --- /dev/null +++ b/new_issue_report.md @@ -0,0 +1,24 @@ +## Title +High/Medium Severity Security Vulnerabilities (RCE via `eval`, Insecure Temp Dir) + +## Description +A secondary security audit using `bandit` revealed additional vulnerabilities in this repository that should be addressed. + +### 1. Remote Code Execution (RCE) via `eval()` +Using `eval()` on untrusted input from an LLM or user allows arbitrary Python code execution. +- `Crewai-agents/trip_planner/tools/calculator_tools.py`: The `calculate` tool calls `eval(operation)` on raw string inputs. +- `anthropic-quickstart/03-function-calling-agent/claude_function_agent.py`: Uses `eval()` for math evaluation. While `__builtins__` is cleared, `eval` is notoriously difficult to sandbox and can still be bypassed. +**Recommendation**: Use `ast.literal_eval`, a secure math parser, or validate the input characters strictly before evaluation. + +### 2. Insecure Temporary Directory Usage +Hardcoding `/tmp` is flagged by security scanners as it can lead to symlink attacks or predictable paths on shared systems. +- `openclaw/fetchai-openclaw-orchestrator/connector/policy.py`: Hardcodes `"/tmp"`. +**Recommendation**: Use `tempfile.gettempdir()` instead. + +### 3. Vulnerable `urllib.urlopen` Usage +The `urllib.request.urlopen` function is prone to protocol injection vulnerabilities. +- `openclaw/agentverse-caller/scripts/call.py` +- `Claude Agent SDK/real-estate-search-agent/sheets.py` +**Recommendation**: Replace `urllib` requests with the safer `requests` library where feasible, or enforce strict URL validation. + +I will be submitting a PR to fix these issues shortly. From 5d95accbda018cca9ddd6840957c7d54e6dd9482 Mon Sep 17 00:00:00 2001 From: HarshavardhanVemali Date: Tue, 16 Jun 2026 07:35:31 +0530 Subject: [PATCH 3/3] Fix CI issues: linting, formatting, and changelog --- .../Nike-products-agent/agent.py | 38 +- .../Nike-products-agent/chat_proto.py | 23 +- .../Nike-products-agent/nike_scraper.py | 585 +++++++---- CHANGELOG.md | 3 + .../real-estate-search-agent/agent.py | 120 ++- .../real-estate-search-agent/scraper.py | 14 +- .../real-estate-search-agent/sheets.py | 12 +- .../stripe_payments.py | 4 +- .../real-estate-search-agent/workflow.py | 149 ++- Composio/gmail-agent/agent.py | 2 +- Composio/gmail-agent/formatting.py | 80 +- Composio/gmail-agent/llm.py | 18 +- Composio/gmail-agent/system_prompt.py | 2 - Composio/gmail-agent/tools.py | 11 +- Composio/gmail-agent/uagent_app.py | 391 ++++++-- Composio/linkedln/agent.py | 281 ++++-- .../scripts/agents.py | 18 +- .../scripts/main.py | 31 +- .../scripts/pdf_downloader.py | 7 +- .../scripts/pdf_to_text.py | 8 +- .../scripts/tasks.py | 14 +- .../Prep-for-a-meeting-Agent/agents.py | 67 +- .../Prep-for-a-meeting-Agent/tasks.py | 55 +- .../tools/ExaSearchTool.py | 43 +- .../Prep-for-a-meeting-Agent/uagent_main.py | 94 +- .../code_analyzer/agents/code_agents.py | 5 +- Crewai-agents/code_analyzer/main.py | 43 +- .../code_analyzer/tasks/code_tasks.py | 3 +- Crewai-agents/trip_planner/main.py | 8 +- Crewai-agents/trip_planner/main_uagents.py | 10 +- .../trip_planner/tools/browser_tools.py | 8 +- .../trip_planner/tools/search_tools.py | 8 +- Crewai-agents/trip_planner/trip_tasks.py | 12 +- Rag-agent/ango/agent.py | 160 +-- a2a-cart-store/__init__.py | 1 - a2a-cart-store/av_adapter.py | 3 +- a2a-cart-store/skyfire_payment.py | 2 - a2a-cart-store/store_executor.py | 84 +- .../airbnb_agent/airbnb_agent_simple.py | 51 +- .../finance_agent/finance_agent_simple.py | 29 +- .../Multiagent-Planner/host_agent/__main__.py | 67 +- .../host_agent/remote_agent_connection.py | 11 +- .../host_agent/routing_agent.py | 140 ++- .../perplexity_agent_simple.py | 42 +- .../weather_agent/__main__.py | 40 +- .../weather_agent/weather_agent.py | 14 +- .../weather_agent/weather_executor.py | 35 +- .../weather_agent/weather_mcp.py | 82 +- .../Multiagent/agents/__init__.py | 6 +- .../Multiagent/agents/analysis_agent.py | 105 +- .../Multiagent/agents/coding_agent.py | 111 ++- .../Multiagent/agents/research_agent.py | 27 +- .../Multiagent/main.py | 23 +- .../book-agent/books_recommender_agent.py | 39 +- .../book-agent/main.py | 28 +- .../braveagent/brave/agent.py | 106 +- .../braveagent/function.py | 25 +- .../collaboration_team/collaboration_team.py | 64 +- .../collaboration_team/main.py | 40 +- .../competitor_analysis_executor.py | 53 +- .../competitor_analysis_agent/main.py | 48 +- .../langgraph/currency.py | 17 +- .../langgraph/currency_agent_system/agent.py | 111 ++- .../currency_agent_system/agent_executor.py | 28 +- .../shopping_agent/main.py | 39 +- .../shopping_agent/shopping_agent.py | 39 +- .../youtube_summarizer/agent.py | 143 +-- .../youtube_summarizer/agent_executor.py | 26 +- .../youtube_summarizer/main.py | 19 +- .../basic_claude_agent.py | 92 +- .../claude_vision_agent.py | 145 +-- .../claude_function_agent.py | 271 +++-- .../04-mcp-agent/claude_mcp_agent.py | 273 +++--- .../05-multi-agent-system/router_agent.py | 118 +-- asi-cloud-agent/agent.py | 7 +- .../ASI1_Langchain/asi1_langchain.py | 2 +- av-script-example/agent.py | 72 +- deploy-agent-on-av/example/agent.py | 47 +- duffel-agent/agent.py | 10 +- duffel-agent/protocols/chat_proto.py | 161 ++- duffel-agent/protocols/payment_proto.py | 392 ++++++-- duffel-agent/schemas/schemas.py | 2 +- duffel-agent/state_store.py | 10 +- duffel-agent/tools/duffel_tools.py | 330 +++++-- duffel-agent/tools/fet_payments.py | 38 +- duffel-agent/tools/openai_client.py | 405 +++++--- duffel-agent/tools/skyfire.py | 17 +- fet-example/agent.py | 4 +- fet-example/chat_proto.py | 9 +- fet-example/client.py | 23 +- fet-example/payment.py | 105 +- fet-example/shared.py | 2 +- .../alice/alice_fetchai_wrapped_agent.py | 4 +- .../agents/bob/bob_fetchai_wrapped_agent.py | 4 +- .../agents/orchestrator/chat_protocol.py | 4 +- .../orchestrator_fetchai_wrapped_agent.py | 15 +- .../uagent_bridge.py | 20 +- .../workflow.py | 94 +- frontend-integration/frontend_app.py | 133 +-- frontend-integration/product_info_agent.py | 45 +- frontend-integration/product_search_agent.py | 32 +- .../basic_gemini_agent.py | 90 +- .../02-imagen-agent/imagen_agent.py | 148 +-- .../03-veo-video-agent/veo_agent.py | 180 ++-- .../04-lyria-music-agent/lyria_agent.py | 157 +-- .../05-tts-speech-agent/tts_agent.py | 194 ++-- .../06-web-research-agent/research_agent.py | 106 +- .../unified-movie-agent/config.py | 8 +- .../unified-movie-agent/main.py | 32 +- .../unified-movie-agent/main_stripe.py | 125 ++- .../unified-movie-agent/models.py | 16 +- .../unified-movie-agent/pipeline/assembly.py | 51 +- .../unified-movie-agent/pipeline/chargen.py | 30 +- .../unified-movie-agent/pipeline/creative.py | 64 +- .../unified-movie-agent/pipeline/music.py | 13 +- .../pipeline/orchestrator.py | 41 +- .../unified-movie-agent/pipeline/safety.py | 6 +- .../unified-movie-agent/pipeline/stitcher.py | 36 +- .../unified-movie-agent/pipeline/tts.py | 28 +- .../unified-movie-agent/pipeline/video.py | 30 +- .../unified-movie-agent/utils/gcs.py | 14 +- .../unified-movie-agent/utils/retry.py | 11 +- .../brand-management-agent/agent.py | 178 ++-- image-agent-payment-protocol/agent.py | 17 +- image-agent-payment-protocol/chat_proto.py | 20 +- image-agent-payment-protocol/payment_proto.py | 67 +- image-agent-payment-protocol/skyfire.py | 28 +- .../launch_your_a2a_agent.py | 16 +- launch-your-a2a-research-team/common.py | 4 +- .../orchestrator_agent.py | 7 +- llama-index/handlers.py | 67 +- llama-index/payment.py | 18 +- llama-index/rag.py | 46 +- llama-index/stripe_payments.py | 1 + mcp-agents/Context7-MCP-Agent/agent.py | 384 ++++---- mcp-agents/Github MCP Agent/agent.py | 418 ++++---- mcp-agents/Perplexity MCP Agent/agent.py | 159 +-- mcp-agents/airbnb-mcp-agent/agent.py | 198 ++-- mcp-agents/calendar_chat_uagent/__init__.py | 2 +- .../calendar_chat_agent.py | 60 +- .../calendar_chat_proto.py | 133 ++- .../calendar_chat_uagent/oauth_server.py | 37 +- mcp-agents/calendar_chat_uagent/server.py | 58 +- mcp-agents/events-finder-mcp-agent/agent.py | 6 +- .../events-finder-mcp-agent/new-adapter.py | 124 ++- mcp-agents/events-finder-mcp-agent/server.py | 29 +- mcp-agents/gmail_chat_uagent/__init__.py | 4 +- .../gmail_chat_uagent/gmail_chat_agent.py | 63 +- .../gmail_chat_uagent/gmail_chat_proto.py | 254 +++-- mcp-agents/gmail_chat_uagent/oauth_server.py | 55 +- mcp-agents/gmail_chat_uagent/server.py | 140 ++- .../Scholarship-finder/test_workflow.py | 40 +- .../Scholarship-finder/uagent_bridge.py | 103 +- .../Scholarship-finder/workflow.py | 27 +- pdf-summariser-example/agent.py | 1 - pdf-summariser-example/chat_proto.py | 13 +- pdf-summariser-example/utils.py | 59 +- stripe-horoscope-agent/agent.py | 1 - stripe-horoscope-agent/chat_proto.py | 1 - stripe-horoscope-agent/config.py | 9 +- stripe-horoscope-agent/handlers.py | 72 +- stripe-horoscope-agent/llm.py | 36 +- stripe-horoscope-agent/payment_proto.py | 1 - stripe-horoscope-agent/state.py | 9 +- stripe-horoscope-agent/stripe_payments.py | 10 +- .../expense-calculator-group/agent.py | 400 ++++++-- .../expense-calculator-group/config.py | 10 +- .../expense-calculator-group/expense_logic.py | 38 +- .../expense-calculator-group/payment_proto.py | 1 + .../receipt_vision.py | 2 + .../stripe_payments.py | 7 +- web3/internet-computer/fetch/agent.py | 99 +- .../agent.py | 45 +- .../metta/investment_rag.py | 62 +- .../metta/knowledge.py | 257 ++++- web3/singularity-net-metta-fetch.ai/agent.py | 38 +- .../metta/knowledge.py | 927 +++++++++++++++--- .../metta/test.py | 10 +- web3/singularity-net-metta/agent.py | 47 +- web3/singularity-net-metta/metta/knowledge.py | 107 +- .../singularity-net-metta/metta/medicalrag.py | 34 +- 181 files changed, 8704 insertions(+), 4646 deletions(-) diff --git a/Browser-based-agents/Notte-labs-agent/Nike-products-agent/agent.py b/Browser-based-agents/Notte-labs-agent/Nike-products-agent/agent.py index f1e21d16..d885f372 100644 --- a/Browser-based-agents/Notte-labs-agent/Nike-products-agent/agent.py +++ b/Browser-based-agents/Notte-labs-agent/Nike-products-agent/agent.py @@ -7,7 +7,6 @@ import os from enum import Enum -import asyncio from uagents import Agent, Context, Model from uagents.experimental.quota import QuotaProtocol, RateLimit @@ -17,11 +16,7 @@ from nike_scraper import get_nike_info_enhanced, NikeScrapeRequest, NikeScrapeResponse # Create the agent with mailbox=True for ASI1 discoverability -agent = Agent( - name="nike_scraper", - port=8000, - mailbox=True -) +agent = Agent(name="nike_scraper", port=8000, mailbox=True) # Print the agent's address for reference print(f"Your Nike Scraper agent's address is: {agent.address}") @@ -34,29 +29,31 @@ default_rate_limit=RateLimit(window_size_minutes=60, max_requests=30), ) + # Handle direct Nike scraper requests (without natural language processing) -@proto.on_message( - NikeScrapeRequest, replies={NikeScrapeResponse, ErrorMessage} -) +@proto.on_message(NikeScrapeRequest, replies={NikeScrapeResponse, ErrorMessage}) async def handle_request(ctx: Context, sender: str, msg: NikeScrapeRequest): """ Handle direct structured requests for Nike product information. This allows other agents to communicate directly with this agent using the NikeScrapeRequest model. """ - ctx.logger.info(f"Received Nike scrape request: action={msg.action}, category={msg.category_name}") - + ctx.logger.info( + f"Received Nike scrape request: action={msg.action}, category={msg.category_name}" + ) + try: # Process the request using the nike_scraper module nike_response = await get_nike_info_enhanced(request=msg) - - ctx.logger.info(f"Successfully processed Nike request") + + ctx.logger.info("Successfully processed Nike request") await ctx.send(sender, NikeScrapeResponse(results=nike_response.results)) - + except Exception as err: ctx.logger.error(f"Error in handle_request: {err}") await ctx.send(sender, ErrorMessage(error=str(err))) + # Health check functionality def agent_is_healthy() -> bool: """ @@ -65,26 +62,32 @@ def agent_is_healthy() -> bool: """ try: import requests + response = requests.get("https://www.nike.com", timeout=5) return response.status_code == 200 except Exception: return False + class HealthCheck(Model): pass + class HealthStatus(str, Enum): HEALTHY = "healthy" UNHEALTHY = "unhealthy" + class AgentHealth(Model): agent_name: str status: HealthStatus + health_protocol = QuotaProtocol( storage_reference=agent.storage, name="HealthProtocol", version="0.1.0" ) + @health_protocol.on_message(HealthCheck, replies={AgentHealth}) async def handle_health_check(ctx: Context, sender: str, msg: HealthCheck): """Handle health check requests to monitor agent status.""" @@ -97,6 +100,7 @@ async def handle_health_check(ctx: Context, sender: str, msg: HealthCheck): finally: await ctx.send(sender, AgentHealth(agent_name="nike_scraper", status=status)) + # Register all protocols with the agent agent.include(proto, publish_manifest=True) agent.include(health_protocol, publish_manifest=True) @@ -108,10 +112,12 @@ async def handle_health_check(ctx: Context, sender: str, msg: HealthCheck): if not os.environ.get("NOTTE_API_KEY"): print("Warning: NOTTE_API_KEY environment variable is not set.") print("The agent will run but scraping functionality will be limited.") - + # Run the agent print("Starting Nike Scraper Agent...") - print("This agent can respond to natural language queries about Nike products via ASI1 LLM.") + print( + "This agent can respond to natural language queries about Nike products via ASI1 LLM." + ) print("Example queries:") print("- Show me Nike categories") print("- List Nike basketball shoes") diff --git a/Browser-based-agents/Notte-labs-agent/Nike-products-agent/chat_proto.py b/Browser-based-agents/Notte-labs-agent/Nike-products-agent/chat_proto.py index 9db3c7e4..3692676a 100644 --- a/Browser-based-agents/Notte-labs-agent/Nike-products-agent/chat_proto.py +++ b/Browser-based-agents/Notte-labs-agent/Nike-products-agent/chat_proto.py @@ -18,12 +18,12 @@ chat_protocol_spec, ) -from nike_scraper import get_nike_info_enhanced, NikeScrapeRequest, NikeScrapeResponse +from nike_scraper import get_nike_info_enhanced, NikeScrapeRequest # Replace the AI Agent Address with one of the LLMs that support StructuredOutput # OpenAI Agent: agent1q0h70caed8ax769shpemapzkyk65uscw4xwk6dc4t3emvp5jdcvqs9xs32y # Claude.ai Agent: agent1qvk7q2av3e2y5gf5s90nfzkc8a48q3wdqeevwrtgqfdl0k78rspd6f2l4dx -AI_AGENT_ADDRESS = 'agent1qtlpfshtlcxekgrfcpmv7m9zpajuwu7d5jfyachvpa4u3dkt6k0uwwp2lct' +AI_AGENT_ADDRESS = "agent1qtlpfshtlcxekgrfcpmv7m9zpajuwu7d5jfyachvpa4u3dkt6k0uwwp2lct" if not AI_AGENT_ADDRESS: raise ValueError("AI_AGENT_ADDRESS not set") @@ -69,7 +69,9 @@ async def handle_message(ctx: Context, sender: str, msg: ChatMessage): ctx.storage.set(str(ctx.session), sender) await ctx.send( sender, - ChatAcknowledgement(timestamp=datetime.utcnow(), acknowledged_msg_id=msg.msg_id), + ChatAcknowledgement( + timestamp=datetime.utcnow(), acknowledged_msg_id=msg.msg_id + ), ) for item in msg.content: @@ -110,7 +112,12 @@ async def handle_structured_output_response( return # Check for empty or clearly invalid output from LLM - if not msg.output or "" in str(msg.output) or not isinstance(msg.output, dict) or not msg.output.get('action'): + if ( + not msg.output + or "" in str(msg.output) + or not isinstance(msg.output, dict) + or not msg.output.get("action") + ): await ctx.send( session_sender, create_text_chat( @@ -123,7 +130,7 @@ async def handle_structured_output_response( # Parse the structured output from the LLM try: request = NikeScrapeRequest.parse_obj(msg.output) - except Exception as p_err: # Catch Pydantic validation errors or others + except Exception as p_err: # Catch Pydantic validation errors or others ctx.logger.error(f"Error parsing LLM output: {msg.output}. Error: {p_err}") await ctx.send( session_sender, @@ -132,14 +139,14 @@ async def handle_structured_output_response( ), ) return - + # Get the Nike information based on the structured request nike_response = await get_nike_info_enhanced(request=request) - + # Send the response back to the user chat_message = create_text_chat(nike_response.results) await ctx.send(session_sender, chat_message) - + except Exception as err: ctx.logger.error(f"Error processing Nike request: {err}") await ctx.send( diff --git a/Browser-based-agents/Notte-labs-agent/Nike-products-agent/nike_scraper.py b/Browser-based-agents/Notte-labs-agent/Nike-products-agent/nike_scraper.py index 159695da..44b3cc90 100644 --- a/Browser-based-agents/Notte-labs-agent/Nike-products-agent/nike_scraper.py +++ b/Browser-based-agents/Notte-labs-agent/Nike-products-agent/nike_scraper.py @@ -6,14 +6,14 @@ from pathlib import Path from dotenv import load_dotenv -load_dotenv() # Load environment variables from .env file -from typing import Optional, List, Dict, Any -from enum import Enum as PythonEnum +load_dotenv() # Load environment variables from .env file +from typing import Optional, List, Dict, Any # noqa: E402 +from enum import Enum as PythonEnum # noqa: E402 -from notte_sdk import NotteClient, RemoteSession, retry -from pydantic import BaseModel, Field -from uagents import Model -from urllib.parse import urljoin +from notte_sdk import NotteClient, RemoteSession, retry # noqa: E402 +from pydantic import BaseModel, Field # noqa: E402 +from uagents import Model # noqa: E402 +from urllib.parse import urljoin # noqa: E402 # Config RESULT_DIR = Path("nike_results") @@ -24,81 +24,138 @@ # ENUMS and MODELS for Scraping Logic # ############################################ + class GenderTarget(str, PythonEnum): MEN = "men" WOMEN = "women" KIDS = "kids" UNISEX = "unisex" + class SortOption(str, PythonEnum): PRICE_ASC = "price_asc" PRICE_DESC = "price_desc" NEWEST = "newest" BEST_SELLING = "best_selling" - RELEVANCE = "relevance" # Default for search + RELEVANCE = "relevance" # Default for search + class ProductCategory(BaseModel): name: str = Field(description="The name of the product category") url: str = Field(description="The URL of the product category") - menu: Optional[str] = Field(None, description="The menu group of the product category (e.g., 'Shoes', 'Clothing')") - parent_category: Optional[str] = Field(None, description="The parent category if this is a sub-category") - filters_applied: Optional[Dict[str, Any]] = Field(None, description="Any filters applied to get this category list (e.g., color, size)") + menu: Optional[str] = Field( + None, + description="The menu group of the product category (e.g., 'Shoes', 'Clothing')", + ) + parent_category: Optional[str] = Field( + None, description="The parent category if this is a sub-category" + ) + filters_applied: Optional[Dict[str, Any]] = Field( + None, + description="Any filters applied to get this category list (e.g., color, size)", + ) + class ProductCategories(BaseModel): categories: List[ProductCategory] - source_url: str = Field(description="The URL from which these categories were scraped") + source_url: str = Field( + description="The URL from which these categories were scraped" + ) @staticmethod def example(): return ProductCategories( categories=[ - ProductCategory(name="Running Shoes", menu="Shoes", url=f"{NIKE_BASE_URL}/w/mens-running-shoes-37v7jzy7ok"), - ProductCategory(name="Lifestyle Sneakers", menu="Shoes", url=f"{NIKE_BASE_URL}/w/mens-lifestyle-shoes-13jrmznik1"), - ProductCategory(name="Basketball Shorts", menu="Clothing", parent_category="Shorts", url=f"{NIKE_BASE_URL}/w/mens-basketball-shorts-38fphz98uk"), + ProductCategory( + name="Running Shoes", + menu="Shoes", + url=f"{NIKE_BASE_URL}/w/mens-running-shoes-37v7jzy7ok", + ), + ProductCategory( + name="Lifestyle Sneakers", + menu="Shoes", + url=f"{NIKE_BASE_URL}/w/mens-lifestyle-shoes-13jrmznik1", + ), + ProductCategory( + name="Basketball Shorts", + menu="Clothing", + parent_category="Shorts", + url=f"{NIKE_BASE_URL}/w/mens-basketball-shorts-38fphz98uk", + ), ], - source_url=f"{NIKE_BASE_URL}/men" + source_url=f"{NIKE_BASE_URL}/men", ) + class ShoppingItem(BaseModel): name: str = Field(description="The name of the product") - price: Optional[float] = Field(None, description="The price of the product. Can be null if price varies or is not listed.") - original_price: Optional[float] = Field(None, description="The original price if the item is on sale") + price: Optional[float] = Field( + None, + description="The price of the product. Can be null if price varies or is not listed.", + ) + original_price: Optional[float] = Field( + None, description="The original price if the item is on sale" + ) currency: str = Field("USD", description="Currency of the price (e.g., USD, EUR)") url: str = Field(description="The URL of the product page") - image_src: Optional[str] = Field(None, description="The URL or src path to the product image") - category: Optional[str] = Field(None, description="The category the product belongs to") - product_id: Optional[str] = Field(None, description="Unique product identifier if available") - available_sizes: Optional[List[str]] = Field(None, description="List of available sizes") + image_src: Optional[str] = Field( + None, description="The URL or src path to the product image" + ) + category: Optional[str] = Field( + None, description="The category the product belongs to" + ) + product_id: Optional[str] = Field( + None, description="Unique product identifier if available" + ) + available_sizes: Optional[List[str]] = Field( + None, description="List of available sizes" + ) color: Optional[str] = Field(None, description="Primary color of the product shown") - rating: Optional[float] = Field(None, description="Average customer rating (e.g., 4.5)") + rating: Optional[float] = Field( + None, description="Average customer rating (e.g., 4.5)" + ) review_count: Optional[int] = Field(None, description="Number of customer reviews") + class ShoppingList(BaseModel): items: List[ShoppingItem] - category_scraped: Optional[str] = Field(None, description="The specific category these items belong to") - total_items_found: Optional[int] = Field(None, description="Total items found for the query, even if not all are listed") - current_page: Optional[int] = Field(1, description="Current page number if paginated") - total_pages: Optional[int] = Field(None, description="Total pages available if paginated") + category_scraped: Optional[str] = Field( + None, description="The specific category these items belong to" + ) + total_items_found: Optional[int] = Field( + None, description="Total items found for the query, even if not all are listed" + ) + current_page: Optional[int] = Field( + 1, description="Current page number if paginated" + ) + total_pages: Optional[int] = Field( + None, description="Total pages available if paginated" + ) source_url: str = Field(description="The URL from which these items were scraped") + # ############################################ # Request/Response Models for uAgent # ############################################ + class NikeAction(str, PythonEnum): # Category focused - GET_MAIN_CATEGORIES = "get_main_categories" # e.g., Men's Shoes, Women's Clothing - GET_SUB_CATEGORIES = "get_sub_categories" # e.g., Men's Running Shoes under Men's Shoes + GET_MAIN_CATEGORIES = "get_main_categories" # e.g., Men's Shoes, Women's Clothing + GET_SUB_CATEGORIES = ( + "get_sub_categories" # e.g., Men's Running Shoes under Men's Shoes + ) # Product focused LIST_PRODUCTS_BY_CATEGORY = "list_products_by_category" - SEARCH_PRODUCTS = "search_products" # General search by keyword - GET_PRODUCT_DETAILS = "get_product_details" # For a specific product URL or name + SEARCH_PRODUCTS = "search_products" # General search by keyword + GET_PRODUCT_DETAILS = "get_product_details" # For a specific product URL or name # Potentially more complex - FIND_BESTSELLERS = "find_bestsellers" # In a category or overall - FIND_NEW_ARRIVALS = "find_new_arrivals" # In a category or overall + FIND_BESTSELLERS = "find_bestsellers" # In a category or overall + FIND_NEW_ARRIVALS = "find_new_arrivals" # In a category or overall # Simplified for direct use / backward compatibility - LIST_CATEGORIES = "list_categories" # Alias for GET_MAIN_CATEGORIES - LIST_PRODUCTS = "list_products" # Alias for LIST_PRODUCTS_BY_CATEGORY (default to men's general) + LIST_CATEGORIES = "list_categories" # Alias for GET_MAIN_CATEGORIES + LIST_PRODUCTS = "list_products" # Alias for LIST_PRODUCTS_BY_CATEGORY (default to men's general) + class NikeScrapeRequest(Model): action: NikeAction @@ -113,22 +170,27 @@ class NikeScrapeRequest(Model): page_number: Optional[int] = 1 max_items: Optional[int] = 10 + class NikeScrapeResponse(Model): - results: str # This will contain the formatted string response + results: str # This will contain the formatted string response # Optional raw structured data for debugging or complex use cases, not sent to user by default raw_data: Optional[Dict[str, Any]] = None + # ############################################ # Helper Functions # ############################################ + def _build_nike_url( gender: GenderTarget = GenderTarget.MEN, - category_path: Optional[str] = None, # e.g., "w/mens-running-shoes-37v7jzy7ok" or "w/new-3n82y" + category_path: Optional[ + str + ] = None, # e.g., "w/mens-running-shoes-37v7jzy7ok" or "w/new-3n82y" search_query: Optional[str] = None, - filters: Optional[Dict[str, str]] = None, # e.g. {"color": "blue", "size": "10"} + filters: Optional[Dict[str, str]] = None, # e.g. {"color": "blue", "size": "10"} sort_by: Optional[SortOption] = None, - page: Optional[int] = None + page: Optional[int] = None, ) -> str: """Constructs a Nike URL based on provided parameters.""" if search_query: @@ -146,34 +208,39 @@ def _build_nike_url( # Nike uses a specific format for filters, often like /color-blue/size-10 # This is a simplified version, actual Nike filter params can be complex # For now, we'll assume filters are appended if the Notte agent is instructed to apply them - pass # Placeholder for more complex filter URL construction if needed + pass # Placeholder for more complex filter URL construction if needed if sort_by: # Nike sort params vary, e.g., "&sort=priceAsc" or part of path # For now, we'll assume Notte agent handles sorting via instructions - pass # Placeholder + pass # Placeholder if page and page > 1: # Nike pagination can be /page/2 or ?page=2 # Assuming Notte agent handles this via instructions for now - pass # Placeholder + pass # Placeholder query_string = "&".join(params) return f"{base}{'?' if query_string and '?' not in base else ''}{'&' if query_string and '?' in base else ''}{query_string}" + async def _scrape_nike_data_with_notte( url: str, task_description: str, output_model: type[BaseModel], session: RemoteSession, - max_steps: int = 25 # Increased default max_steps + max_steps: int = 25, # Increased default max_steps ) -> Optional[BaseModel]: """Generic function to scrape data from a Nike URL using Notte agent and parse to a Pydantic model.""" try: print(f"Attempting to scrape URL: {url} with task: {task_description[:100]}...") - response = NotteClient(api_key=NOTTE_API_KEY).Agent(session=session, max_steps=max_steps).run( - task=task_description, - url=url, + response = ( + NotteClient(api_key=NOTTE_API_KEY) + .Agent(session=session, max_steps=max_steps) + .run( + task=task_description, + url=url, + ) ) if response.answer: @@ -184,32 +251,42 @@ async def _scrape_nike_data_with_notte( except Exception as val_err_direct: # If direct validation fails, try to find JSON within a markdown code block import re - import json + match = re.search(r"```json\n(.*?)\n```", response.answer, re.DOTALL) if match: json_str = match.group(1) try: return output_model.model_validate_json(json_str) except Exception as val_err_md: - print(f"Error validating JSON from markdown for {url}: {val_err_md}. JSON string: {json_str[:200]}...") + print( + f"Error validating JSON from markdown for {url}: {val_err_md}. JSON string: {json_str[:200]}..." + ) return None else: - print(f"Error validating direct JSON for {url}: {val_err_direct}. No markdown JSON found. Answer: {response.answer[:200]}...") + print( + f"Error validating direct JSON for {url}: {val_err_direct}. No markdown JSON found. Answer: {response.answer[:200]}..." + ) return None else: - print(f"No answer received from Notte agent for URL: {url}. Error: {response.error if hasattr(response, 'error') else 'Unknown'}") + print( + f"No answer received from Notte agent for URL: {url}. Error: {response.error if hasattr(response, 'error') else 'Unknown'}" + ) return None except Exception as e: print(f"Exception during Notte scraping for {url}: {e}") return None + # ############################################ # Scraping Functions (using the new models) # ############################################ + @retry(max_tries=3) -async def scrape_main_categories(session: RemoteSession, gender: GenderTarget = GenderTarget.MEN) -> Optional[ProductCategories]: - """Fetch main Nike product categories for a given gender.""" +async def scrape_main_categories( + session: RemoteSession, gender: GenderTarget = GenderTarget.MEN +) -> Optional[ProductCategories]: + """Fetch main Nike product categories for a given gender.""" target_url = _build_nike_url(gender=gender) task = f""" 1. Go to {target_url}. @@ -221,26 +298,31 @@ async def scrape_main_categories(session: RemoteSession, gender: GenderTarget = Make sure the 'source_url' field in the response is set to '{target_url}'. Example of a successful output category item: {{"name": "Men's New Arrivals", "url": "{NIKE_BASE_URL}/w/mens-new-arrivals-3n82y", "menu": "Featured"}} """ - return await _scrape_nike_data_with_notte(target_url, task, ProductCategories, session) + return await _scrape_nike_data_with_notte( + target_url, task, ProductCategories, session + ) + -@retry(max_tries=2) # Products can be more volatile +@retry(max_tries=2) # Products can be more volatile async def scrape_products_by_category( - category_url: str, - category_name: str, - session: RemoteSession, + category_url: str, + category_name: str, + session: RemoteSession, max_items: int = 10, filters: Optional[Dict[str, Any]] = None, - sort_by: Optional[SortOption] = None + sort_by: Optional[SortOption] = None, ) -> Optional[ShoppingList]: """Scrapes products for a single category URL.""" filter_instructions = "" if filters: - filter_str = ", ".join([f"{k} as {v}" for k,v in filters.items()]) + filter_str = ", ".join([f"{k} as {v}" for k, v in filters.items()]) filter_instructions = f"Apply filters: {filter_str}. " - + sort_instructions = "" if sort_by: - sort_instructions = f"Sort the products by {sort_by.name.lower().replace('_', ' ')}. " + sort_instructions = ( + f"Sort the products by {sort_by.name.lower().replace('_', ' ')}. " + ) task = f""" 1. Go to the category page: {category_url}. @@ -254,26 +336,33 @@ async def scrape_products_by_category( """ return await _scrape_nike_data_with_notte(category_url, task, ShoppingList, session) + @retry(max_tries=2) async def search_nike_products( - search_term: str, - session: RemoteSession, - gender: GenderTarget = GenderTarget.MEN, + search_term: str, + session: RemoteSession, + gender: GenderTarget = GenderTarget.MEN, max_items: int = 10, filters: Optional[Dict[str, Any]] = None, - sort_by: Optional[SortOption] = None + sort_by: Optional[SortOption] = None, ) -> Optional[ShoppingList]: """Searches for products on Nike.com.""" - target_url = _build_nike_url(search_query=search_term, gender=gender) # Search URL might not always include gender directly - + target_url = _build_nike_url( + search_query=search_term, gender=gender + ) # Search URL might not always include gender directly + filter_instructions = "" if filters: - filter_str = ", ".join([f"{k} as {v}" for k,v in filters.items()]) - filter_instructions = f"After searching, if possible, apply filters: {filter_str}. " + filter_str = ", ".join([f"{k} as {v}" for k, v in filters.items()]) + filter_instructions = ( + f"After searching, if possible, apply filters: {filter_str}. " + ) sort_instructions = "" if sort_by: - sort_instructions = f"Sort the search results by {sort_by.name.lower().replace('_', ' ')}. " + sort_instructions = ( + f"Sort the search results by {sort_by.name.lower().replace('_', ' ')}. " + ) task = f""" 1. Go to Nike.com and search for '{search_term}'. You can use this URL as a starting point: {target_url} @@ -286,12 +375,23 @@ async def search_nike_products( """ return await _scrape_nike_data_with_notte(target_url, task, ShoppingList, session) + @retry(max_tries=2) -async def scrape_product_details(product_url_or_name: str, session: RemoteSession, is_url: bool) -> Optional[ShoppingItem]: +async def scrape_product_details( + product_url_or_name: str, session: RemoteSession, is_url: bool +) -> Optional[ShoppingItem]: """Fetches detailed information for a single Nike product, either by URL or by searching its name.""" - target_url = product_url_or_name if is_url else _build_nike_url(search_query=product_url_or_name) - - initial_step = f"Go to the product page: {target_url}." if is_url else f"Search for the product '{product_url_or_name}' on Nike.com (you can start at {target_url}) and navigate to its main product page." + target_url = ( + product_url_or_name + if is_url + else _build_nike_url(search_query=product_url_or_name) + ) + + initial_step = ( + f"Go to the product page: {target_url}." + if is_url + else f"Search for the product '{product_url_or_name}' on Nike.com (you can start at {target_url}) and navigate to its main product page." + ) task = f""" 1. {initial_step} @@ -301,6 +401,7 @@ async def scrape_product_details(product_url_or_name: str, session: RemoteSessio Ensure the 'url' field is the canonical product page URL.\n""" return await _scrape_nike_data_with_notte(target_url, task, ShoppingItem, session) + async def scrape_sub_categories( main_category_url: str, main_category_name: str, @@ -332,7 +433,7 @@ class NotteSubCategoryList(BaseModel): task_description=task_description, output_model=NotteSubCategoryList, session=session, - max_steps=20 + max_steps=20, ) if raw_scraped_data and raw_scraped_data.categories: @@ -341,34 +442,48 @@ class NotteSubCategoryList(BaseModel): cat_url = cat_data.url if not cat_url.startswith("http"): cat_url = urljoin(NIKE_BASE_URL, cat_url) - + sub_categories_list.append( ProductCategory( name=cat_data.name, url=cat_url, parent_category=main_category_name, - menu=main_category_name + menu=main_category_name, ) ) - if not sub_categories_list: # If parsing resulted in an empty list but raw_data had categories - print(f"Sub-category parsing resulted in empty list for {main_category_name} at {main_category_url}, though Notte might have returned data.") + if ( + not sub_categories_list + ): # If parsing resulted in an empty list but raw_data had categories + print( + f"Sub-category parsing resulted in empty list for {main_category_name} at {main_category_url}, though Notte might have returned data." + ) return None - return ProductCategories(categories=sub_categories_list, source_url=main_category_url) + return ProductCategories( + categories=sub_categories_list, source_url=main_category_url + ) else: - print(f"No sub-categories found or failed to parse for {main_category_name} at {main_category_url}") + print( + f"No sub-categories found or failed to parse for {main_category_name} at {main_category_url}" + ) return None except Exception as e: - print(f"Error scraping sub-categories for {main_category_name} from {main_category_url}: {e}") + print( + f"Error scraping sub-categories for {main_category_name} from {main_category_url}: {e}" + ) return None + # ############################################ # Main uAgent Request Handler Function # ############################################ + async def get_nike_info_enhanced(request: NikeScrapeRequest) -> NikeScrapeResponse: """Main function to handle Nike scraping requests using Notte SDK with headless=False sessions.""" if not NOTTE_API_KEY: - return NikeScrapeResponse(results="Error: NOTTE_API_KEY is not set. Please configure it.") + return NikeScrapeResponse( + results="Error: NOTTE_API_KEY is not set. Please configure it." + ) notte = NotteClient(api_key=NOTTE_API_KEY) response_str = "Could not process the request. Please try again." @@ -378,202 +493,329 @@ async def get_nike_info_enhanced(request: NikeScrapeRequest) -> NikeScrapeRespon # Use headless=False for all Notte sessions as per previous successful patterns with notte.Session(headless=False) as session: action = request.action - max_items_to_return = request.max_items if request.max_items and request.max_items > 0 else 5 - - if action == NikeAction.GET_MAIN_CATEGORIES or action == NikeAction.LIST_CATEGORIES: - categories_data = await scrape_main_categories(session=session, gender=request.gender) + max_items_to_return = ( + request.max_items if request.max_items and request.max_items > 0 else 5 + ) + + if ( + action == NikeAction.GET_MAIN_CATEGORIES + or action == NikeAction.LIST_CATEGORIES + ): + categories_data = await scrape_main_categories( + session=session, gender=request.gender + ) if categories_data and categories_data.categories: response_str = f"Found {len(categories_data.categories)} main categories for {request.gender.value} from {categories_data.source_url}:\n" - response_str += "\n".join([f"- {cat.name} ({cat.menu if cat.menu else 'General'}) - URL: {cat.url}" for cat in categories_data.categories[:max_items_to_return]]) + response_str += "\n".join( + [ + f"- {cat.name} ({cat.menu if cat.menu else 'General'}) - URL: {cat.url}" + for cat in categories_data.categories[:max_items_to_return] + ] + ) if len(categories_data.categories) > max_items_to_return: response_str += f"\n... and {len(categories_data.categories) - max_items_to_return} more." raw_data_response = categories_data.model_dump() else: response_str = f"No main categories found for {request.gender.value} or an error occurred." - - elif action == NikeAction.LIST_PRODUCTS_BY_CATEGORY or action == NikeAction.LIST_PRODUCTS: - category_to_process = request.category_name.strip() if request.category_name else None - actual_category_name_found = None # To store the name of the category as found on Nike's site - target_category_url = None # Initialize target_category_url here + + elif ( + action == NikeAction.LIST_PRODUCTS_BY_CATEGORY + or action == NikeAction.LIST_PRODUCTS + ): + category_to_process = ( + request.category_name.strip() if request.category_name else None + ) + actual_category_name_found = ( + None # To store the name of the category as found on Nike's site + ) + target_category_url = None # Initialize target_category_url here # Clean category_to_process by removing gender specifics if gender is set if category_to_process and request.gender: gender_val_lower = request.gender.value.lower() prefixes_to_remove = [ - f"{gender_val_lower}'s ", - f"{gender_val_lower} ", + f"{gender_val_lower}'s ", + f"{gender_val_lower} ", # Add common variations if needed, e.g., 'male ', 'female ' ] for prefix in prefixes_to_remove: if category_to_process.lower().startswith(prefix): - category_to_process = category_to_process[len(prefix):].strip() - print(f"Cleaned category_to_process to: '{category_to_process}' after removing gender prefix '{prefix.strip()}'") + category_to_process = category_to_process[ + len(prefix) : + ].strip() + print( + f"Cleaned category_to_process to: '{category_to_process}' after removing gender prefix '{prefix.strip()}'" + ) break # If action is LIST_PRODUCTS_BY_CATEGORY, category_name is mandatory - if action == NikeAction.LIST_PRODUCTS_BY_CATEGORY and not category_to_process: + if ( + action == NikeAction.LIST_PRODUCTS_BY_CATEGORY + and not category_to_process + ): response_str = "Error: Category name is required for listing products by category." - return NikeScrapeResponse(results=response_str, raw_data=raw_data_response) + return NikeScrapeResponse( + results=response_str, raw_data=raw_data_response + ) - if not category_to_process: # Implies action == NikeAction.LIST_PRODUCTS and no category given - print(f"No category specified for LIST_PRODUCTS. Attempting to find a default like 'New Arrivals' for {request.gender.value}.") - main_categories_data = await scrape_main_categories(session=session, gender=request.gender) + if ( + not category_to_process + ): # Implies action == NikeAction.LIST_PRODUCTS and no category given + print( + f"No category specified for LIST_PRODUCTS. Attempting to find a default like 'New Arrivals' for {request.gender.value}." + ) + main_categories_data = await scrape_main_categories( + session=session, gender=request.gender + ) if main_categories_data and main_categories_data.categories: # Try to find 'New Arrivals', 'New Releases', etc. - for cat_pattern in ["new arrivals", "new releases", "latest", "just in"]: + for cat_pattern in [ + "new arrivals", + "new releases", + "latest", + "just in", + ]: for cat in main_categories_data.categories: if cat_pattern in cat.name.lower(): target_category_url = cat.url - actual_category_name_found = cat.name - print(f"Found default category: '{actual_category_name_found}' at {target_category_url}") + actual_category_name_found = cat.name + print( + f"Found default category: '{actual_category_name_found}' at {target_category_url}" + ) break - if target_category_url: break - + if target_category_url: + break + # Fallback: if no 'new' type category, try a general one like 'Shoes' or 'Clothing' if not target_category_url: - for cat_pattern in ["shoes", "clothing", "shop all", "all products"]: + for cat_pattern in [ + "shoes", + "clothing", + "shop all", + "all products", + ]: for cat in main_categories_data.categories: if cat_pattern in cat.name.lower(): target_category_url = cat.url actual_category_name_found = cat.name - print(f"Found fallback default category: '{actual_category_name_found}' at {target_category_url}") + print( + f"Found fallback default category: '{actual_category_name_found}' at {target_category_url}" + ) break - if target_category_url: break - - if not target_category_url and main_categories_data.categories: # Still nothing, take the very first one + if target_category_url: + break + + if ( + not target_category_url and main_categories_data.categories + ): # Still nothing, take the very first one target_category_url = main_categories_data.categories[0].url - actual_category_name_found = main_categories_data.categories[0].name - print(f"Using first main category as default: '{actual_category_name_found}' at {target_category_url}") - + actual_category_name_found = ( + main_categories_data.categories[0].name + ) + print( + f"Using first main category as default: '{actual_category_name_found}' at {target_category_url}" + ) + if not target_category_url: response_str = f"Error: Could not determine a default category (e.g., New Arrivals) for {request.gender.value} to list products." - return NikeScrapeResponse(results=response_str, raw_data=raw_data_response) - - else: # A category_name was provided (either by user or set by previous default logic if it were structured differently) + return NikeScrapeResponse( + results=response_str, raw_data=raw_data_response + ) + + else: # A category_name was provided (either by user or set by previous default logic if it were structured differently) # Attempt to find the URL for the given category_to_process - main_categories_data = await scrape_main_categories(session=session, gender=request.gender) + main_categories_data = await scrape_main_categories( + session=session, gender=request.gender + ) if main_categories_data and main_categories_data.categories: for cat in main_categories_data.categories: if category_to_process.lower() in cat.name.lower(): target_category_url = cat.url actual_category_name_found = cat.name - print(f"Found '{actual_category_name_found}' as a main category. URL: {target_category_url}") + print( + f"Found '{actual_category_name_found}' as a main category. URL: {target_category_url}" + ) break - - if not target_category_url and main_categories_data and main_categories_data.categories: - print(f"'{category_to_process}' not found as a main category. Checking sub-categories...") + + if ( + not target_category_url + and main_categories_data + and main_categories_data.categories + ): + print( + f"'{category_to_process}' not found as a main category. Checking sub-categories..." + ) for main_cat in main_categories_data.categories: - print(f"Checking sub-categories under '{main_cat.name}' for '{category_to_process}'...") + print( + f"Checking sub-categories under '{main_cat.name}' for '{category_to_process}'..." + ) sub_categories_data = await scrape_sub_categories( main_category_url=main_cat.url, main_category_name=main_cat.name, session=session, - gender=request.gender + gender=request.gender, ) if sub_categories_data and sub_categories_data.categories: for sub_cat in sub_categories_data.categories: - if category_to_process.lower() in sub_cat.name.lower(): + if ( + category_to_process.lower() + in sub_cat.name.lower() + ): target_category_url = sub_cat.url actual_category_name_found = sub_cat.name - print(f"Found '{actual_category_name_found}' as a sub-category under '{main_cat.name}'. URL: {target_category_url}") + print( + f"Found '{actual_category_name_found}' as a sub-category under '{main_cat.name}'. URL: {target_category_url}" + ) break # Found the sub-category - if target_category_url: # If found in current main_cat's sub-categories + if ( + target_category_url + ): # If found in current main_cat's sub-categories break - + if target_category_url and actual_category_name_found: - print(f"Proceeding to scrape products from: '{actual_category_name_found}' at {target_category_url}") + print( + f"Proceeding to scrape products from: '{actual_category_name_found}' at {target_category_url}" + ) products_data = await scrape_products_by_category( category_url=target_category_url, category_name=actual_category_name_found, session=session, max_items=max_items_to_return, filters=request.filters, - sort_by=request.sort_by + sort_by=request.sort_by, ) if products_data and products_data.items: response_str = f"Found {len(products_data.items)} products in {request.gender.value}'s '{actual_category_name_found}' (from {products_data.source_url}):\n" - for prod in products_data.items: # Already limited by max_items in scrape_products_by_category - price_str = f"Price: {prod.price}" if prod.price else "Price not available" - response_str += f"- {prod.name} - {price_str} - URL: {prod.url}\n" - if products_data.total_items_found and products_data.total_items_found > len(products_data.items): + for prod in products_data.items: # Already limited by max_items in scrape_products_by_category + price_str = ( + f"Price: {prod.price}" + if prod.price + else "Price not available" + ) + response_str += ( + f"- {prod.name} - {price_str} - URL: {prod.url}\n" + ) + if ( + products_data.total_items_found + and products_data.total_items_found + > len(products_data.items) + ): response_str += f"... and more products available (total ~{products_data.total_items_found})." raw_data_response = products_data.model_dump() else: response_str = f"No products found for {request.gender.value}'s '{actual_category_name_found}' at {target_category_url}, or an error occurred during scraping." else: response_str = f"Error: Could not find the category URL for '{category_to_process if category_to_process else 'the default category'}' for {request.gender.value}. Please check the category name or try a broader query." - + elif action == NikeAction.SEARCH_PRODUCTS: if not request.search_term: - return NikeScrapeResponse(results="Error: Search term is required for searching products.") - + return NikeScrapeResponse( + results="Error: Search term is required for searching products." + ) + search_results = await search_nike_products( search_term=request.search_term, session=session, gender=request.gender, max_items=max_items_to_return, filters=request.filters, - sort_by=request.sort_by + sort_by=request.sort_by, ) if search_results and search_results.items: response_str = f"Found {len(search_results.items)} products (showing up to {max_items_to_return}) for search term '{request.search_term}' from {search_results.source_url}:\n" for item in search_results.items: - price_info = f"${item.price:.2f}" if item.price is not None else "Price not available" - response_str += f"- {item.name} ({price_info}) - URL: {item.url}\n" - if search_results.total_items_found and search_results.total_items_found > len(search_results.items): - response_str += f"... and {search_results.total_items_found - len(search_results.items)} more products matching this search." + price_info = ( + f"${item.price:.2f}" + if item.price is not None + else "Price not available" + ) + response_str += ( + f"- {item.name} ({price_info}) - URL: {item.url}\n" + ) + if ( + search_results.total_items_found + and search_results.total_items_found > len(search_results.items) + ): + response_str += f"... and {search_results.total_items_found - len(search_results.items)} more products matching this search." raw_data_response = search_results.model_dump() else: response_str = f"No products found for search term '{request.search_term}' or an error occurred." elif action == NikeAction.GET_PRODUCT_DETAILS: - product_to_find = request.product_url or request.product_name_for_details + product_to_find = ( + request.product_url or request.product_name_for_details + ) if not product_to_find: - return NikeScrapeResponse(results="Error: Product URL or name is required to get details.") - + return NikeScrapeResponse( + results="Error: Product URL or name is required to get details." + ) + is_url_provided = bool(request.product_url) - product_detail = await scrape_product_details(product_to_find, session, is_url=is_url_provided) - + product_detail = await scrape_product_details( + product_to_find, session, is_url=is_url_provided + ) + if product_detail: response_str = f"Details for {product_detail.name}:\n" - price_info = f"${product_detail.price:.2f} {product_detail.currency}" if product_detail.price is not None else "Price not available" - if product_detail.original_price and product_detail.price is not None and product_detail.original_price > product_detail.price: - price_info += f" (Original: ${product_detail.original_price:.2f})" + price_info = ( + f"${product_detail.price:.2f} {product_detail.currency}" + if product_detail.price is not None + else "Price not available" + ) + if ( + product_detail.original_price + and product_detail.price is not None + and product_detail.original_price > product_detail.price + ): + price_info += ( + f" (Original: ${product_detail.original_price:.2f})" + ) response_str += f"- Price: {price_info}\n" response_str += f"- URL: {product_detail.url}\n" - if product_detail.product_id: response_str += f"- Product ID: {product_detail.product_id}\n" - if product_detail.color: response_str += f"- Color: {product_detail.color}\n" - if product_detail.available_sizes: response_str += f"- Available Sizes: {', '.join(product_detail.available_sizes)}\n" - if product_detail.rating: response_str += f"- Rating: {product_detail.rating}/5 ({product_detail.review_count or 0} reviews)\n" - if product_detail.image_src: response_str += f"- Image: {product_detail.image_src}\n" + if product_detail.product_id: + response_str += f"- Product ID: {product_detail.product_id}\n" + if product_detail.color: + response_str += f"- Color: {product_detail.color}\n" + if product_detail.available_sizes: + response_str += f"- Available Sizes: {', '.join(product_detail.available_sizes)}\n" + if product_detail.rating: + response_str += f"- Rating: {product_detail.rating}/5 ({product_detail.review_count or 0} reviews)\n" + if product_detail.image_src: + response_str += f"- Image: {product_detail.image_src}\n" # Could add description if needed, but might be too long for chat raw_data_response = product_detail.model_dump() else: - response_str = f"Could not retrieve details for '{product_to_find}'." - + response_str = ( + f"Could not retrieve details for '{product_to_find}'." + ) + # Placeholder for GET_SUB_CATEGORIES, FIND_BESTSELLERS, FIND_NEW_ARRIVALS elif action == NikeAction.GET_SUB_CATEGORIES: if not request.category_name: - return NikeScrapeResponse(results="Error: Main category name (e.g., 'Shoes') is required to get sub-categories.") - - main_categories_data = await scrape_main_categories(session, request.gender) + return NikeScrapeResponse( + results="Error: Main category name (e.g., 'Shoes') is required to get sub-categories." + ) + + main_categories_data = await scrape_main_categories( + session, request.gender + ) parent_category_url = None - parent_category_name_actual = "" # Store the exact name found + parent_category_name_actual = "" # Store the exact name found if main_categories_data and main_categories_data.categories: for cat in main_categories_data.categories: # Match if requested category_name is part of the found category name (e.g., "Shoes" in "Men's Shoes") if request.category_name.lower() in cat.name.lower(): parent_category_url = cat.url - parent_category_name_actual = cat.name - break - + parent_category_name_actual = cat.name + break + if parent_category_url and parent_category_name_actual: sub_categories_data = await scrape_sub_categories( main_category_url=parent_category_url, main_category_name=parent_category_name_actual, session=session, - gender=request.gender + gender=request.gender, ) if sub_categories_data and sub_categories_data.categories: response_str = f"Found {len(sub_categories_data.categories)} sub-categories under {request.gender.value}'s '{parent_category_name_actual}' (from {sub_categories_data.source_url}):\n" @@ -589,14 +831,13 @@ async def get_nike_info_enhanced(request: NikeScrapeRequest) -> NikeScrapeRespon elif action in [NikeAction.FIND_BESTSELLERS, NikeAction.FIND_NEW_ARRIVALS]: response_str = f"Action '{action.value}' is planned but not yet fully implemented. Try asking for main categories or searching products." - else: response_str = f"Unsupported action: {action.value}. Please use a supported Nike action." except Exception as e: - print(f"Exception in get_nike_info_enhanced: {e}") # Log the full exception - response_str = f"An unexpected error occurred: {str(e)[:100]}... Please check logs." - - return NikeScrapeResponse(results=response_str, raw_data=raw_data_response) - + print(f"Exception in get_nike_info_enhanced: {e}") # Log the full exception + response_str = ( + f"An unexpected error occurred: {str(e)[:100]}... Please check logs." + ) + return NikeScrapeResponse(results=response_str, raw_data=raw_data_response) diff --git a/CHANGELOG.md b/CHANGELOG.md index f84f70b7..7b6055e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ All notable changes to this repository are documented in this file. ## [Unreleased] +### Fixed +- Fixed security vulnerabilities in Flask frontend, agent timeouts, and exception suppression (PR #137) + ### Added - Auto badge workflow [award-contributor-badge.yml](.github/workflows/award-contributor-badge.yml) for merged external contributor PRs; [BADGE_REGISTRY.json](contributors/BADGE_REGISTRY.json) and [profile-badge-sync](contributors/profile-badge-sync/) for GitHub Profile README - Maintainer bypass for `review-required` and `stargazer-gate` (Fetch.ai org, repo write access, [.github/MAINTAINERS](.github/MAINTAINERS)) diff --git a/Claude Agent SDK/real-estate-search-agent/agent.py b/Claude Agent SDK/real-estate-search-agent/agent.py index ab48f19d..1b4947cf 100644 --- a/Claude Agent SDK/real-estate-search-agent/agent.py +++ b/Claude Agent SDK/real-estate-search-agent/agent.py @@ -33,7 +33,11 @@ payment_protocol_spec, ) -from sheets import GoogleAuthRequiredError, create_listings_sheet, get_google_auth_message +from sheets import ( + GoogleAuthRequiredError, + create_listings_sheet, + get_google_auth_message, +) from stripe_payments import ( STRIPE_AMOUNT_CENTS, create_checkout_session, @@ -49,6 +53,7 @@ # Message models # ───────────────────────────────────────────────────────────────────────────── + class SearchRequest(Model): query: str user_id: str = "" @@ -71,26 +76,29 @@ class SearchResponse(Model): # Pending-payment state # ───────────────────────────────────────────────────────────────────────────── + @dataclass class _PendingPayment: user_id: str - sender: str # agent address to reply to after payment - df: object # pandas DataFrame with search results - search: object # SearchInput (location, listing_type, …) + sender: str # agent address to reply to after payment + df: object # pandas DataFrame with search results + search: object # SearchInput (location, listing_type, …) checkout_session_id: str - is_chat: bool = False # True when the request came via chat protocol + is_chat: bool = False # True when the request came via chat protocol created_at: float = field(default_factory=time.time) _pending_payments: dict[str, _PendingPayment] = {} # keyed by checkout_session_id -_pending_by_user: dict[str, str] = {} # user_id → checkout_session_id +_pending_by_user: dict[str, str] = {} # user_id → checkout_session_id _PAYMENT_EXPIRY = 3600 # seconds — expire pending sessions after 1 hour def _cleanup_expired() -> None: now = time.time() - expired = [k for k, v in _pending_payments.items() if now - v.created_at > _PAYMENT_EXPIRY] + expired = [ + k for k, v in _pending_payments.items() if now - v.created_at > _PAYMENT_EXPIRY + ] for k in expired: p = _pending_payments.pop(k, None) if p: @@ -115,6 +123,7 @@ def _amount_str() -> str: # Agent setup # ───────────────────────────────────────────────────────────────────────────── + def _require_env(name: str) -> str: value = os.getenv(name, "").strip() if not value: @@ -178,7 +187,9 @@ async def _check_mailbox_loop(self): ) as resp: if resp.status == 200: for item in await resp.json(): - await self._handle_envelope(StoredEnvelope.model_validate(item)) + await self._handle_envelope( + StoredEnvelope.model_validate(item) + ) elif resp.status == 404: if not self._missing_mailbox_warning_logged: self._logger.warning( @@ -268,19 +279,29 @@ async def on_commit(ctx: Context, sender: str, msg: CommitPayment): # Secondary lookup: transaction_id might be a PaymentIntent ID (pi_...). # Scan pending payments by the sender's agent address. if not pending and transaction_id.startswith("pi_"): - pending = next((p for p in _pending_payments.values() if p.sender == sender), None) + pending = next( + (p for p in _pending_payments.values() if p.sender == sender), None + ) if pending: - ctx.logger.info(f"Resolved PI {transaction_id} → checkout session {pending.checkout_session_id}") + ctx.logger.info( + f"Resolved PI {transaction_id} → checkout session {pending.checkout_session_id}" + ) if not pending: - ctx.logger.warning(f"CommitPayment for unknown/expired session: {transaction_id}") - await ctx.send(sender, RejectPayment(reason="Payment session not found or expired.")) + ctx.logger.warning( + f"CommitPayment for unknown/expired session: {transaction_id}" + ) + await ctx.send( + sender, RejectPayment(reason="Payment session not found or expired.") + ) return if msg.funds.payment_method != "stripe": await ctx.send( sender, - RejectPayment(reason=f"Unsupported payment method: {msg.funds.payment_method}"), + RejectPayment( + reason=f"Unsupported payment method: {msg.funds.payment_method}" + ), ) return @@ -289,7 +310,9 @@ async def on_commit(ctx: Context, sender: str, msg: CommitPayment): paid = verify_payment(transaction_id) except Exception as exc: ctx.logger.exception("Stripe verification error") - await ctx.send(sender, RejectPayment(reason=f"Payment verification failed: {exc}")) + await ctx.send( + sender, RejectPayment(reason=f"Payment verification failed: {exc}") + ) return if not paid: @@ -307,7 +330,9 @@ async def on_commit(ctx: Context, sender: str, msg: CommitPayment): if not paid: await ctx.send( sender, - RejectPayment(reason="Payment not completed. Please finish the Stripe checkout first."), + RejectPayment( + reason="Payment not completed. Please finish the Stripe checkout first." + ), ) return @@ -316,7 +341,9 @@ async def on_commit(ctx: Context, sender: str, msg: CommitPayment): sheet_url = await _create_sheet_for_pending(pending) except GoogleAuthRequiredError as exc: ctx.logger.error(f"Google auth required — {exc}") - await ctx.send(sender, RejectPayment(reason=f"Google authorization required: {exc}")) + await ctx.send( + sender, RejectPayment(reason=f"Google authorization required: {exc}") + ) return except Exception as exc: ctx.logger.exception("Sheet creation failed after payment") @@ -329,7 +356,9 @@ async def on_commit(ctx: Context, sender: str, msg: CommitPayment): _pending_by_user.pop(pending.user_id, None) try: - await ctx.send(sender, CompletePayment(transaction_id=pending.checkout_session_id)) + await ctx.send( + sender, CompletePayment(transaction_id=pending.checkout_session_id) + ) except Exception as exc: ctx.logger.warning(f"CompletePayment send failed: {exc}") @@ -340,12 +369,14 @@ async def on_commit(ctx: Context, sender: str, msg: CommitPayment): await ctx.send( pending.sender, ChatMessage( - content=[TextContent( - text=( - f"Payment confirmed! Your Google Sheet is ready:\n{sheet_url}\n\n" - f"({num_results} listings in {pending.search.location})" + content=[ + TextContent( + text=( + f"Payment confirmed! Your Google Sheet is ready:\n{sheet_url}\n\n" + f"({num_results} listings in {pending.search.location})" + ) ) - )] + ] ), ) else: @@ -378,6 +409,7 @@ async def on_reject(ctx: Context, sender: str, msg: RejectPayment): # Shared helper: run search → optionally request payment → return # ───────────────────────────────────────────────────────────────────────────── + async def _search_and_request_payment( ctx: Context, sender: str, @@ -405,7 +437,10 @@ async def _search_and_request_payment( await ctx.send(sender, ChatMessage(content=[TextContent(text=reply)])) else: error = "" - if not result.sheet_url and "Google authorization required" in result.summary: + if ( + not result.sheet_url + and "Google authorization required" in result.summary + ): error = result.summary await ctx.send( sender, @@ -425,7 +460,9 @@ async def _search_and_request_payment( if is_chat: await ctx.send( sender, - ChatMessage(content=[TextContent(text=result.summary or "No listings found.")]), + ChatMessage( + content=[TextContent(text=result.summary or "No listings found.")] + ), ) else: await ctx.send( @@ -439,7 +476,9 @@ async def _search_and_request_payment( return try: - description = f"{result.num_results} listings in {result.pending_search.location}" + description = ( + f"{result.num_results} listings in {result.pending_search.location}" + ) chat_session_id = str(ctx.session) if hasattr(ctx, "session") else user_id checkout = create_checkout_session(user_id, chat_session_id, description) except Exception as exc: @@ -465,7 +504,9 @@ async def _search_and_request_payment( await ctx.send( sender, RequestPayment( - accepted_funds=[Funds(currency="USD", amount=amount, payment_method="stripe")], + accepted_funds=[ + Funds(currency="USD", amount=amount, payment_method="stripe") + ], recipient=str(ctx.agent.address), deadline_seconds=_PAYMENT_EXPIRY, reference=chat_session_id, @@ -507,13 +548,19 @@ async def handle_chat(ctx: Context, sender: str, msg: ChatMessage): """Handle messages from ASI:One and other chat-protocol clients.""" await ctx.send(sender, ChatAcknowledgement(acknowledged_msg_id=msg.msg_id)) - query = next((item.text for item in msg.content if isinstance(item, TextContent)), "").strip() + query = next( + (item.text for item in msg.content if isinstance(item, TextContent)), "" + ).strip() if not query: await ctx.send( sender, - ChatMessage(content=[TextContent( - text="Please send a search query, e.g. '3 bed house for sale in Austin TX under $400k'" - )]), + ChatMessage( + content=[ + TextContent( + text="Please send a search query, e.g. '3 bed house for sale in Austin TX under $400k'" + ) + ] + ), ) return @@ -525,7 +572,9 @@ async def handle_chat(ctx: Context, sender: str, msg: ChatMessage): ctx.logger.exception("Chat handler failed") await ctx.send( sender, - ChatMessage(content=[TextContent(text=f"Sorry, something went wrong: {exc}")]), + ChatMessage( + content=[TextContent(text=f"Sorry, something went wrong: {exc}")] + ), ) @@ -541,6 +590,7 @@ async def handle_chat_ack(_ctx: Context, _sender: str, _msg: ChatAcknowledgement # SearchRequest / FollowUpRequest protocol # ───────────────────────────────────────────────────────────────────────────── + @agent.on_message(model=SearchRequest) async def handle_search(ctx: Context, sender: str, msg: SearchRequest): user_id = _resolve_user_id(msg.user_id, sender) @@ -554,7 +604,9 @@ async def handle_search(ctx: Context, sender: str, msg: SearchRequest): SearchResponse( summary=instructions, session_id=user_id, - error="" if instructions.startswith("Google is already connected") else instructions, + error="" + if instructions.startswith("Google is already connected") + else instructions, ), ) return @@ -575,7 +627,9 @@ async def handle_followup(ctx: Context, sender: str, msg: FollowUpRequest): if not stripe_configured(): try: - result = await resume_workflow(WorkflowInput(user_request=msg.query, user_id=user_id)) + result = await resume_workflow( + WorkflowInput(user_request=msg.query, user_id=user_id) + ) await ctx.send( sender, SearchResponse( diff --git a/Claude Agent SDK/real-estate-search-agent/scraper.py b/Claude Agent SDK/real-estate-search-agent/scraper.py index 3f689435..825529c4 100644 --- a/Claude Agent SDK/real-estate-search-agent/scraper.py +++ b/Claude Agent SDK/real-estate-search-agent/scraper.py @@ -23,16 +23,16 @@ @dataclass class SearchInput: - location: str # e.g. "Austin, TX" or zip "78701" - listing_type: str = "for_sale" # for_sale | for_rent | sold + location: str # e.g. "Austin, TX" or zip "78701" + listing_type: str = "for_sale" # for_sale | for_rent | sold min_price: Optional[int] = None max_price: Optional[int] = None min_beds: Optional[int] = None max_beds: Optional[int] = None min_sqft: Optional[int] = None max_sqft: Optional[int] = None - property_type: Optional[list] = None # ["single_family", "condo", "townhouse"] - past_days: int = 30 # only listings from last N days + property_type: Optional[list] = None # ["single_family", "condo", "townhouse"] + past_days: int = 30 # only listings from last N days def _normalize_property_types(property_types: Optional[list]) -> Optional[list]: @@ -65,7 +65,9 @@ def fetch_listings(search: SearchInput) -> pd.DataFrame: city, state, zip_code, beds, baths, square_feet, listing_type, site_name. """ print(f" Searching for properties in: {search.location}") - print(f" Type: {search.listing_type} | Budget: ${search.min_price or 0:,} - ${search.max_price or '∞'}") + print( + f" Type: {search.listing_type} | Budget: ${search.min_price or 0:,} - ${search.max_price or '∞'}" + ) normalized_property_types = _normalize_property_types(search.property_type) properties = scrape_property( @@ -134,4 +136,4 @@ def format_price(price) -> str: try: return f"${int(price):,}" except (ValueError, TypeError): - return "N/A" \ No newline at end of file + return "N/A" diff --git a/Claude Agent SDK/real-estate-search-agent/sheets.py b/Claude Agent SDK/real-estate-search-agent/sheets.py index 741ea2d2..fa841e40 100644 --- a/Claude Agent SDK/real-estate-search-agent/sheets.py +++ b/Claude Agent SDK/real-estate-search-agent/sheets.py @@ -23,7 +23,9 @@ TOKEN_ENDPOINT = "https://oauth2.googleapis.com/token" TOKEN_STORE_FILE = os.getenv("GOOGLE_OAUTH_TOKEN_STORE_FILE", "google_user_tokens.json") -DEVICE_STORE_FILE = os.getenv("GOOGLE_OAUTH_DEVICE_STORE_FILE", "google_device_flows.json") +DEVICE_STORE_FILE = os.getenv( + "GOOGLE_OAUTH_DEVICE_STORE_FILE", "google_device_flows.json" +) class GoogleAuthRequiredError(RuntimeError): @@ -103,7 +105,9 @@ def _start_device_flow(client_id: str) -> dict[str, Any]: if "error" in response: description = response.get("error_description", "") - raise RuntimeError(f"Google device authorization failed: {response['error']} {description}") + raise RuntimeError( + f"Google device authorization failed: {response['error']} {description}" + ) now = int(time.time()) expires_in = int(response.get("expires_in", 900)) @@ -119,7 +123,9 @@ def _start_device_flow(client_id: str) -> dict[str, Any]: } -def _poll_device_flow(client_id: str, client_secret: str, device_code: str) -> dict[str, Any]: +def _poll_device_flow( + client_id: str, client_secret: str, device_code: str +) -> dict[str, Any]: payload: dict[str, Any] = { "client_id": client_id, "device_code": device_code, diff --git a/Claude Agent SDK/real-estate-search-agent/stripe_payments.py b/Claude Agent SDK/real-estate-search-agent/stripe_payments.py index 1ca34ff4..adb7ac63 100644 --- a/Claude Agent SDK/real-estate-search-agent/stripe_payments.py +++ b/Claude Agent SDK/real-estate-search-agent/stripe_payments.py @@ -45,7 +45,9 @@ def _expires_at() -> int: return int(time.time()) + secs -def create_checkout_session(user_address: str, chat_session_id: str, description: str) -> dict: +def create_checkout_session( + user_address: str, chat_session_id: str, description: str +) -> dict: """Create a Stripe embedded checkout session. Returns a dict with: diff --git a/Claude Agent SDK/real-estate-search-agent/workflow.py b/Claude Agent SDK/real-estate-search-agent/workflow.py index e2798f6d..267bb858 100644 --- a/Claude Agent SDK/real-estate-search-agent/workflow.py +++ b/Claude Agent SDK/real-estate-search-agent/workflow.py @@ -14,10 +14,8 @@ import asyncio import json import re -import os import time -from datetime import datetime -from dataclasses import dataclass, field +from dataclasses import dataclass from typing import Optional import anthropic @@ -40,6 +38,7 @@ # Data models # ───────────────────────────────────────────────────────────────────────────── + @dataclass class WorkflowInput: user_request: str @@ -53,7 +52,7 @@ class WorkflowResult: num_results: int session_id: Optional[str] = None # Set when sheet creation is deferred pending payment - pending_df: Optional[object] = None # pandas DataFrame + pending_df: Optional[object] = None # pandas DataFrame pending_search: Optional["SearchInput"] = None @@ -71,19 +70,31 @@ class WorkflowResult: "input_schema": { "type": "object", "properties": { - "location": {"type": "string", "description": "City, State or zip code"}, - "listing_type": {"type": "string", "enum": ["for_sale", "for_rent", "sold"]}, - "min_price": {"type": "integer", "description": "Minimum price in USD"}, - "max_price": {"type": "integer", "description": "Maximum price in USD"}, - "min_beds": {"type": "integer"}, - "max_beds": {"type": "integer"}, - "min_sqft": {"type": "integer"}, - "max_sqft": {"type": "integer"}, + "location": { + "type": "string", + "description": "City, State or zip code", + }, + "listing_type": { + "type": "string", + "enum": ["for_sale", "for_rent", "sold"], + }, + "min_price": {"type": "integer", "description": "Minimum price in USD"}, + "max_price": {"type": "integer", "description": "Maximum price in USD"}, + "min_beds": {"type": "integer"}, + "max_beds": {"type": "integer"}, + "min_sqft": {"type": "integer"}, + "max_sqft": {"type": "integer"}, "property_type": { "type": "array", - "items": {"type": "string", "enum": ["single_family", "condo", "townhouse", "multi_family"]}, + "items": { + "type": "string", + "enum": ["single_family", "condo", "townhouse", "multi_family"], + }, + }, + "past_days": { + "type": "integer", + "description": "Listings from last N days (default 30)", }, - "past_days": {"type": "integer", "description": "Listings from last N days (default 30)"}, }, "required": ["location", "listing_type"], }, @@ -121,6 +132,7 @@ class WorkflowResult: # Step 1 — Parse natural language → structured SearchInput # ───────────────────────────────────────────────────────────────────────────── + async def parse_search_intent(user_request: str) -> SearchInput: client = anthropic.Anthropic() @@ -176,12 +188,13 @@ async def parse_search_intent(user_request: str) -> SearchInput: # Tool execution — called when Claude requests a tool # ───────────────────────────────────────────────────────────────────────────── + # Shared state within a single workflow run @dataclass class _RunState: search: Optional[SearchInput] = None user_id: str = "default" - df: object = None # pandas DataFrame or None + df: object = None # pandas DataFrame or None sheet_url: str = "" num_results: int = 0 last_error: str = "" @@ -198,16 +211,20 @@ async def _execute_tool(tool_name: str, tool_input: dict, state: _RunState) -> s elapsed = now - _last_search_time if _last_search_time > 0 and elapsed < SEARCH_COOLDOWN_SECONDS: wait = round(SEARCH_COOLDOWN_SECONDS - elapsed, 1) - return json.dumps({ - "status": "rate_limited", - "message": f"Please wait {wait}s before searching again.", - }) + return json.dumps( + { + "status": "rate_limited", + "message": f"Please wait {wait}s before searching again.", + } + ) _last_search_time = time.time() # Build SearchInput from Claude's tool_input search = SearchInput( - location=tool_input.get("location", state.search.location if state.search else ""), + location=tool_input.get( + "location", state.search.location if state.search else "" + ), listing_type=tool_input.get("listing_type", "for_sale"), min_price=tool_input.get("min_price"), max_price=tool_input.get("max_price"), @@ -228,24 +245,40 @@ async def _execute_tool(tool_name: str, tool_input: dict, state: _RunState) -> s state.last_error = "" if df is None or df.empty: - return json.dumps({ - "status": "no_results", - "message": f"No listings found in {search.location} with the given filters.", - "num_results": 0, - "location": search.location, - }) + return json.dumps( + { + "status": "no_results", + "message": f"No listings found in {search.location} with the given filters.", + "num_results": 0, + "location": search.location, + } + ) preview = json.loads(df.head(5).to_json(orient="records")) - return json.dumps({ - "status": "success", - "num_results": len(df), - "location": search.location, - "listing_type": search.listing_type, - "price_min": (lambda v: None if v != v else int(v))(df["Price ($)"].min()) if "Price ($)" in df.columns else None, - "price_max": (lambda v: None if v != v else int(v))(df["Price ($)"].max()) if "Price ($)" in df.columns else None, - "price_avg": (lambda v: None if v != v else int(v))(df["Price ($)"].mean()) if "Price ($)" in df.columns else None, - "sample_listings": preview, - }) + return json.dumps( + { + "status": "success", + "num_results": len(df), + "location": search.location, + "listing_type": search.listing_type, + "price_min": (lambda v: None if v != v else int(v))( + df["Price ($)"].min() + ) + if "Price ($)" in df.columns + else None, + "price_max": (lambda v: None if v != v else int(v))( + df["Price ($)"].max() + ) + if "Price ($)" in df.columns + else None, + "price_avg": (lambda v: None if v != v else int(v))( + df["Price ($)"].mean() + ) + if "Price ($)" in df.columns + else None, + "sample_listings": preview, + } + ) except Exception as e: print(f"[Tool] search_listings error: {e}") @@ -257,7 +290,12 @@ async def _execute_tool(tool_name: str, tool_input: dict, state: _RunState) -> s # ── create_sheet ───────────────────────────────────────────────────────── elif tool_name == "create_sheet": if state.df is None or (hasattr(state.df, "empty") and state.df.empty): - return json.dumps({"status": "error", "error": "No listings data — run search_listings first."}) + return json.dumps( + { + "status": "error", + "error": "No listings data — run search_listings first.", + } + ) print("[Tool] create_sheet → writing to Google Sheets...") try: @@ -271,7 +309,9 @@ async def _execute_tool(tool_name: str, tool_input: dict, state: _RunState) -> s state.sheet_url = sheet_url state.last_error = "" print(f"[Tool] Sheet created: {sheet_url}") - return json.dumps({"status": "success", "sheet_url": sheet_url, "num_rows": len(state.df)}) + return json.dumps( + {"status": "success", "sheet_url": sheet_url, "num_rows": len(state.df)} + ) except GoogleAuthRequiredError as e: state.last_error = str(e) @@ -361,11 +401,13 @@ async def run_agent_loop( if block.type == "tool_use": print(f"[Agent] Claude calling: {block.name}") result_str = await _execute_tool(block.name, block.input, state) - tool_results.append({ - "type": "tool_result", - "tool_use_id": block.id, - "content": result_str, - }) + tool_results.append( + { + "type": "tool_result", + "tool_use_id": block.id, + "content": result_str, + } + ) # Feed all results back to Claude in one user turn messages.append({"role": "user", "content": tool_results}) @@ -390,7 +432,9 @@ async def run_agent_loop( f"{state.last_error}" ) else: - final_summary = f"No listings found in {search.location} matching your criteria." + final_summary = ( + f"No listings found in {search.location} matching your criteria." + ) return WorkflowResult( sheet_url=state.sheet_url, @@ -407,15 +451,18 @@ async def run_agent_loop( # Public entry points # ───────────────────────────────────────────────────────────────────────────── + async def run_workflow(input_data: WorkflowInput) -> WorkflowResult: """Start a fresh search. Clears any saved session for this user_id.""" print(f"\n🏠 New search — user: {input_data.user_id}") - print(f" Request: \"{input_data.user_request}\"") + print(f' Request: "{input_data.user_request}"') search = await parse_search_intent(input_data.user_request) - print(f" Parsed: {search.location} | {search.listing_type} | " - f"${search.min_price or 0:,}–{'∞' if not search.max_price else f'${search.max_price:,}'} | " - f"{search.min_beds or 'any'}+ beds") + print( + f" Parsed: {search.location} | {search.listing_type} | " + f"${search.min_price or 0:,}–{'∞' if not search.max_price else f'${search.max_price:,}'} | " + f"{search.min_beds or 'any'}+ beds" + ) if not search.location: return WorkflowResult( @@ -440,7 +487,9 @@ async def resume_workflow(input_data: WorkflowInput) -> WorkflowResult: if prior_messages: print(f"\n🔄 Resuming session for user: {input_data.user_id}") search = await parse_search_intent(input_data.user_request) - return await run_agent_loop(search, user_id=input_data.user_id, prior_messages=prior_messages) + return await run_agent_loop( + search, user_id=input_data.user_id, prior_messages=prior_messages + ) else: print(f"\n⚠️ No session for '{input_data.user_id}' — starting fresh") return await run_workflow(input_data) @@ -453,7 +502,7 @@ async def run_search_only(input_data: WorkflowInput) -> WorkflowResult: and result.pending_search, creates the sheet after CommitPayment is received. """ print(f"\n🔍 Search-only (payment-gated) — user: {input_data.user_id}") - print(f" Request: \"{input_data.user_request}\"") + print(f' Request: "{input_data.user_request}"') search = await parse_search_intent(input_data.user_request) if not search.location: diff --git a/Composio/gmail-agent/agent.py b/Composio/gmail-agent/agent.py index 09ebd6e9..86dcf089 100644 --- a/Composio/gmail-agent/agent.py +++ b/Composio/gmail-agent/agent.py @@ -1,4 +1,4 @@ from uagent_app import agent if __name__ == "__main__": - agent.run() \ No newline at end of file + agent.run() diff --git a/Composio/gmail-agent/formatting.py b/Composio/gmail-agent/formatting.py index 08b623a6..b33cbf12 100644 --- a/Composio/gmail-agent/formatting.py +++ b/Composio/gmail-agent/formatting.py @@ -26,16 +26,24 @@ def extract_gmail_response(intermediate_steps: List) -> Dict[str, Any]: except Exception: parsed = None if parsed and isinstance(parsed, dict): - resp = parsed.get('data', {}).get('response_data', {}) - email_id = resp.get('id', email_id) - thread_id = resp.get('threadId', thread_id) - labels = resp.get('labelIds', labels) or labels + resp = parsed.get("data", {}).get("response_data", {}) + email_id = resp.get("id", email_id) + thread_id = resp.get("threadId", thread_id) + labels = resp.get("labelIds", labels) or labels except Exception: pass return {"email_id": email_id, "thread_id": thread_id, "labels": labels} -def format_success_markdown(recipient: str, sender_email: str, subject: str, body: str, email_id: str, thread_id: str, labels: List[str]) -> str: +def format_success_markdown( + recipient: str, + sender_email: str, + subject: str, + body: str, + email_id: str, + thread_id: str, + labels: List[str], +) -> str: return f"""✅ **Email sent successfully to {recipient}!** **Email Details:** @@ -46,32 +54,38 @@ def format_success_markdown(recipient: str, sender_email: str, subject: str, bod **Gmail Response:** - **Email ID:** `{email_id}` - **Thread ID:** `{thread_id}` -- **Labels:** {', '.join(labels) if labels else 'N/A'} +- **Labels:** {", ".join(labels) if labels else "N/A"} - **Status:** ✅ Sent successfully""" -def format_reply_success_markdown(recipient: str, body: str, thread_id: str, message_id: str | None) -> str: +def format_reply_success_markdown( + recipient: str, body: str, thread_id: str, message_id: str | None +) -> str: return f"""✅ **Replied successfully** **Reply Details:** - **To:** {recipient} - **Thread ID:** `{thread_id}` -- **Message ID:** `{message_id or 'N/A'}` -- **Body:** {body[:500]}{'...' if body and len(body) > 500 else ''} +- **Message ID:** `{message_id or "N/A"}` +- **Body:** {body[:500]}{"..." if body and len(body) > 500 else ""} """ -def format_trash_success_markdown(message_id: str, thread_id: str | None, labels: List[str] | None) -> str: +def format_trash_success_markdown( + message_id: str, thread_id: str | None, labels: List[str] | None +) -> str: return f"""🗑️ **Moved to Trash** **Email:** - **Message ID:** `{message_id}` -- **Thread ID:** `{thread_id or 'N/A'}` -- **Labels:** {', '.join(labels or []) if labels else 'TRASH'} +- **Thread ID:** `{thread_id or "N/A"}` +- **Labels:** {", ".join(labels or []) if labels else "TRASH"} """ -def format_contacts_markdown(connections: List[Dict[str, Any]], next_page_token: str | None) -> str: +def format_contacts_markdown( + connections: List[Dict[str, Any]], next_page_token: str | None +) -> str: lines = ["👥 **Contacts**\n"] for idx, c in enumerate(connections, start=1): names = [] @@ -85,14 +99,18 @@ def format_contacts_markdown(connections: List[Dict[str, Any]], next_page_token: emails.append(e.get("value")) except Exception: pass - lines.append(f"- {idx}. **Name:** {', '.join(names) if names else '(unknown)'} - **Emails:** {', '.join(emails) if emails else '(none)'}") + lines.append( + f"- {idx}. **Name:** {', '.join(names) if names else '(unknown)'} - **Emails:** {', '.join(emails) if emails else '(none)'}" + ) if next_page_token: lines.append("") lines.append(f"Next page token: `{next_page_token}`") return "\n".join(lines) -def format_people_search_markdown(results: List[Dict[str, Any]], next_page_token: str | None) -> str: +def format_people_search_markdown( + results: List[Dict[str, Any]], next_page_token: str | None +) -> str: lines = ["🔎 **People Search Results**\n"] for idx, p in enumerate(results, start=1): names = [] @@ -126,7 +144,9 @@ def _format_ts(ts_val: Any) -> str: if ts_val is None or ts_val == "": return "(no time)" # If already an ISO-like string, return as-is - if isinstance(ts_val, str) and ("T" in ts_val or ts_val.isalpha() or ":" in ts_val): + if isinstance(ts_val, str) and ( + "T" in ts_val or ts_val.isalpha() or ":" in ts_val + ): return ts_val # If numeric string (ms or s) if isinstance(ts_val, str) and ts_val.isdigit(): @@ -147,7 +167,9 @@ def _format_ts(ts_val: Any) -> str: return str(ts_val) -def format_thread_messages_markdown(messages: List[Dict[str, Any]], next_page_token: str | None) -> str: +def format_thread_messages_markdown( + messages: List[Dict[str, Any]], next_page_token: str | None +) -> str: header = "🧵 **Thread Messages**\n" # Include thread header when available thread_id_hdr = None @@ -156,14 +178,18 @@ def format_thread_messages_markdown(messages: List[Dict[str, Any]], next_page_to thread_id_hdr = first.get("threadId") or first.get("thread_id") lines = [header] if thread_id_hdr: - lines.append(f"Here are the details of the email thread with ID {thread_id_hdr}:\n") + lines.append( + f"Here are the details of the email thread with ID {thread_id_hdr}:\n" + ) if not messages: lines.append("(no messages found)") for idx, m in enumerate(messages, start=1): subject = m.get("subject") or "(no subject)" sender = m.get("sender") or m.get("from") or "(unknown sender)" to_email = m.get("to") or m.get("recipient") or "(unknown recipient)" - ts = _format_ts(m.get("messageTimestamp") or m.get("timestamp") or m.get("internalDate")) + ts = _format_ts( + m.get("messageTimestamp") or m.get("timestamp") or m.get("internalDate") + ) body_text = m.get("messageText") or "" if not body_text: preview_obj = m.get("preview") or {} @@ -194,7 +220,9 @@ def format_profile_markdown(profile: Dict[str, Any]) -> str: ) -def format_drafts_list_markdown(drafts: List[Dict[str, Any]], next_page_token: str | None) -> str: +def format_drafts_list_markdown( + drafts: List[Dict[str, Any]], next_page_token: str | None +) -> str: lines = ["📝 **Drafts**\n"] for idx, d in enumerate(drafts, start=1): draft_id = d.get("id") or d.get("draftId") or "" @@ -238,12 +266,14 @@ def format_single_message_markdown(message: Dict[str, Any]) -> str: ) -def format_draft_created_markdown(draft_id: str, thread_id: str | None, to_email: str, subject: str) -> str: +def format_draft_created_markdown( + draft_id: str, thread_id: str | None, to_email: str, subject: str +) -> str: return f"""📝 **Draft created** **Draft Details:** - **Draft ID:** `{draft_id}` -- **Thread ID:** `{thread_id or 'N/A'}` +- **Thread ID:** `{thread_id or "N/A"}` - **To:** {to_email} - **Subject:** {subject} """ @@ -263,7 +293,9 @@ def format_draft_deleted_markdown(draft_id: str) -> str: """ -def format_fetched_emails_markdown(messages: List[Dict[str, Any]], next_page_token: str | None, estimate: int) -> str: +def format_fetched_emails_markdown( + messages: List[Dict[str, Any]], next_page_token: str | None, estimate: int +) -> str: lines = ["📥 **Fetched Emails (latest)**\n"] for idx, m in enumerate(messages, start=1): subject = m.get("subject") or "(no subject)" @@ -287,5 +319,3 @@ def format_fetched_emails_markdown(messages: List[Dict[str, Any]], next_page_tok if next_page_token: lines.append(f"Next page token: `{next_page_token}`") return "\n".join(lines) - - diff --git a/Composio/gmail-agent/llm.py b/Composio/gmail-agent/llm.py index f545e101..aefef1ee 100644 --- a/Composio/gmail-agent/llm.py +++ b/Composio/gmail-agent/llm.py @@ -15,10 +15,14 @@ def build_email_agent(tools) -> AgentExecutor: model_name = os.getenv("OPENAI_MODEL", "gpt-4-turbo") llm = ChatOpenAI(model=model_name, temperature=0) agent = create_openai_functions_agent(llm, tools, prompt) - return AgentExecutor(agent=agent, tools=tools, verbose=True, return_intermediate_steps=True) + return AgentExecutor( + agent=agent, tools=tools, verbose=True, return_intermediate_steps=True + ) -def run_email_task(agent_executor: AgentExecutor, recipient: str, subject: str, body: str) -> Dict[str, Any]: +def run_email_task( + agent_executor: AgentExecutor, recipient: str, subject: str, body: str +) -> Dict[str, Any]: task = ( f"Send an email to {recipient} with the subject '{subject}' and " f"the body '{body}'. Make sure to actually send the email, not create a draft." @@ -32,10 +36,14 @@ def build_fetch_agent(tools) -> AgentExecutor: model_name = os.getenv("OPENAI_MODEL", "gpt-4-turbo") llm = ChatOpenAI(model=model_name, temperature=0) agent = create_openai_functions_agent(llm, tools, prompt) - return AgentExecutor(agent=agent, tools=tools, verbose=True, return_intermediate_steps=True) + return AgentExecutor( + agent=agent, tools=tools, verbose=True, return_intermediate_steps=True + ) -def run_fetch_task(agent_executor: AgentExecutor, max_results: int = 10, label_ids=None) -> Dict[str, Any]: +def run_fetch_task( + agent_executor: AgentExecutor, max_results: int = 10, label_ids=None +) -> Dict[str, Any]: label_ids = label_ids or ["INBOX"] task = ( "Fetch the latest emails using the GMAIL_FETCH_EMAILS tool with these parameters: " @@ -44,5 +52,3 @@ def run_fetch_task(agent_executor: AgentExecutor, max_results: int = 10, label_i "Only use the tool; do not summarize; return tool output." ) return agent_executor.invoke({"input": task}) - - diff --git a/Composio/gmail-agent/system_prompt.py b/Composio/gmail-agent/system_prompt.py index ef6c793a..446ce835 100644 --- a/Composio/gmail-agent/system_prompt.py +++ b/Composio/gmail-agent/system_prompt.py @@ -58,5 +58,3 @@ Notes: - I remember your authenticated email per chat; no hardcoded defaults. - You can also include From: you@example.com in any message to switch the active account.""" - - diff --git a/Composio/gmail-agent/tools.py b/Composio/gmail-agent/tools.py index 59b63aa0..e8c851bb 100644 --- a/Composio/gmail-agent/tools.py +++ b/Composio/gmail-agent/tools.py @@ -24,7 +24,10 @@ def get_gmail_auth_url(user_email: str): composio = Composio(api_key=os.getenv("COMPOSIO_API_KEY", "")) auth_config_id = os.getenv("GMAIL_AUTH_CONFIG_ID") if not auth_config_id: - return {"success": False, "error": "Gmail Auth Config ID not found. Please set GMAIL_AUTH_CONFIG_ID in your .env file"} + return { + "success": False, + "error": "Gmail Auth Config ID not found. Please set GMAIL_AUTH_CONFIG_ID in your .env file", + } connection_request = composio.connected_accounts.initiate( user_id=user_email, auth_config_id=auth_config_id, @@ -74,7 +77,9 @@ def get_gmail_draft_tools_for_user(user_email: str): def get_gmail_thread_fetch_tools_for_user(user_email: str): composio_lc = get_composio_lc() - return composio_lc.tools.get(user_id=user_email, tools=["GMAIL_FETCH_MESSAGE_BY_THREAD_ID"]) + return composio_lc.tools.get( + user_id=user_email, tools=["GMAIL_FETCH_MESSAGE_BY_THREAD_ID"] + ) def get_gmail_profile_tools_for_user(user_email: str): @@ -99,5 +104,3 @@ def get_gmail_list_drafts_tools_for_user(user_email: str): def get_gmail_message_by_id_tools_for_user(user_email: str): # Deprecated: removed fetch message by ID feature per request return [] - - diff --git a/Composio/gmail-agent/uagent_app.py b/Composio/gmail-agent/uagent_app.py index 9bb30f54..ab79feed 100644 --- a/Composio/gmail-agent/uagent_app.py +++ b/Composio/gmail-agent/uagent_app.py @@ -1,6 +1,5 @@ -from datetime import datetime, timezone +from datetime import datetime from uuid import uuid4 -import os import re from dotenv import load_dotenv from uagents import Context, Protocol, Agent @@ -69,10 +68,13 @@ async def handle_message(ctx: Context, sender: str, msg: ChatMessage): lower_text = text.lower() - if ("fetch" in lower_text and "inbox" in lower_text) or any(keyword in lower_text for keyword in ["fetch emails", "get inbox", "list emails"]): + if ("fetch" in lower_text and "inbox" in lower_text) or any( + keyword in lower_text + for keyword in ["fetch emails", "get inbox", "list emails"] + ): try: # default: last 10 inbox - email_pattern = r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b' + email_pattern = r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b" emails = re.findall(email_pattern, text) if emails: SENDER_TO_USER_EMAIL[sender] = emails[0] @@ -85,6 +87,7 @@ async def handle_message(ctx: Context, sender: str, msg: ChatMessage): else: # parse requested count (e.g., "fetch 10 emails") import re as _re + count = 10 m = _re.search(r"\b(\d{1,3})\b", lower_text) if m: @@ -95,10 +98,13 @@ async def handle_message(ctx: Context, sender: str, msg: ChatMessage): # Execute GMAIL_FETCH_EMAILS directly via the tool to avoid LLM context bloat from tools import get_gmail_fetch_tools_for_user + tools = get_gmail_fetch_tools_for_user(user_email) fetch_tool = tools[0] if tools else None if not fetch_tool: - response = "❌ Fetch tool unavailable. Please try reconnecting your Gmail." + response = ( + "❌ Fetch tool unavailable. Please try reconnecting your Gmail." + ) else: params = { "ids_only": False, @@ -114,6 +120,7 @@ async def handle_message(ctx: Context, sender: str, msg: ChatMessage): except Exception: # Some tool wrappers accept string input import json as _json + result = fetch_tool.run(_json.dumps(params)) # Parse result @@ -124,6 +131,7 @@ async def handle_message(ctx: Context, sender: str, msg: ChatMessage): data = result if isinstance(result, str): import ast + brace_idx = result.find("{") if brace_idx != -1: data = ast.literal_eval(result[brace_idx:]) @@ -136,17 +144,27 @@ async def handle_message(ctx: Context, sender: str, msg: ChatMessage): except Exception: pass - response = format_fetched_emails_markdown(messages, next_token, estimate) + response = format_fetched_emails_markdown( + messages, next_token, estimate + ) except Exception as e: response = f"❌ Error fetching emails: {str(e)}" - elif any(keyword in lower_text for keyword in ["move to trash", "trash message", "delete email", "move email to trash"]): + elif any( + keyword in lower_text + for keyword in [ + "move to trash", + "trash message", + "delete email", + "move email to trash", + ] + ): try: # Expect: message id and optional From message_id = "" sender_email = "" - lines = text.split('\n') - parts = text.split(',') if ',' in text else lines + lines = text.split("\n") + parts = text.split(",") if "," in text else lines for part in parts: segment = part.strip() lower = segment.lower() @@ -154,8 +172,11 @@ async def handle_message(ctx: Context, sender: str, msg: ChatMessage): raw_id = segment.split(":", 1)[1].strip() # Extract clean message id token (avoid trailing text like 'From: ...') import re as _re2 + m_id = _re2.search(r"([0-9A-Za-z]+)", raw_id) - message_id = m_id.group(1) if m_id else raw_id.split()[0].rstrip(",") + message_id = ( + m_id.group(1) if m_id else raw_id.split()[0].rstrip(",") + ) elif "from:" in lower or "sender:" in lower: sender_email = segment.split(":", 1)[1].strip() @@ -182,6 +203,7 @@ async def handle_message(ctx: Context, sender: str, msg: ChatMessage): result = tool.invoke(params) except Exception: import json as _json + result = tool.run(_json.dumps(params)) msg_id = message_id @@ -191,6 +213,7 @@ async def handle_message(ctx: Context, sender: str, msg: ChatMessage): data = result if isinstance(result, str): import ast + brace_idx = result.find("{") if brace_idx != -1: data = ast.literal_eval(result[brace_idx:]) @@ -206,11 +229,19 @@ async def handle_message(ctx: Context, sender: str, msg: ChatMessage): except Exception as e: response = f"❌ Error moving to trash: {str(e)}" - elif any(keyword in lower_text for keyword in ["get contacts", "list contacts", "fetch contacts", "my contacts"]): + elif any( + keyword in lower_text + for keyword in [ + "get contacts", + "list contacts", + "fetch contacts", + "my contacts", + ] + ): try: sender_email = "" - lines = text.split('\n') - parts = text.split(',') if ',' in text else lines + lines = text.split("\n") + parts = text.split(",") if "," in text else lines for part in parts: segment = part.strip() lower = segment.lower() @@ -231,7 +262,9 @@ async def handle_message(ctx: Context, sender: str, msg: ChatMessage): tools = get_gmail_contacts_tools_for_user(sender_email) tool = tools[0] if tools else None if not tool: - response = "❌ Contacts tool unavailable. Please reconnect your Gmail." + response = ( + "❌ Contacts tool unavailable. Please reconnect your Gmail." + ) else: params = { "include_other_contacts": True, @@ -242,6 +275,7 @@ async def handle_message(ctx: Context, sender: str, msg: ChatMessage): result = tool.invoke(params) except Exception: import json as _json + result = tool.run(_json.dumps(params)) connections = [] @@ -250,6 +284,7 @@ async def handle_message(ctx: Context, sender: str, msg: ChatMessage): data = result if isinstance(result, str): import ast + brace_idx = result.find("{") if brace_idx != -1: data = ast.literal_eval(result[brace_idx:]) @@ -264,7 +299,15 @@ async def handle_message(ctx: Context, sender: str, msg: ChatMessage): except Exception as e: response = f"❌ Error fetching contacts: {str(e)}" - elif any(keyword in lower_text for keyword in ["search contacts", "search people", "find contact", "find people"]): + elif any( + keyword in lower_text + for keyword in [ + "search contacts", + "search people", + "find contact", + "find people", + ] + ): try: # Expect: query; optional other_contacts/pageSize/person_fields; optional From sender_email = "" @@ -272,8 +315,8 @@ async def handle_message(ctx: Context, sender: str, msg: ChatMessage): other_contacts = True page_size = 10 person_fields = "emailAddresses,names,phoneNumbers" - lines = text.split('\n') - parts = text.split(',') if ',' in text else lines + lines = text.split("\n") + parts = text.split(",") if "," in text else lines for part in parts: segment = part.strip() lower = segment.lower() @@ -284,7 +327,9 @@ async def handle_message(ctx: Context, sender: str, msg: ChatMessage): other_contacts = flag in ["1", "true", "yes"] elif "pagesize:" in lower: try: - page_size = max(1, min(30, int(segment.split(":", 1)[1].strip()))) + page_size = max( + 1, min(30, int(segment.split(":", 1)[1].strip())) + ) except Exception: page_size = 10 elif "person_fields:" in lower: @@ -305,13 +350,17 @@ async def handle_message(ctx: Context, sender: str, msg: ChatMessage): elif not query: # Allow simple language if no explicit Query: provided (e.g., "Search people Rishank Javar") import re as _re3 - m_q = _re3.search(r"search\s+(people|contacts)\s*[,\s]*(.*)", lower_text) + + m_q = _re3.search( + r"search\s+(people|contacts)\s*[,\s]*(.*)", lower_text + ) if m_q and m_q.group(2).strip(): query = m_q.group(2).strip() else: response = "❌ Please provide a search query. Example: Search people John Doe" else: from tools import get_gmail_search_people_tools_for_user + tools = get_gmail_search_people_tools_for_user(sender_email) tool = tools[0] if tools else None if not tool: @@ -327,6 +376,7 @@ async def handle_message(ctx: Context, sender: str, msg: ChatMessage): result = tool.invoke(params) except Exception: import json as _json + result = tool.run(_json.dumps(params)) results = [] @@ -335,16 +385,21 @@ async def handle_message(ctx: Context, sender: str, msg: ChatMessage): data = result if isinstance(result, str): import ast + brace_idx = result.find("{") if brace_idx != -1: data = ast.literal_eval(result[brace_idx:]) if isinstance(data, dict): resp = data.get("data", {}).get("response_data", {}) - raw_list = resp.get("results") or resp.get("connections") or [] + raw_list = ( + resp.get("results") or resp.get("connections") or [] + ) # Normalize items that may be nested under 'person' normalized = [] for item in raw_list: - if isinstance(item, dict) and isinstance(item.get("person"), dict): + if isinstance(item, dict) and isinstance( + item.get("person"), dict + ): normalized.append(item["person"]) else: normalized.append(item) @@ -354,6 +409,7 @@ async def handle_message(ctx: Context, sender: str, msg: ChatMessage): pass from formatting import format_people_search_markdown + response = format_people_search_markdown(results, next_token) # Fallback: if empty, fetch contacts and filter locally for a best-effort match @@ -371,35 +427,54 @@ async def handle_message(ctx: Context, sender: str, msg: ChatMessage): cres = ctool.invoke(cparams) except Exception: import json as _json + cres = ctool.run(_json.dumps(cparams)) cdata = cres if isinstance(cres, str): import ast + brace_idx2 = cres.find("{") if brace_idx2 != -1: cdata = ast.literal_eval(cres[brace_idx2:]) connections = [] if isinstance(cdata, dict): - connections = cdata.get("data", {}).get("response_data", {}).get("connections", []) + connections = ( + cdata.get("data", {}) + .get("response_data", {}) + .get("connections", []) + ) q = query.lower() filtered = [] for person in connections: - names = [n.get("displayName", "") for n in (person.get("names") or [])] - emails = [e.get("value", "") for e in (person.get("emailAddresses") or [])] - if any(q in n.lower() for n in names) or any(q in e.lower() for e in emails): + names = [ + n.get("displayName", "") + for n in (person.get("names") or []) + ] + emails = [ + e.get("value", "") + for e in (person.get("emailAddresses") or []) + ] + if any(q in n.lower() for n in names) or any( + q in e.lower() for e in emails + ): filtered.append(person) - response = format_people_search_markdown(filtered[:10], None) + response = format_people_search_markdown( + filtered[:10], None + ) except Exception: pass except Exception as e: response = f"❌ Error searching people: {str(e)}" - elif any(keyword in lower_text for keyword in ["fetch thread", "get thread messages", "messages in thread"]): + elif any( + keyword in lower_text + for keyword in ["fetch thread", "get thread messages", "messages in thread"] + ): try: # Expect: Thread Id; optional From sender_email = "" thread_id = "" - parts = text.split(',') if ',' in text else text.split('\n') + parts = text.split(",") if "," in text else text.split("\n") for part in parts: segment = part.strip() lower = segment.lower() @@ -422,10 +497,13 @@ async def handle_message(ctx: Context, sender: str, msg: ChatMessage): response = "❌ Please provide a Thread Id. Example: Thread Id: 1995..." else: from tools import get_gmail_thread_fetch_tools_for_user + tools = get_gmail_thread_fetch_tools_for_user(sender_email) tool = tools[0] if tools else None if not tool: - response = "❌ Fetch thread tool unavailable. Please reconnect your Gmail." + response = ( + "❌ Fetch thread tool unavailable. Please reconnect your Gmail." + ) else: params = { "thread_id": thread_id, @@ -438,15 +516,18 @@ async def handle_message(ctx: Context, sender: str, msg: ChatMessage): result = tool.invoke(params) except Exception: import json as _json + result = tool.run(_json.dumps(params)) from formatting import format_thread_messages_markdown + messages = [] next_token = None try: data = result if isinstance(result, str): import ast + brace_idx = result.find("{") if brace_idx != -1: data = ast.literal_eval(result[brace_idx:]) @@ -468,11 +549,18 @@ async def handle_message(ctx: Context, sender: str, msg: ChatMessage): or resp.get("list") or [] ) - if isinstance(raw_messages, dict) and "messages" in raw_messages: + if ( + isinstance(raw_messages, dict) + and "messages" in raw_messages + ): raw_messages = raw_messages.get("messages", []) - if not raw_messages and (resp.get("message") or resp.get("email")): + if not raw_messages and ( + resp.get("message") or resp.get("email") + ): candidate = resp.get("message") or resp.get("email") - raw_messages = [candidate] if isinstance(candidate, dict) else [] + raw_messages = ( + [candidate] if isinstance(candidate, dict) else [] + ) # Normalize message fields for formatter normalized = [] @@ -482,29 +570,45 @@ async def handle_message(ctx: Context, sender: str, msg: ChatMessage): mm = dict(m) if "messageId" not in mm and mm.get("id"): mm["messageId"] = mm.get("id") - if "threadId" not in mm and (mm.get("thread_id") or mm.get("threadId")): - mm["threadId"] = mm.get("thread_id") or mm.get("threadId") + if "threadId" not in mm and ( + mm.get("thread_id") or mm.get("threadId") + ): + mm["threadId"] = mm.get("thread_id") or mm.get( + "threadId" + ) if "sender" not in mm: - mm["sender"] = mm.get("from") or mm.get("From") or mm.get("sender") + mm["sender"] = ( + mm.get("from") + or mm.get("From") + or mm.get("sender") + ) if "to" not in mm: mm["to"] = mm.get("to") or mm.get("To") - if not mm.get("subject") and (mm.get("Subject") is not None): + if not mm.get("subject") and ( + mm.get("Subject") is not None + ): mm["subject"] = mm.get("Subject") if "messageTimestamp" not in mm: - ts = mm.get("timestamp") or mm.get("date") or mm.get("Date") or mm.get("internalDate") + ts = ( + mm.get("timestamp") + or mm.get("date") + or mm.get("Date") + or mm.get("internalDate") + ) if ts: mm["messageTimestamp"] = ts if "messageText" not in mm: - mm["messageText"] = mm.get("text") or mm.get("snippet") or "" + mm["messageText"] = ( + mm.get("text") or mm.get("snippet") or "" + ) if "labelIds" not in mm and mm.get("labels"): mm["labelIds"] = mm.get("labels") normalized.append(mm) messages = normalized - next_token = ( - resp.get("nextPageToken") - or data.get("data", {}).get("nextPageToken") - ) + next_token = resp.get("nextPageToken") or data.get( + "data", {} + ).get("nextPageToken") except Exception: pass @@ -512,10 +616,13 @@ async def handle_message(ctx: Context, sender: str, msg: ChatMessage): except Exception as e: response = f"❌ Error fetching thread messages: {str(e)}" - elif any(keyword in lower_text for keyword in ["get profile", "my profile", "gmail profile"]): + elif any( + keyword in lower_text + for keyword in ["get profile", "my profile", "gmail profile"] + ): try: sender_email = "" - parts = text.split(',') if ',' in text else text.split('\n') + parts = text.split(",") if "," in text else text.split("\n") for part in parts: segment = part.strip() lower = segment.lower() @@ -534,16 +641,20 @@ async def handle_message(ctx: Context, sender: str, msg: ChatMessage): ) else: from tools import get_gmail_profile_tools_for_user + tools = get_gmail_profile_tools_for_user(sender_email) tool = tools[0] if tools else None if not tool: - response = "❌ Profile tool unavailable. Please reconnect your Gmail." + response = ( + "❌ Profile tool unavailable. Please reconnect your Gmail." + ) else: params = {"user_id": "me"} try: result = tool.invoke(params) except Exception: import json as _json + result = tool.run(_json.dumps(params)) profile = {} @@ -551,6 +662,7 @@ async def handle_message(ctx: Context, sender: str, msg: ChatMessage): data = result if isinstance(result, str): import ast + brace_idx = result.find("{") if brace_idx != -1: data = ast.literal_eval(result[brace_idx:]) @@ -560,16 +672,18 @@ async def handle_message(ctx: Context, sender: str, msg: ChatMessage): pass from formatting import format_profile_markdown + response = format_profile_markdown(profile) except Exception as e: response = f"❌ Error fetching profile: {str(e)}" - - elif any(keyword in lower_text for keyword in ["list drafts", "show drafts", "my drafts"]): + elif any( + keyword in lower_text for keyword in ["list drafts", "show drafts", "my drafts"] + ): try: sender_email = "" max_results = 10 - parts = text.split(',') if ',' in text else text.split('\n') + parts = text.split(",") if "," in text else text.split("\n") for part in parts: segment = part.strip() lower = segment.lower() @@ -577,7 +691,9 @@ async def handle_message(ctx: Context, sender: str, msg: ChatMessage): sender_email = segment.split(":", 1)[1].strip() elif "max:" in lower or "max results:" in lower: try: - max_results = max(1, min(100, int(segment.split(":", 1)[1].strip()))) + max_results = max( + 1, min(100, int(segment.split(":", 1)[1].strip())) + ) except Exception: max_results = 10 @@ -593,10 +709,13 @@ async def handle_message(ctx: Context, sender: str, msg: ChatMessage): ) else: from tools import get_gmail_list_drafts_tools_for_user + tools = get_gmail_list_drafts_tools_for_user(sender_email) tool = tools[0] if tools else None if not tool: - response = "❌ List drafts tool unavailable. Please reconnect your Gmail." + response = ( + "❌ List drafts tool unavailable. Please reconnect your Gmail." + ) else: # Send both snake_case and camelCase to support different wrappers params = { @@ -610,6 +729,7 @@ async def handle_message(ctx: Context, sender: str, msg: ChatMessage): result = tool.invoke(params) except Exception: import json as _json + result = tool.run(_json.dumps(params)) drafts = [] @@ -618,6 +738,7 @@ async def handle_message(ctx: Context, sender: str, msg: ChatMessage): data = result if isinstance(result, str): import ast + brace_idx = result.find("{") if brace_idx != -1: data = ast.literal_eval(result[brace_idx:]) @@ -672,13 +793,28 @@ def _find_header_value(headers, header_name): for d in drafts if isinstance(drafts, list) else []: nd = { - "id": d.get("id") or d.get("draftId") or d.get("draft_id") or "", - "to": d.get("to") or d.get("recipient") or d.get("toEmail") or "", - "subject": d.get("subject") or d.get("subjectLine") or "", - "messageTimestamp": d.get("messageTimestamp") or d.get("timestamp") or "", + "id": d.get("id") + or d.get("draftId") + or d.get("draft_id") + or "", + "to": d.get("to") + or d.get("recipient") + or d.get("toEmail") + or "", + "subject": d.get("subject") + or d.get("subjectLine") + or "", + "messageTimestamp": d.get("messageTimestamp") + or d.get("timestamp") + or "", } - message = d.get("message") or d.get("email") or d.get("messageData") or {} + message = ( + d.get("message") + or d.get("email") + or d.get("messageData") + or {} + ) if isinstance(message, dict): payload = message.get("payload") or {} headers = ( @@ -687,52 +823,80 @@ def _find_header_value(headers, header_name): or [] ) if not nd["to"]: - nd["to"] = _find_header_value(headers, "To") or nd["to"] or "" + nd["to"] = ( + _find_header_value(headers, "To") + or nd["to"] + or "" + ) if not nd["subject"]: - nd["subject"] = _find_header_value(headers, "Subject") or nd["subject"] or "" + nd["subject"] = ( + _find_header_value(headers, "Subject") + or nd["subject"] + or "" + ) # Time: try internalDate first (ms since epoch), then Date header if not nd["messageTimestamp"]: - internal_date = message.get("internalDate") or payload.get("internalDate") + internal_date = message.get( + "internalDate" + ) or payload.get("internalDate") if internal_date: try: ts_ms = int(internal_date) # If value looks like seconds, upscale to ms if ts_ms < 10_000_000_000: ts_ms *= 1000 - nd["messageTimestamp"] = datetime.utcfromtimestamp(ts_ms / 1000).isoformat() + nd["messageTimestamp"] = ( + datetime.utcfromtimestamp( + ts_ms / 1000 + ).isoformat() + ) except Exception: nd["messageTimestamp"] = str(internal_date) else: - date_header = _find_header_value(headers, "Date") + date_header = _find_header_value( + headers, "Date" + ) if date_header: nd["messageTimestamp"] = date_header # Final fallbacks if not nd["to"]: - nd["to"] = d.get("to_address") or d.get("recipient_email") or "" + nd["to"] = ( + d.get("to_address") + or d.get("recipient_email") + or "" + ) if not nd["subject"]: - nd["subject"] = d.get("email_subject") or d.get("title") or "" + nd["subject"] = ( + d.get("email_subject") or d.get("title") or "" + ) normalized_drafts.append(nd) except Exception: normalized_drafts = drafts if isinstance(drafts, list) else [] from formatting import format_drafts_list_markdown - response = format_drafts_list_markdown(normalized_drafts, next_token) + + response = format_drafts_list_markdown( + normalized_drafts, next_token + ) except Exception as e: response = f"❌ Error listing drafts: {str(e)}" - elif any(keyword in lower_text for keyword in ["create draft", "draft email", "save draft"]): + elif any( + keyword in lower_text + for keyword in ["create draft", "draft email", "save draft"] + ): try: # Expect: To, Subject, Body, optional HTML, optional From - lines = text.split('\n') + lines = text.split("\n") recipient = "" subject = "" body = "" is_html = False sender_email = "" - parts = text.split(',') if ',' in text else lines + parts = text.split(",") if "," in text else lines for part in parts: segment = part.strip() lower = segment.lower() @@ -762,11 +926,15 @@ def _find_header_value(headers, header_name): response = "❌ Provide To, Subject, and Body to create a draft." else: from tools import get_gmail_draft_tools_for_user + tools = get_gmail_draft_tools_for_user(sender_email) # First tool should be create draft create_tool = None for t in tools: - if getattr(t, "name", "") == "GMAIL_CREATE_EMAIL_DRAFT" or getattr(t, "slug", "") == "GMAIL_CREATE_EMAIL_DRAFT": + if ( + getattr(t, "name", "") == "GMAIL_CREATE_EMAIL_DRAFT" + or getattr(t, "slug", "") == "GMAIL_CREATE_EMAIL_DRAFT" + ): create_tool = t break if not create_tool: @@ -785,6 +953,7 @@ def _find_header_value(headers, header_name): result = create_tool.invoke(params) except Exception: import json as _json + result = create_tool.run(_json.dumps(params)) draft_id = None @@ -793,6 +962,7 @@ def _find_header_value(headers, header_name): data = result if isinstance(result, str): import ast + brace_idx = result.find("{") if brace_idx != -1: data = ast.literal_eval(result[brace_idx:]) @@ -804,7 +974,10 @@ def _find_header_value(headers, header_name): pass from formatting import format_draft_created_markdown - response = format_draft_created_markdown(draft_id or '(unknown)', thread_id, recipient, subject) + + response = format_draft_created_markdown( + draft_id or "(unknown)", thread_id, recipient, subject + ) except Exception as e: response = f"❌ Error creating draft: {str(e)}" @@ -813,7 +986,7 @@ def _find_header_value(headers, header_name): # Expect: Draft Id, optional From draft_id = "" sender_email = "" - parts = text.split(',') if ',' in text else text.split('\n') + parts = text.split(",") if "," in text else text.split("\n") for part in parts: segment = part.strip() lower = segment.lower() @@ -836,35 +1009,46 @@ def _find_header_value(headers, header_name): response = "❌ Please provide a Draft Id. Example: Draft Id: r123..." else: from tools import get_gmail_draft_tools_for_user + tools = get_gmail_draft_tools_for_user(sender_email) send_tool = None for t in tools: - if getattr(t, "name", "") == "GMAIL_SEND_DRAFT" or getattr(t, "slug", "") == "GMAIL_SEND_DRAFT": + if ( + getattr(t, "name", "") == "GMAIL_SEND_DRAFT" + or getattr(t, "slug", "") == "GMAIL_SEND_DRAFT" + ): send_tool = t break if not send_tool: send_tool = tools[0] if tools else None if not send_tool: - response = "❌ Send draft tool unavailable. Please reconnect your Gmail." + response = ( + "❌ Send draft tool unavailable. Please reconnect your Gmail." + ) else: params = {"draft_id": draft_id, "user_id": "me"} try: _ = send_tool.invoke(params) except Exception: import json as _json + _ = send_tool.run(_json.dumps(params)) from formatting import format_draft_sent_markdown + response = format_draft_sent_markdown(draft_id) except Exception as e: response = f"❌ Error sending draft: {str(e)}" - elif any(keyword in lower_text for keyword in ["delete draft", "remove draft", "trash draft"]): + elif any( + keyword in lower_text + for keyword in ["delete draft", "remove draft", "trash draft"] + ): try: # Expect: Draft Id, optional From draft_id = "" sender_email = "" - parts = text.split(',') if ',' in text else text.split('\n') + parts = text.split(",") if "," in text else text.split("\n") for part in parts: segment = part.strip() lower = segment.lower() @@ -887,32 +1071,42 @@ def _find_header_value(headers, header_name): response = "❌ Please provide a Draft Id. Example: Draft Id: r123..." else: from tools import get_gmail_draft_tools_for_user + tools = get_gmail_draft_tools_for_user(sender_email) del_tool = None for t in tools: - if getattr(t, "name", "") == "GMAIL_DELETE_DRAFT" or getattr(t, "slug", "") == "GMAIL_DELETE_DRAFT": + if ( + getattr(t, "name", "") == "GMAIL_DELETE_DRAFT" + or getattr(t, "slug", "") == "GMAIL_DELETE_DRAFT" + ): del_tool = t break if not del_tool: del_tool = tools[0] if tools else None if not del_tool: - response = "❌ Delete draft tool unavailable. Please reconnect your Gmail." + response = ( + "❌ Delete draft tool unavailable. Please reconnect your Gmail." + ) else: params = {"draft_id": draft_id, "user_id": "me"} try: _ = del_tool.invoke(params) except Exception: import json as _json + _ = del_tool.run(_json.dumps(params)) from formatting import format_draft_deleted_markdown + response = format_draft_deleted_markdown(draft_id) except Exception as e: response = f"❌ Error deleting draft: {str(e)}" - elif any(keyword in lower_text for keyword in ["reply", "reply to", "respond", "answer"]): + elif any( + keyword in lower_text for keyword in ["reply", "reply to", "respond", "answer"] + ): try: # Extract thread id, recipient, body, optional flags - lines = text.split('\n') + lines = text.split("\n") thread_id = "" recipient = "" body = "" @@ -965,6 +1159,7 @@ def _find_header_value(headers, header_name): result = reply_tool.invoke(params) except Exception: import json as _json + result = reply_tool.run(_json.dumps(params)) # Extract message id if present @@ -973,6 +1168,7 @@ def _find_header_value(headers, header_name): data = result if isinstance(result, str): import ast + brace_idx = result.find("{") if brace_idx != -1: data = ast.literal_eval(result[brace_idx:]) @@ -982,7 +1178,9 @@ def _find_header_value(headers, header_name): except Exception: pass - response = format_reply_success_markdown(recipient, body, thread_id, message_id) + response = format_reply_success_markdown( + recipient, body, thread_id, message_id + ) else: response = ( "❌ Missing fields. Provide: Thread Id, To/Recipient, Body.\n" @@ -991,9 +1189,12 @@ def _find_header_value(headers, header_name): except Exception as e: response = f"❌ Error replying to thread: {str(e)}" - elif any(keyword in lower_text for keyword in ["send email", "send mail", "email to", "mail to"]): + elif any( + keyword in lower_text + for keyword in ["send email", "send mail", "email to", "mail to"] + ): try: - lines = text.split('\n') + lines = text.split("\n") recipient = "" subject = "" body = "" @@ -1027,7 +1228,9 @@ def _find_header_value(headers, header_name): if not subject: response = "❌ Please provide a subject. Include: Subject: Your subject" elif not body: - response = "❌ Please provide a message body. Include: Body: Your message" + response = ( + "❌ Please provide a message body. Include: Body: Your message" + ) if sender_email: SENDER_TO_USER_EMAIL[sender] = sender_email else: @@ -1044,7 +1247,9 @@ def _find_header_value(headers, header_name): email_result = send_gmail_email(recipient, subject, body, sender_email) if email_result.get("success"): result_obj = email_result.get("result") or {} - meta = extract_gmail_response(result_obj.get("intermediate_steps", [])) + meta = extract_gmail_response( + result_obj.get("intermediate_steps", []) + ) response = format_success_markdown( recipient=recipient, sender_email=sender_email, @@ -1063,11 +1268,12 @@ def _find_header_value(headers, header_name): elif "connect" in text.lower() and "mail" in text.lower(): try: - email_pattern = r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b' + email_pattern = r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b" emails = re.findall(email_pattern, text) email = emails[0] if emails else "" if email: from tools import get_gmail_auth_url + auth_result = get_gmail_auth_url(email) if auth_result["success"]: SENDER_TO_USER_EMAIL[sender] = email @@ -1091,13 +1297,16 @@ def _find_header_value(headers, header_name): elif "help" in text.lower(): response = DEFAULT_HELP_TEXT - await ctx.send(sender, ChatMessage( - timestamp=datetime.utcnow(), - msg_id=uuid4(), - content=[ - TextContent(type="text", text=response), - ] - )) + await ctx.send( + sender, + ChatMessage( + timestamp=datetime.utcnow(), + msg_id=uuid4(), + content=[ + TextContent(type="text", text=response), + ], + ), + ) @protocol.on_message(ChatAcknowledgement) @@ -1106,5 +1315,3 @@ async def handle_ack(ctx: Context, sender: str, msg: ChatAcknowledgement): agent.include(protocol, publish_manifest=True) - - diff --git a/Composio/linkedln/agent.py b/Composio/linkedln/agent.py index fc27a261..239e72e2 100644 --- a/Composio/linkedln/agent.py +++ b/Composio/linkedln/agent.py @@ -31,6 +31,7 @@ ) protocol = Protocol(spec=chat_protocol_spec) + class LinkedInAgent: def __init__(self, user_id: str, auth_config_id: str): self.user_id = user_id @@ -48,8 +49,7 @@ def initiate_auth(self) -> str: print(f"🔐 Initiating LinkedIn auth for {self.user_id}...") # Use the correct method signature according to Composio documentation self.connection_request = self.composio.connected_accounts.initiate( - user_id=self.user_id, - auth_config_id=self.auth_config_id + user_id=self.user_id, auth_config_id=self.auth_config_id ) return f"Please visit this URL to authenticate LinkedIn: {self.connection_request.redirect_url}\nAfter completing, send 'Auth complete' or your next query." except Exception as e: @@ -61,8 +61,12 @@ def complete_auth(self) -> bool: return False try: print("⏳ Checking for authentication completion...") - self.connected_account = self.connection_request.wait_for_connection(timeout=5) - self.tools = self.composio.tools.get(user_id=self.user_id, toolkits=["LINKEDIN"]) + self.connected_account = self.connection_request.wait_for_connection( + timeout=5 + ) + self.tools = self.composio.tools.get( + user_id=self.user_id, toolkits=["LINKEDIN"] + ) print("✅ LinkedIn authentication successful!") return True except Exception as e: @@ -77,19 +81,24 @@ def get_author_id(self) -> str: """Get LinkedIn author ID""" if self.author_id: return self.author_id - + try: response = self.openai_client.chat.completions.create( model="gpt-4o-mini", tools=self.tools, messages=[ - {"role": "system", "content": "Use LINKEDIN_GET_MY_INFO to get profile info."}, - {"role": "user", "content": "Get my LinkedIn profile"} + { + "role": "system", + "content": "Use LINKEDIN_GET_MY_INFO to get profile info.", + }, + {"role": "user", "content": "Get my LinkedIn profile"}, ], ) - - result = self.composio.provider.handle_tool_calls(response=response, user_id=self.user_id) - + + result = self.composio.provider.handle_tool_calls( + response=response, user_id=self.user_id + ) + if result and len(result) > 0: item = result[0] if item.get("successful", False): @@ -99,7 +108,7 @@ def get_author_id(self) -> str: if author_id: self.author_id = author_id return author_id - + return None except Exception as e: print(f"Error getting author ID: {e}") @@ -107,8 +116,10 @@ def get_author_id(self) -> str: def process_query(self, user_query: str) -> Dict[str, Any]: """Process user query and execute LinkedIn actions""" - cleaned_query = re.sub(r'^@composio\s+agent\s+', '', user_query, flags=re.IGNORECASE).strip() - + cleaned_query = re.sub( + r"^@composio\s+agent\s+", "", user_query, flags=re.IGNORECASE + ).strip() + # Simple intent detection if "create" in cleaned_query.lower() and "post" in cleaned_query.lower(): intent = "CREATE_POST" @@ -116,7 +127,10 @@ def process_query(self, user_query: str) -> Dict[str, Any]: intent = "DELETE_POST" elif "info" in cleaned_query.lower() or "profile" in cleaned_query.lower(): intent = "GET_INFO" - elif "connect" in cleaned_query.lower() or "authenticate" in cleaned_query.lower(): + elif ( + "connect" in cleaned_query.lower() + or "authenticate" in cleaned_query.lower() + ): intent = "AUTH" else: intent = "UNKNOWN" @@ -128,7 +142,10 @@ def process_query(self, user_query: str) -> Dict[str, Any]: return {"success": True, "result": auth_url} if not self.is_authenticated(): - return {"success": False, "error": "LinkedIn not authenticated. Send 'Authenticate LinkedIn' to start auth."} + return { + "success": False, + "error": "LinkedIn not authenticated. Send 'Authenticate LinkedIn' to start auth.", + } try: if intent == "CREATE_POST": @@ -138,7 +155,10 @@ def process_query(self, user_query: str) -> Dict[str, Any]: elif intent == "GET_INFO": return self._get_info() else: - return {"success": False, "error": "Unknown command. Try: create post, delete post, or get info"} + return { + "success": False, + "error": "Unknown command. Try: create post, delete post, or get info", + } except Exception as e: return {"success": False, "error": f"Error: {str(e)}"} @@ -150,63 +170,77 @@ def _create_post(self, query: str) -> Dict[str, Any]: # Generate professional blog-style content content = self._generate_blog_content(query) - + try: # Prepare post parameters post_params = f"author={author_id}, commentary='{content}', visibility='PUBLIC', lifecycleState='PUBLISHED'" - + response = self.openai_client.chat.completions.create( model="gpt-4o-mini", tools=self.tools, messages=[ - {"role": "system", "content": "Use LINKEDIN_CREATE_LINKED_IN_POST to create a post. Use the exact parameters provided."}, - {"role": "user", "content": f"Create a LinkedIn post with {post_params}"} + { + "role": "system", + "content": "Use LINKEDIN_CREATE_LINKED_IN_POST to create a post. Use the exact parameters provided.", + }, + { + "role": "user", + "content": f"Create a LinkedIn post with {post_params}", + }, ], ) - - result = self.composio.provider.handle_tool_calls(response=response, user_id=self.user_id) - + + result = self.composio.provider.handle_tool_calls( + response=response, user_id=self.user_id + ) + print(f"🔍 Create post result: {json.dumps(result, indent=2)}") - + if result and len(result) > 0: item = result[0] if item.get("successful", False): data = item.get("data", {}) response_data = data.get("response_data", {}) - + # Try different possible keys for share ID - share_id = (response_data.get("id") or - response_data.get("shareId") or - response_data.get("share_id")) - + share_id = ( + response_data.get("id") + or response_data.get("shareId") + or response_data.get("share_id") + ) + if share_id: # Convert share ID to activity ID for correct URL format - if 'urn:li:share:' in share_id: - activity_id = share_id.replace('urn:li:share:', 'urn:li:activity:') - post_url = f"https://www.linkedin.com/feed/update/{activity_id}/" + if "urn:li:share:" in share_id: + activity_id = share_id.replace( + "urn:li:share:", "urn:li:activity:" + ) + post_url = ( + f"https://www.linkedin.com/feed/update/{activity_id}/" + ) else: - post_url = f"https://www.linkedin.com/feed/update/{share_id}/" - + post_url = ( + f"https://www.linkedin.com/feed/update/{share_id}/" + ) + # Format content with proper line breaks for chat interface - formatted_content = content.replace('\n', '\\n').replace('\r', '') - + formatted_content = content.replace("\n", "\\n").replace( + "\r", "" + ) + result_text = f"✅ **LinkedIn Post Created Successfully!**\\n\\n📝 **Share ID:** `{share_id}`\\n🔗 **Post URL:** {post_url}\\n\\n📄 **Post Content:**\\n\\n{formatted_content}" - - return { - "success": True, - "result": result_text - } + + return {"success": True, "result": result_text} else: # Format content with proper line breaks for chat interface - formatted_content = content.replace('\n', '\\n').replace('\r', '') - + formatted_content = content.replace("\n", "\\n").replace( + "\r", "" + ) + result_text = f"✅ **LinkedIn Post Created Successfully!**\\n\\n📄 **Post Content:**\\n\\n{formatted_content}" - - return { - "success": True, - "result": result_text - } - + + return {"success": True, "result": result_text} + return {"success": False, "error": "Failed to create post"} except Exception as e: return {"success": False, "error": f"Error creating post: {str(e)}"} @@ -214,29 +248,43 @@ def _create_post(self, query: str) -> Dict[str, Any]: def _delete_post(self, query: str) -> Dict[str, Any]: """Delete a LinkedIn post""" # Extract share ID from query - share_id_match = re.search(r'(\d+)', query) + share_id_match = re.search(r"(\d+)", query) if not share_id_match: - return {"success": False, "error": "Please provide share ID. Example: delete post 7371239374954356736"} - + return { + "success": False, + "error": "Please provide share ID. Example: delete post 7371239374954356736", + } + share_id = share_id_match.group(1) - + try: response = self.openai_client.chat.completions.create( model="gpt-4o-mini", tools=self.tools, messages=[ - {"role": "system", "content": "Use LINKEDIN_DELETE_LINKED_IN_POST to delete a post."}, - {"role": "user", "content": f"Delete LinkedIn post with share ID: {share_id}"} + { + "role": "system", + "content": "Use LINKEDIN_DELETE_LINKED_IN_POST to delete a post.", + }, + { + "role": "user", + "content": f"Delete LinkedIn post with share ID: {share_id}", + }, ], ) - - result = self.composio.provider.handle_tool_calls(response=response, user_id=self.user_id) - + + result = self.composio.provider.handle_tool_calls( + response=response, user_id=self.user_id + ) + if result and len(result) > 0: item = result[0] if item.get("successful", False): - return {"success": True, "result": "✅ LinkedIn post deleted successfully!"} - + return { + "success": True, + "result": "✅ LinkedIn post deleted successfully!", + } + return {"success": False, "error": "Failed to delete post"} except Exception as e: return {"success": False, "error": f"Error deleting post: {str(e)}"} @@ -248,21 +296,26 @@ def _get_info(self) -> Dict[str, Any]: model="gpt-4o-mini", tools=self.tools, messages=[ - {"role": "system", "content": "Use LINKEDIN_GET_MY_INFO to get profile info."}, - {"role": "user", "content": "Get my LinkedIn profile information"} + { + "role": "system", + "content": "Use LINKEDIN_GET_MY_INFO to get profile info.", + }, + {"role": "user", "content": "Get my LinkedIn profile information"}, ], ) - - result = self.composio.provider.handle_tool_calls(response=response, user_id=self.user_id) - + + result = self.composio.provider.handle_tool_calls( + response=response, user_id=self.user_id + ) + print(f"🔍 Profile result: {json.dumps(result, indent=2)}") - + if result and len(result) > 0: item = result[0] if item.get("successful", False): data = item.get("data", {}) response_data = data.get("response_dict", {}) - + name = response_data.get("name", "Unknown") given_name = response_data.get("given_name", "") family_name = response_data.get("family_name", "") @@ -272,9 +325,17 @@ def _get_info(self) -> Dict[str, Any]: sub_id = response_data.get("sub", "Not available") picture_url = response_data.get("picture", "Not available") locale = response_data.get("locale", {}) - country = locale.get("country", "Not specified") if isinstance(locale, dict) else "Not specified" - language = locale.get("language", "Not specified") if isinstance(locale, dict) else "Not specified" - + country = ( + locale.get("country", "Not specified") + if isinstance(locale, dict) + else "Not specified" + ) + language = ( + locale.get("language", "Not specified") + if isinstance(locale, dict) + else "Not specified" + ) + return { "success": True, "result": f"""👤 **LinkedIn Profile Information** @@ -283,7 +344,7 @@ def _get_info(self) -> Dict[str, Any]: **Given Name:** {given_name} **Family Name:** {family_name} **Email:** {email} -**Email Verified:** {'Yes' if email_verified else 'No'} +**Email Verified:** {"Yes" if email_verified else "No"} **Country:** {country} **Language:** {language} **Author ID:** {author_id} @@ -291,9 +352,9 @@ def _get_info(self) -> Dict[str, Any]: **Profile Picture:** ![Profile Picture]({picture_url}) - """ + """, } - + return {"success": False, "error": "Failed to get profile info"} except Exception as e: return {"success": False, "error": f"Error getting profile: {str(e)}"} @@ -303,12 +364,14 @@ def _generate_blog_content(self, query: str) -> str: try: # Extract topic from query topic = self._extract_topic(query) - + # Generate comprehensive blog content within LinkedIn's 3000 character limit response = self.openai_client.chat.completions.create( model="gpt-4o-mini", messages=[ - {"role": "system", "content": """You are a professional LinkedIn content writer. Create comprehensive, detailed blog posts that: + { + "role": "system", + "content": """You are a professional LinkedIn content writer. Create comprehensive, detailed blog posts that: - Are 6-8 sentences long (detailed but concise) - Use professional but engaging tone - Include relevant emojis (2-3 max) @@ -321,23 +384,31 @@ def _generate_blog_content(self, query: str) -> str: - Provide substantial value to the LinkedIn professional community - Include specific examples or statistics when relevant - IMPORTANT: Keep total content under 2800 characters to stay within LinkedIn's 3000 character limit - - Make it feel like a comprehensive article but concise enough for LinkedIn"""}, - {"role": "user", "content": f"Create a comprehensive LinkedIn blog post about: {topic}. Make it detailed and insightful but keep it under 2800 characters total."} + - Make it feel like a comprehensive article but concise enough for LinkedIn""", + }, + { + "role": "user", + "content": f"Create a comprehensive LinkedIn blog post about: {topic}. Make it detailed and insightful but keep it under 2800 characters total.", + }, ], max_tokens=800, - temperature=0.7 + temperature=0.7, ) - + generated_content = response.choices[0].message.content.strip() - + # Ensure content is within LinkedIn's 3000 character limit if len(generated_content) > 2800: - print(f"⚠️ Content too long ({len(generated_content)} chars), truncating...") + print( + f"⚠️ Content too long ({len(generated_content)} chars), truncating..." + ) generated_content = generated_content[:2800] + "..." - - print(f"📝 Generated content ({len(generated_content)} chars): {generated_content}") + + print( + f"📝 Generated content ({len(generated_content)} chars): {generated_content}" + ) return generated_content - + except Exception as e: print(f"Error generating content: {e}") # Fallback content @@ -351,7 +422,7 @@ def _extract_topic(self, query: str) -> str: parts = query.lower().split("about") if len(parts) > 1: return parts[1].strip() - + # Extract text after "post" if "post" in query.lower(): parts = query.lower().split("post") @@ -360,7 +431,7 @@ def _extract_topic(self, query: str) -> str: if topic.startswith("about"): topic = topic.replace("about", "").strip() return topic - + # Default topic return "my latest update" @@ -436,37 +507,37 @@ def get_blog_examples(self) -> str: Each post will automatically generate professional content and a relevant image! 🖼️""" + # Initialize LinkedInAgent linkedin_agent = LinkedInAgent( - user_id="", - auth_config_id=os.getenv("LINKEDIN_AUTH_CONFIG_ID") + user_id="", auth_config_id=os.getenv("LINKEDIN_AUTH_CONFIG_ID") ) + def extract_user_id_from_query(text: str) -> str: """Extract LinkedIn username from query""" - patterns = [ - r'linkedin\s+(\w+)', - r'(\w+)\s+linkedin', - r'(\w+)$' - ] - + patterns = [r"linkedin\s+(\w+)", r"(\w+)\s+linkedin", r"(\w+)$"] + for pattern in patterns: match = re.search(pattern, text, re.IGNORECASE) if match: username = match.group(1).strip() if username.isalnum() and len(username) >= 3: return username - + return "" + @protocol.on_message(ChatMessage) async def handle_message(ctx: Context, sender: str, msg: ChatMessage): """Handle incoming messages""" await ctx.send( sender, - ChatAcknowledgement(timestamp=datetime.now(timezone.utc), acknowledged_msg_id=msg.msg_id), + ChatAcknowledgement( + timestamp=datetime.now(timezone.utc), acknowledged_msg_id=msg.msg_id + ), ) - + text = "" for item in msg.content: if isinstance(item, TextContent): @@ -475,7 +546,13 @@ async def handle_message(ctx: Context, sender: str, msg: ChatMessage): print(f"📥 Received query: {text}") # Check if this is a help request - if text.lower().strip() in ["help", "what can i do", "available operations", "permissions", "scopes"]: + if text.lower().strip() in [ + "help", + "what can i do", + "available operations", + "permissions", + "scopes", + ]: response = linkedin_agent.get_available_operations() elif text.lower().strip() in ["blog examples", "examples", "blog post examples"]: response = linkedin_agent.get_blog_examples() @@ -511,15 +588,17 @@ async def handle_message(ctx: Context, sender: str, msg: ChatMessage): ChatMessage( timestamp=datetime.now(timezone.utc), msg_id=uuid4(), - content=[TextContent(type="text", text=response)] - ) + content=[TextContent(type="text", text=response)], + ), ) + @protocol.on_message(ChatAcknowledgement) async def handle_ack(ctx: Context, sender: str, msg: ChatAcknowledgement): """Handle acknowledgment""" pass + agent.include(protocol, publish_manifest=True) if __name__ == "__main__": @@ -527,4 +606,4 @@ async def handle_ack(ctx: Context, sender: str, msg: ChatAcknowledgement): try: agent.run() except KeyboardInterrupt: - print("🛑 Agent stopped gracefully. Goodbye!") \ No newline at end of file + print("🛑 Agent stopped gracefully. Goodbye!") diff --git a/Crewai-agents/Blood-Report-Analysis-Agent/scripts/agents.py b/Crewai-agents/Blood-Report-Analysis-Agent/scripts/agents.py index 689ae395..530833f6 100644 --- a/Crewai-agents/Blood-Report-Analysis-Agent/scripts/agents.py +++ b/Crewai-agents/Blood-Report-Analysis-Agent/scripts/agents.py @@ -4,10 +4,13 @@ FileReadTool, SerperDevTool, WebsiteSearchTool, - PDFSearchTool + PDFSearchTool, ) import os -os.environ["SERPER_API_KEY"] = "b924f60ae1564e450f297dc464410ff7190abb85" + +os.environ["SERPER_API_KEY"] = "b924f60ae1564e450f297dc464410ff7190abb85" + + class BloodReportAgents: def blood_report_analyst(self): return Agent( @@ -24,12 +27,9 @@ def blood_report_analyst(self): "You are adept at translating complex medical data into clear, actionable insights for healthcare " "professionals and patients alike." ), - tools=[ - PDFSearchTool(), - DirectoryReadTool(directory="input") - ], + tools=[PDFSearchTool(), DirectoryReadTool(directory="input")], allow_delegation=False, - verbose=True + verbose=True, ) def health_advisor(self): @@ -53,8 +53,8 @@ def health_advisor(self): SerperDevTool(), WebsiteSearchTool(), FileReadTool(), - DirectoryReadTool(directory="output") + DirectoryReadTool(directory="output"), ], allow_delegation=False, - verbose=True + verbose=True, ) diff --git a/Crewai-agents/Blood-Report-Analysis-Agent/scripts/main.py b/Crewai-agents/Blood-Report-Analysis-Agent/scripts/main.py index 41ae48e6..6c0c11e0 100644 --- a/Crewai-agents/Blood-Report-Analysis-Agent/scripts/main.py +++ b/Crewai-agents/Blood-Report-Analysis-Agent/scripts/main.py @@ -9,15 +9,16 @@ from pdf_downloader import download_pdf_from_drive from pdf_to_text import pdf_to_text -from agents import BloodReportAgents -from tasks import BloodReportTasks +from agents import BloodReportAgents +from tasks import BloodReportTasks load_dotenv() + class BloodReportCrew: def __init__(self, drive_link, text_file): self.drive_link = drive_link - self.text_file = text_file or "input/blood_report.txt" + self.text_file = text_file or "input/blood_report.txt" def run(self): # Validate inputs @@ -45,7 +46,7 @@ def run(self): crew = Crew( agents=[analyst, advisor], tasks=[analyze_task, recommend_task], - verbose=True + verbose=True, ) crew.kickoff() @@ -58,15 +59,18 @@ def run(self): # Check if files exist and contain valid content try: if os.path.exists(summary_path) and os.path.getsize(summary_path) > 0: - with open(summary_path, 'r', encoding='utf-8') as f: + with open(summary_path, "r", encoding="utf-8") as f: summary_md = f.read().strip() if not summary_md: return f"Error: {summary_path} is empty" else: return f"Error: {summary_path} does not exist or is empty" - if os.path.exists(recommendations_path) and os.path.getsize(recommendations_path) > 0: - with open(recommendations_path, 'r', encoding='utf-8') as f: + if ( + os.path.exists(recommendations_path) + and os.path.getsize(recommendations_path) > 0 + ): + with open(recommendations_path, "r", encoding="utf-8") as f: recommendations_md = f.read().strip() if not recommendations_md: return f"Error: {recommendations_path} is empty" @@ -111,6 +115,7 @@ def kickoff(self, inputs=None): self.text_file = inputs.get("text_file", self.text_file) return self.run() + def main(): """Main function to demonstrate Blood Report Analysis with uAgents adapter.""" api_key = os.getenv("AGENTVERSE_API_KEY") @@ -124,16 +129,12 @@ def main(): os.environ["OPENAI_API_KEY"] = openai_api_key os.environ["SERPER_API_KEY"] = serper_api_key - - crew = BloodReportCrew( - drive_link="", - text_file="input/blood_report.txt" - ) + crew = BloodReportCrew(drive_link="", text_file="input/blood_report.txt") register_tool = CrewaiRegisterTool() query_params = { "drive_link": {"type": "str", "required": True}, - "text_file": {"type": "str", "required": False} + "text_file": {"type": "str", "required": False}, } result = register_tool.run( @@ -154,9 +155,11 @@ def main(): try: while True: import time + time.sleep(1) except KeyboardInterrupt: print("\nExiting...") + if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/Crewai-agents/Blood-Report-Analysis-Agent/scripts/pdf_downloader.py b/Crewai-agents/Blood-Report-Analysis-Agent/scripts/pdf_downloader.py index 43905858..505effe3 100644 --- a/Crewai-agents/Blood-Report-Analysis-Agent/scripts/pdf_downloader.py +++ b/Crewai-agents/Blood-Report-Analysis-Agent/scripts/pdf_downloader.py @@ -1,14 +1,14 @@ - import requests import os + def download_pdf_from_drive(drive_link, output_path): try: - file_id = drive_link.split('/d/')[1].split('/')[0] + file_id = drive_link.split("/d/")[1].split("/")[0] download_url = f"https://drive.google.com/uc?export=download&id={file_id}" response = requests.get(download_url, stream=True) if response.status_code == 200: - with open(output_path, 'wb') as f: + with open(output_path, "wb") as f: for chunk in response.iter_content(chunk_size=8192): f.write(chunk) print(f"PDF downloaded to {output_path}") @@ -17,6 +17,7 @@ def download_pdf_from_drive(drive_link, output_path): except Exception as e: print(f"Error downloading PDF: {e}") + if __name__ == "__main__": drive_link = "" os.makedirs("input", exist_ok=True) diff --git a/Crewai-agents/Blood-Report-Analysis-Agent/scripts/pdf_to_text.py b/Crewai-agents/Blood-Report-Analysis-Agent/scripts/pdf_to_text.py index 79e73060..99fecb03 100644 --- a/Crewai-agents/Blood-Report-Analysis-Agent/scripts/pdf_to_text.py +++ b/Crewai-agents/Blood-Report-Analysis-Agent/scripts/pdf_to_text.py @@ -1,20 +1,20 @@ - import PyPDF2 -import os + def pdf_to_text(pdf_path, text_path): try: - with open(pdf_path, 'rb') as file: + with open(pdf_path, "rb") as file: reader = PyPDF2.PdfReader(file) text = "" for page in reader.pages: text += page.extract_text() + "\n" - with open(text_path, 'w', encoding='utf-8') as text_file: + with open(text_path, "w", encoding="utf-8") as text_file: text_file.write(text) print(f"PDF converted to text and saved to {text_path}") except Exception as e: print(f"Error converting PDF to text: {e}") + if __name__ == "__main__": pdf_path = "input/blood_report.pdf" text_path = "input/blood_report.txt" diff --git a/Crewai-agents/Blood-Report-Analysis-Agent/scripts/tasks.py b/Crewai-agents/Blood-Report-Analysis-Agent/scripts/tasks.py index 24b3f9c8..c7eb3dc3 100644 --- a/Crewai-agents/Blood-Report-Analysis-Agent/scripts/tasks.py +++ b/Crewai-agents/Blood-Report-Analysis-Agent/scripts/tasks.py @@ -1,20 +1,20 @@ - from crewai import Task + class BloodReportTasks: def analyze_blood_report(self, agent, pdf_path): return Task( - description=f'Analyze the blood test report provided in the PDF file at {pdf_path}. Summarize key health indicators, including their significance, normal ranges, and potential health implications for abnormal results. Ensure the analysis is detailed and medically accurate, suitable, provied medicine and medicine link medications for a senior blood report analyst .', - expected_output='A Markdown file containing a detailed summary of key health indicators, including their values, reference ranges, and explanations of their significance.', + description=f"Analyze the blood test report provided in the PDF file at {pdf_path}. Summarize key health indicators, including their significance, normal ranges, and potential health implications for abnormal results. Ensure the analysis is detailed and medically accurate, suitable, provied medicine and medicine link medications for a senior blood report analyst .", + expected_output="A Markdown file containing a detailed summary of key health indicators, including their values, reference ranges, and explanations of their significance.", agent=agent, - output_file='output/blood_report_summary.md' + output_file="output/blood_report_summary.md", ) def generate_recommendations(self, agent, context): return Task( - description='Provide detailed health recommendations for each blood test indicator in the summary, including what the indicator means, how to control abnormal results, additional context, dietary advice, exercise routines, provoed medicine and medicine link medications (with a disclaimer to consult a healthcare provider), and lifestyle changes. Include information from related articles and their URLs. Citing source URLs is mandatory And also provied the hospital, clinic and lab map link accordind to lab location area city.', - expected_output='A Markdown file containing detailed health recommendations for each indicator, including control measures, explanations, and source URLs.', + description="Provide detailed health recommendations for each blood test indicator in the summary, including what the indicator means, how to control abnormal results, additional context, dietary advice, exercise routines, provoed medicine and medicine link medications (with a disclaimer to consult a healthcare provider), and lifestyle changes. Include information from related articles and their URLs. Citing source URLs is mandatory And also provied the hospital, clinic and lab map link accordind to lab location area city.", + expected_output="A Markdown file containing detailed health recommendations for each indicator, including control measures, explanations, and source URLs.", agent=agent, context=context, - output_file='output/health_recommendations.md' + output_file="output/health_recommendations.md", ) diff --git a/Crewai-agents/Prep-for-a-meeting-Agent/agents.py b/Crewai-agents/Prep-for-a-meeting-Agent/agents.py index 1f62551b..529a0328 100644 --- a/Crewai-agents/Prep-for-a-meeting-Agent/agents.py +++ b/Crewai-agents/Prep-for-a-meeting-Agent/agents.py @@ -3,50 +3,51 @@ from tools.ExaSearchTool import tools -class MeetingPreparationAgents(): - def research_agent(self): - return Agent( - role='Research Specialist', - goal='Conduct thorough research on people and companies involved in the meeting', - tools=tools(), - backstory=dedent("""\ + +class MeetingPreparationAgents: + def research_agent(self): + return Agent( + role="Research Specialist", + goal="Conduct thorough research on people and companies involved in the meeting", + tools=tools(), + backstory=dedent("""\ As a Research Specialist, your mission is to uncover detailed information about the individuals and entities participating in the meeting. Your insights will lay the groundwork for strategic meeting preparation."""), - verbose=True - ) + verbose=True, + ) - def industry_analysis_agent(self): - return Agent( - role='Industry Analyst', - goal='Analyze the current industry trends, challenges, and opportunities', - tools=tools(), - backstory=dedent("""\ + def industry_analysis_agent(self): + return Agent( + role="Industry Analyst", + goal="Analyze the current industry trends, challenges, and opportunities", + tools=tools(), + backstory=dedent("""\ As an Industry Analyst, your analysis will identify key trends, challenges facing the industry, and potential opportunities that could be leveraged during the meeting for strategic advantage."""), - verbose=True - ) + verbose=True, + ) - def meeting_strategy_agent(self): - return Agent( - role='Meeting Strategy Advisor', - goal='Develop talking points, questions, and strategic angles for the meeting', - tools=tools(), - backstory=dedent("""\ + def meeting_strategy_agent(self): + return Agent( + role="Meeting Strategy Advisor", + goal="Develop talking points, questions, and strategic angles for the meeting", + tools=tools(), + backstory=dedent("""\ As a Strategy Advisor, your expertise will guide the development of talking points, insightful questions, and strategic angles to ensure the meeting's objectives are achieved."""), - verbose=True - ) + verbose=True, + ) - def summary_and_briefing_agent(self): - return Agent( - role='Briefing Coordinator', - goal='Compile all gathered information into a concise, informative briefing document', - tools=tools(), - backstory=dedent("""\ + def summary_and_briefing_agent(self): + return Agent( + role="Briefing Coordinator", + goal="Compile all gathered information into a concise, informative briefing document", + tools=tools(), + backstory=dedent("""\ As the Briefing Coordinator, your role is to consolidate the research, analysis, and strategic insights."""), - verbose=True - ) \ No newline at end of file + verbose=True, + ) diff --git a/Crewai-agents/Prep-for-a-meeting-Agent/tasks.py b/Crewai-agents/Prep-for-a-meeting-Agent/tasks.py index bcf28d84..a628390e 100644 --- a/Crewai-agents/Prep-for-a-meeting-Agent/tasks.py +++ b/Crewai-agents/Prep-for-a-meeting-Agent/tasks.py @@ -1,10 +1,11 @@ from textwrap import dedent from crewai import Task -class MeetingPreparationTasks(): - def research_task(self, agent, participants, context): - return Task( - description=dedent(f"""\ + +class MeetingPreparationTasks: + def research_task(self, agent, participants, context): + return Task( + description=dedent(f"""\ Conduct comprehensive research on each of the individuals and companies involved in the upcoming meeting. Gather information on recent news, achievements, professional background, and any relevant @@ -12,16 +13,16 @@ def research_task(self, agent, participants, context): Participants: {participants} Meeting Context: {context}"""), - expected_output=dedent("""\ + expected_output=dedent("""\ A detailed report summarizing key findings about each participant and company, highlighting information that could be relevant for the meeting."""), - async_execution=True, - agent=agent - ) + async_execution=True, + agent=agent, + ) - def industry_analysis_task(self, agent, participants, context): - return Task( - description=dedent(f"""\ + def industry_analysis_task(self, agent, participants, context): + return Task( + description=dedent(f"""\ Analyze the current industry trends, challenges, and opportunities relevant to the meeting's context. Consider market reports, recent developments, and expert opinions to provide a comprehensive @@ -29,30 +30,30 @@ def industry_analysis_task(self, agent, participants, context): Participants: {participants} Meeting Context: {context}"""), - expected_output=dedent("""\ + expected_output=dedent("""\ An insightful analysis that identifies major trends, potential challenges, and strategic opportunities."""), - async_execution=True, - agent=agent - ) + async_execution=True, + agent=agent, + ) - def meeting_strategy_task(self, agent, context, objective): - return Task( - description=dedent(f"""\ + def meeting_strategy_task(self, agent, context, objective): + return Task( + description=dedent(f"""\ Develop strategic talking points, questions, and discussion angles for the meeting based on the research and industry analysis conducted Meeting Context: {context} Meeting Objective: {objective}"""), - expected_output=dedent("""\ + expected_output=dedent("""\ Complete report with a list of key talking points, strategic questions to ask to help achieve the meetings objective during the meeting."""), - agent=agent - ) + agent=agent, + ) - def summary_and_briefing_task(self, agent, context, objective): - return Task( - description=dedent(f"""\ + def summary_and_briefing_task(self, agent, context, objective): + return Task( + description=dedent(f"""\ Compile all the research findings, industry analysis, and strategic talking points into a concise, comprehensive briefing document for the meeting. @@ -61,9 +62,9 @@ def summary_and_briefing_task(self, agent, context, objective): Meeting Context: {context} Meeting Objective: {objective}"""), - expected_output=dedent("""\ + expected_output=dedent("""\ A well-structured briefing document that includes sections for participant bios, industry overview, talking points, and strategic recommendations."""), - agent=agent - ) \ No newline at end of file + agent=agent, + ) diff --git a/Crewai-agents/Prep-for-a-meeting-Agent/tools/ExaSearchTool.py b/Crewai-agents/Prep-for-a-meeting-Agent/tools/ExaSearchTool.py index 312f9f11..3292831d 100644 --- a/Crewai-agents/Prep-for-a-meeting-Agent/tools/ExaSearchTool.py +++ b/Crewai-agents/Prep-for-a-meeting-Agent/tools/ExaSearchTool.py @@ -1,13 +1,14 @@ import os -from typing import List, Optional +from typing import List from pydantic import BaseModel, Field from crewai.tools import BaseTool from exa_py import Exa + # Singleton pattern for Exa API instance class ExaAPI: _instance = None - + @classmethod def get_instance(cls): if cls._instance is None: @@ -17,29 +18,37 @@ def get_instance(cls): cls._instance = Exa(api_key=api_key) return cls._instance + class SearchSchema(BaseModel): query: str = Field(description="The search query to look up") + class ExaSearchTool(BaseTool): name: str = "search" - description: str = "Search the web using Exa, one of the best web search APIs for AI." + description: str = ( + "Search the web using Exa, one of the best web search APIs for AI." + ) args_schema: BaseModel = SearchSchema - + def _run(self, query: str) -> str: try: - results = ExaAPI.get_instance().search(query, use_autoprompt=True, num_results=3) + results = ExaAPI.get_instance().search( + query, use_autoprompt=True, num_results=3 + ) return str(results) except Exception as e: return f"Error searching: {str(e)}" + class FindSimilarSchema(BaseModel): url: str = Field(description="URL to find similar content for") + class ExaFindSimilarTool(BaseTool): name: str = "find_similar" description: str = "Search for webpages similar to a given URL. The URL should be from a previous search result." args_schema: BaseModel = FindSimilarSchema - + def _run(self, url: str) -> str: try: results = ExaAPI.get_instance().find_similar(url, num_results=3) @@ -47,39 +56,47 @@ def _run(self, url: str) -> str: except Exception as e: return f"Error finding similar: {str(e)}" + class GetContentsSchema(BaseModel): ids: List[str] = Field(description="List of Exa result IDs to retrieve content for") + class ExaGetContentsTool(BaseTool): name: str = "get_contents" description: str = "Get the contents of webpages. Provide a list of result IDs from a previous search." args_schema: BaseModel = GetContentsSchema - + def _run(self, ids: List[str]) -> str: try: # Convert string representation of list to actual list if needed - if isinstance(ids, str) and ids.startswith('[') and ids.endswith(']'): + if isinstance(ids, str) and ids.startswith("[") and ids.endswith("]"): import ast + ids = ast.literal_eval(ids) elif isinstance(ids, str): # If a single string ID was passed ids = [ids] - + results = ExaAPI.get_instance().get_contents(ids) - + # Format the results for readability formatted_results = [] if isinstance(results, list): for result in results: - formatted_results.append(f"URL: {result.url}\nTitle: {result.title}\nContent: {result.text[:1000]}...") + formatted_results.append( + f"URL: {result.url}\nTitle: {result.title}\nContent: {result.text[:1000]}..." + ) else: # Handle case where a single result is returned - formatted_results.append(f"URL: {results.url}\nTitle: {results.title}\nContent: {results.text[:1000]}...") - + formatted_results.append( + f"URL: {results.url}\nTitle: {results.title}\nContent: {results.text[:1000]}..." + ) + return "\n\n---\n\n".join(formatted_results) except Exception as e: return f"Error getting contents: {str(e)}" + def tools(): """Return a list of tool instances for use in agents.""" return [ExaSearchTool(), ExaFindSimilarTool(), ExaGetContentsTool()] diff --git a/Crewai-agents/Prep-for-a-meeting-Agent/uagent_main.py b/Crewai-agents/Prep-for-a-meeting-Agent/uagent_main.py index 6883f909..4e441ccd 100644 --- a/Crewai-agents/Prep-for-a-meeting-Agent/uagent_main.py +++ b/Crewai-agents/Prep-for-a-meeting-Agent/uagent_main.py @@ -3,7 +3,7 @@ import os from openai import OpenAI -from typing import Dict, Any +from typing import Dict import json from crewai import Crew from dotenv import load_dotenv @@ -26,18 +26,18 @@ def parse_input_message(self, message: str) -> Dict[str, str]: """ try: client = OpenAI() - + print(f"Attempting to parse message: {message}") - + response = client.chat.completions.create( model="gpt-4o", messages=[ { - "role": "system", - "content": "You are a helpful assistant that extracts structured information from meeting descriptions. Always respond with valid JSON only." + "role": "system", + "content": "You are a helpful assistant that extracts structured information from meeting descriptions. Always respond with valid JSON only.", }, { - "role": "user", + "role": "user", "content": f""" Extract the following information from this meeting description and return as JSON: - participants: array of email addresses found in the text @@ -52,32 +52,35 @@ def parse_input_message(self, message: str) -> Dict[str, str]: "context": "brief meeting context", "objective": "what user wants to achieve" }} - """ - } + """, + }, ], temperature=0.1, - max_tokens=500 + max_tokens=500, ) - + print(f"Received response from OpenAI: {response}") - + result = json.loads(response.choices[0].message.content) - + print(f"Parsed result: {result}") - + return { "participants": ", ".join(result.get("participants", [])), "context": result.get("context", message), - "objective": result.get("objective", "Prepare thoroughly for the meeting and achieve positive outcomes") + "objective": result.get( + "objective", + "Prepare thoroughly for the meeting and achieve positive outcomes", + ), } - + except (Exception, json.JSONDecodeError) as e: print(f"Error parsing message with OpenAI: {e}") # Fallback to using original message as context return { "participants": "", "context": message, - "objective": "Prepare thoroughly for the meeting and achieve positive outcomes" + "objective": "Prepare thoroughly for the meeting and achieve positive outcomes", } def run(self): @@ -87,40 +90,42 @@ def run(self): # Create agents researcher_agent = agents.research_agent() - industry_analysis_agent = agents.industry_analysis_agent() + industry_analysis_agent = agents.industry_analysis_agent() meeting_strategist_agent = agents.meeting_strategy_agent() briefing_coordinator_agent = agents.summary_and_briefing_agent() # Create tasks research_task = tasks.research_task( - researcher_agent, - self.participants, - self.context + researcher_agent, self.participants, self.context ) - + industry_analysis_task = tasks.industry_analysis_task( - industry_analysis_agent, - self.participants, - self.context + industry_analysis_agent, self.participants, self.context ) - + meeting_strategy_task = tasks.meeting_strategy_task( - meeting_strategist_agent, - self.context, - self.objective + meeting_strategist_agent, self.context, self.objective ) - + summary_task = tasks.summary_and_briefing_task( - briefing_coordinator_agent, - self.context, - self.objective + briefing_coordinator_agent, self.context, self.objective ) # Create crew crew = Crew( - agents=[researcher_agent, industry_analysis_agent, meeting_strategist_agent, briefing_coordinator_agent], - tasks=[research_task, industry_analysis_task, meeting_strategy_task, summary_task], - verbose=True + agents=[ + researcher_agent, + industry_analysis_agent, + meeting_strategist_agent, + briefing_coordinator_agent, + ], + tasks=[ + research_task, + industry_analysis_task, + meeting_strategy_task, + summary_task, + ], + verbose=True, ) result = crew.kickoff() @@ -136,7 +141,7 @@ def kickoff(self, inputs=None): # Parse natural language input parsed = self.parse_input_message(inputs) self.participants = parsed["participants"] - self.context = parsed["context"] + self.context = parsed["context"] self.objective = parsed["objective"] elif isinstance(inputs, dict): # Handle structured input @@ -155,21 +160,21 @@ def kickoff(self, inputs=None): def main(): """Main function to register Meeting Prep Crew with uAgents.""" - + # Load environment variables load_dotenv() api_key = os.getenv("AGENTVERSE_API_KEY") openai_api_key = os.getenv("OPENAI_API_KEY") exa_api_key = os.getenv("EXA_API_KEY") - + if not api_key: print("Error: AGENTVERSE_API_KEY not found in environment") return - + if not openai_api_key: print("Error: OPENAI_API_KEY not found in environment") return - + if not exa_api_key: print("Error: EXA_API_KEY not found in environment") return @@ -187,9 +192,9 @@ def main(): # Define parameters schema - flexible to accept natural language query_params = { "message": { - "type": "str", + "type": "str", "required": True, - "description": "Natural language message containing meeting details, participant emails, and objectives" + "description": "Natural language message containing meeting details, participant emails, and objectives", } } @@ -209,10 +214,10 @@ def main(): # Print registration result print(f"\nMeeting Prep CrewAI agent registration result: {result}") - + if isinstance(result, dict) and "address" in result: print(f"Agent address: {result['address']}") - print(f"You can now interact with this agent through ASI:One LLM!") + print("You can now interact with this agent through ASI:One LLM!") # Keep the program running try: @@ -220,6 +225,7 @@ def main(): print("Press Ctrl+C to stop the agent.") while True: import time + time.sleep(1) except KeyboardInterrupt: print("\nStopping agent...") diff --git a/Crewai-agents/code_analyzer/agents/code_agents.py b/Crewai-agents/code_analyzer/agents/code_agents.py index 6124e78d..8d52efb9 100644 --- a/Crewai-agents/code_analyzer/agents/code_agents.py +++ b/Crewai-agents/code_analyzer/agents/code_agents.py @@ -2,6 +2,7 @@ from langchain_openai import ChatOpenAI from langchain_core.prompts import PromptTemplate + class CodeAgents: def __init__(self): # Initialize the LLM with enhanced prompt for fetching relevant links @@ -14,7 +15,7 @@ def __init__(self): Additionally, include relevant resources from Medium, StackOverflow, and Dev.to related to the query or code. Format links as markdown: [Source Title](URL). Ensure all code is syntactically correct and follows best practices (e.g., PEP 8 for Python). - """) + """), ) def code_analyzer_agent(self): @@ -51,4 +52,4 @@ def code_writer_agent(self): backstory="A skilled developer proficient in writing clean, efficient, and well-documented code across multiple languages", verbose=True, llm=self.llm, - ) \ No newline at end of file + ) diff --git a/Crewai-agents/code_analyzer/main.py b/Crewai-agents/code_analyzer/main.py index b2dc819f..9e81acb0 100644 --- a/Crewai-agents/code_analyzer/main.py +++ b/Crewai-agents/code_analyzer/main.py @@ -8,8 +8,11 @@ from agents.code_agents import CodeAgents from tasks.code_tasks import CodeTasks + class CodeAnalyzerCrew: - def __init__(self, code_snippet="", language="python", error_log="", code_requirements=""): + def __init__( + self, code_snippet="", language="python", error_log="", code_requirements="" + ): self.code_snippet = code_snippet self.language = language self.error_log = error_log @@ -18,10 +21,16 @@ def __init__(self, code_snippet="", language="python", error_log="", code_requir def validate_inputs(self): """Validate input parameters.""" if not self.code_snippet and not self.code_requirements: - raise ValueError("Either code_snippet or code_requirements must be provided") + raise ValueError( + "Either code_snippet or code_requirements must be provided" + ) if not self.language: raise ValueError("Language must be specified") - if self.language.lower() not in ["python", "javascript", "java",]: + if self.language.lower() not in [ + "python", + "javascript", + "java", + ]: raise ValueError(f"Unsupported language: {self.language}") def run(self): @@ -36,12 +45,23 @@ def run(self): task_list = [] if self.code_requirements: - write_task = tasks.write_task(writer_agent, self.code_requirements, self.language) + write_task = tasks.write_task( + writer_agent, self.code_requirements, self.language + ) task_list.append(write_task) if self.code_snippet: - analyze_task = tasks.analyze_task(analyzer_agent, self.code_snippet, self.language) - debug_task = tasks.debug_task(debug_agent, self.code_snippet, self.language, self.error_log) - fix_task = tasks.fix_task(fixer_agent, self.code_snippet, self.language, debug_task.expected_output) + analyze_task = tasks.analyze_task( + analyzer_agent, self.code_snippet, self.language + ) + debug_task = tasks.debug_task( + debug_agent, self.code_snippet, self.language, self.error_log + ) + fix_task = tasks.fix_task( + fixer_agent, + self.code_snippet, + self.language, + debug_task.expected_output, + ) task_list.extend([analyze_task, debug_task, fix_task]) crew = Crew( @@ -59,9 +79,12 @@ def kickoff(self, inputs=None): self.code_snippet = inputs.get("code_snippet", self.code_snippet) self.language = inputs.get("language", self.language) self.error_log = inputs.get("error_log", self.error_log) - self.code_requirements = inputs.get("code_requirements", self.code_requirements) + self.code_requirements = inputs.get( + "code_requirements", self.code_requirements + ) return self.run() + def main(): """Main function to demonstrate Code Analyzer with CrewAI adapter.""" load_dotenv() @@ -112,9 +135,11 @@ def main(): try: while True: import time + time.sleep(1) except KeyboardInterrupt: print("\nExiting...") + if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/Crewai-agents/code_analyzer/tasks/code_tasks.py b/Crewai-agents/code_analyzer/tasks/code_tasks.py index 8931a72b..d66ee1f9 100644 --- a/Crewai-agents/code_analyzer/tasks/code_tasks.py +++ b/Crewai-agents/code_analyzer/tasks/code_tasks.py @@ -1,6 +1,7 @@ from textwrap import dedent from crewai import Task + class CodeTasks: def analyze_task(self, agent, code_snippet, language): return Task( @@ -103,4 +104,4 @@ def write_task(self, agent, code_requirements, language): ) def __tip_section(self): - return "Code Analysis " \ No newline at end of file + return "Code Analysis " diff --git a/Crewai-agents/trip_planner/main.py b/Crewai-agents/trip_planner/main.py index 4cc91a34..9ec9c15a 100644 --- a/Crewai-agents/trip_planner/main.py +++ b/Crewai-agents/trip_planner/main.py @@ -31,8 +31,12 @@ def run(self): self.interests, self.date_range, ) - gather_task = tasks.gather_task(local_expert_agent, self.origin, self.interests, self.date_range) - plan_task = tasks.plan_task(travel_concierge_agent, self.origin, self.interests, self.date_range) + gather_task = tasks.gather_task( + local_expert_agent, self.origin, self.interests, self.date_range + ) + plan_task = tasks.plan_task( + travel_concierge_agent, self.origin, self.interests, self.date_range + ) crew = Crew( agents=[city_selector_agent, local_expert_agent, travel_concierge_agent], diff --git a/Crewai-agents/trip_planner/main_uagents.py b/Crewai-agents/trip_planner/main_uagents.py index 68d60fc7..ffb4b413 100644 --- a/Crewai-agents/trip_planner/main_uagents.py +++ b/Crewai-agents/trip_planner/main_uagents.py @@ -33,8 +33,12 @@ def run(self): self.interests, self.date_range, ) - gather_task = tasks.gather_task(local_expert_agent, self.origin, self.interests, self.date_range) - plan_task = tasks.plan_task(travel_concierge_agent, self.origin, self.interests, self.date_range) + gather_task = tasks.gather_task( + local_expert_agent, self.origin, self.interests, self.date_range + ) + plan_task = tasks.plan_task( + travel_concierge_agent, self.origin, self.interests, self.date_range + ) crew = Crew( agents=[city_selector_agent, local_expert_agent, travel_concierge_agent], @@ -102,7 +106,7 @@ def main(): "mailbox": True, "query_params": query_params, "example_query": "Plan a trip from New York to Paris in June, I'm interested in art and history other than museums.", - "ai_agent_address": "agent1q0h70caed8ax769shpemapzkyk65uscw4xwk6dc4t3emvp5jdcvqs9xs32y" + "ai_agent_address": "agent1q0h70caed8ax769shpemapzkyk65uscw4xwk6dc4t3emvp5jdcvqs9xs32y", } ) diff --git a/Crewai-agents/trip_planner/tools/browser_tools.py b/Crewai-agents/trip_planner/tools/browser_tools.py index e52f6746..1d057a10 100644 --- a/Crewai-agents/trip_planner/tools/browser_tools.py +++ b/Crewai-agents/trip_planner/tools/browser_tools.py @@ -22,8 +22,12 @@ def scrape_and_summarize_website(website): for _chunk in content: agent = Agent( role="Principal Researcher", - goal=("Do amazing researches and summaries based on the content you are working with"), - backstory=("You're a Principal Researcher at a big company and you need to do a research about a given topic."), + goal=( + "Do amazing researches and summaries based on the content you are working with" + ), + backstory=( + "You're a Principal Researcher at a big company and you need to do a research about a given topic." + ), allow_delegation=False, ) task = Task( diff --git a/Crewai-agents/trip_planner/tools/search_tools.py b/Crewai-agents/trip_planner/tools/search_tools.py index 6d4dad0e..82df28cb 100644 --- a/Crewai-agents/trip_planner/tools/search_tools.py +++ b/Crewai-agents/trip_planner/tools/search_tools.py @@ -3,6 +3,7 @@ import requests from langchain.tools import tool + class SearchTools: @tool("Search the internet") def search_internet(query): @@ -77,7 +78,9 @@ def search_cab_services(self, location): } response = requests.request("POST", url, headers=headers, data=payload) if "organic" not in response.json(): - return "Sorry, I couldn't find cab services. Please check your Serper API key." + return ( + "Sorry, I couldn't find cab services. Please check your Serper API key." + ) results = response.json()["organic"] string = [] for result in results[:3]: @@ -186,6 +189,3 @@ def fetch_weather(self, location): ) except KeyError: return f"Error processing weather data for {location}." - - - \ No newline at end of file diff --git a/Crewai-agents/trip_planner/trip_tasks.py b/Crewai-agents/trip_planner/trip_tasks.py index d76e39ae..83704832 100644 --- a/Crewai-agents/trip_planner/trip_tasks.py +++ b/Crewai-agents/trip_planner/trip_tasks.py @@ -26,7 +26,9 @@ def identify_task(self, agent, origin, cities, interests, range): Traveler Interests: {interests} """), agent=agent, - expected_output=("Detailed report on the chosen city including flight costs, weather forecast, Helpfull Link , News and Photos and attractions"), + expected_output=( + "Detailed report on the chosen city including flight costs, weather forecast, Helpfull Link , News and Photos and attractions" + ), ) def gather_task(self, agent, origin, interests, range): @@ -54,7 +56,9 @@ def gather_task(self, agent, origin, interests, range): Traveler Interests: {interests} """), agent=agent, - expected_output=("Comprehensive city guide including hidden gems, cultural hotspots, and practical travel tips and photos "), + expected_output=( + "Comprehensive city guide including hidden gems, cultural hotspots, and practical travel tips and photos " + ), ) def plan_task(self, agent, origin, interests, range): @@ -84,7 +88,9 @@ def plan_task(self, agent, origin, interests, range): Traveler Interests: {interests} """), agent=agent, - expected_output=("Complete travel plan with daily schedule, weather conditions, packing suggestions, and budget breakdown"), + expected_output=( + "Complete travel plan with daily schedule, weather conditions, packing suggestions, and budget breakdown" + ), ) def __tip_section(self): diff --git a/Rag-agent/ango/agent.py b/Rag-agent/ango/agent.py index 0d3f29f7..b3ea2c2f 100644 --- a/Rag-agent/ango/agent.py +++ b/Rag-agent/ango/agent.py @@ -8,14 +8,14 @@ load_dotenv() # Import Agno components for RAG functionality -from agno.agent import Agent as AgnoAgent, AgentKnowledge -from agno.embedder.openai import OpenAIEmbedder -from agno.models.openai import OpenAIChat -from agno.vectordb.pgvector import PgVector, SearchType +from agno.agent import Agent as AgnoAgent, AgentKnowledge # noqa: E402 +from agno.embedder.openai import OpenAIEmbedder # noqa: E402 +from agno.models.openai import OpenAIChat # noqa: E402 +from agno.vectordb.pgvector import PgVector, SearchType # noqa: E402 # Import uAgent components for chat protocol -from uagents import Context, Protocol, Agent -from uagents_core.contrib.protocols.chat import ( +from uagents import Context, Protocol, Agent # noqa: E402 +from uagents_core.contrib.protocols.chat import ( # noqa: E402 ChatAcknowledgement, ChatMessage, TextContent, @@ -26,9 +26,10 @@ logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) + class RAGChatAgent: """Integrated RAG Chat Agent combining Agno RAG with uAgent chat protocol""" - + def __init__(self): # Initialize the knowledge base with PgVector self.db_url = "postgresql+psycopg://ai:ai@localhost:5532/ai" @@ -38,44 +39,44 @@ def __init__(self): db_url=self.db_url, search_type=SearchType.hybrid, embedder=OpenAIEmbedder( - id="text-embedding-3-small", - api_key=os.getenv("OPENAI_API_KEY") + id="text-embedding-3-small", api_key=os.getenv("OPENAI_API_KEY") ), ), ) - + # Initialize the knowledge base first try: self.knowledge.load() logger.info("Knowledge base initialized successfully") except Exception as init_error: logger.warning(f"Could not initialize knowledge base: {init_error}") - + # Add PDF content to knowledge base try: from agno.document.reader.pdf_reader import PDFReader - from agno.document.base import Document import uuid - + pdf_reader = PDFReader() documents = pdf_reader.read("ai.pdf") - + # Ensure each document has a proper ID and fix serialization issue for i, document in enumerate(documents): # Always set a unique ID - document.id = f"ai_doc_{i+1}_{uuid.uuid4().hex[:8]}" - + document.id = f"ai_doc_{i + 1}_{uuid.uuid4().hex[:8]}" + # Set document name - document.name = f"ai_document_{i+1}" - + document.name = f"ai_document_{i + 1}" + # Ensure the document has all required fields - document.meta_data = {'source': 'ai.pdf', 'page': i+1} - + document.meta_data = {"source": "ai.pdf", "page": i + 1} + # Load documents using load_documents method try: self.knowledge.load_documents(documents, upsert=True) loaded_count = len(documents) - logger.info(f"Successfully loaded {loaded_count} documents from ai.pdf to knowledge base") + logger.info( + f"Successfully loaded {loaded_count} documents from ai.pdf to knowledge base" + ) except Exception as load_error: logger.error(f"Failed to load documents: {load_error}") # Try loading documents one by one as fallback @@ -85,31 +86,33 @@ def __init__(self): self.knowledge.load_document(document, upsert=True) loaded_count += 1 except Exception as doc_error: - logger.error(f"Failed to load document {document.id}: {doc_error}") - + logger.error( + f"Failed to load document {document.id}: {doc_error}" + ) + # Verify documents are in the knowledge base try: test_results = self.knowledge.search("AI", num_documents=1) - logger.info(f"Knowledge base verification: Found {len(test_results)} documents for test query") + logger.info( + f"Knowledge base verification: Found {len(test_results)} documents for test query" + ) except Exception as verify_error: logger.warning(f"Could not verify knowledge base: {verify_error}") - + except Exception as e: logger.error(f"Failed to add PDF to knowledge base: {e}") import traceback + traceback.print_exc() - + # Initialize Agno RAG agent self.rag_agent = AgnoAgent( - model=OpenAIChat( - id="gpt-4o-mini", - api_key=os.getenv("OPENAI_API_KEY") - ), + model=OpenAIChat(id="gpt-4o-mini", api_key=os.getenv("OPENAI_API_KEY")), knowledge=self.knowledge, search_knowledge=True, markdown=True, ) - + # Initialize uAgent self.uagent = Agent( name="RAG-Chat-Agent", @@ -117,14 +120,14 @@ def __init__(self): port=8001, mailbox=True, ) - + # Set up chat protocol self.setup_protocol() - + def setup_protocol(self): """Set up the chat protocol for the uAgent""" protocol = Protocol(spec=chat_protocol_spec) - + @protocol.on_message(ChatMessage) async def handle_message(ctx: Context, sender: str, msg: ChatMessage): """Handle incoming chat messages and generate RAG-powered responses""" @@ -133,90 +136,107 @@ async def handle_message(ctx: Context, sender: str, msg: ChatMessage): await ctx.send( sender, ChatAcknowledgement( - timestamp=datetime.now(), - acknowledged_msg_id=msg.msg_id + timestamp=datetime.now(), acknowledged_msg_id=msg.msg_id ), ) - + # Extract text from message user_text = "" for item in msg.content: if isinstance(item, TextContent): user_text += item.text - + logger.info(f"Received message from {sender}: {user_text}") - + # Generate response using RAG agent response = await self.generate_rag_response(user_text) - + # Send response back - await ctx.send(sender, ChatMessage( - timestamp=datetime.now(), - msg_id=uuid4(), - content=[ - TextContent(type="text", text=response), - ] - )) - + await ctx.send( + sender, + ChatMessage( + timestamp=datetime.now(), + msg_id=uuid4(), + content=[ + TextContent(type="text", text=response), + ], + ), + ) + logger.info(f"Sent response to {sender}") - + except Exception as e: logger.error(f"Error handling message: {e}") # Send error response - await ctx.send(sender, ChatMessage( - timestamp=datetime.now(), - msg_id=uuid4(), - content=[ - TextContent(type="text", text=f"Sorry, I encountered an error: {str(e)}"), - ] - )) - + await ctx.send( + sender, + ChatMessage( + timestamp=datetime.now(), + msg_id=uuid4(), + content=[ + TextContent( + type="text", + text=f"Sorry, I encountered an error: {str(e)}", + ), + ], + ), + ) + @protocol.on_message(ChatAcknowledgement) async def handle_ack(ctx: Context, sender: str, msg: ChatAcknowledgement): """Handle chat acknowledgements""" logger.info(f"Received acknowledgement from {sender}") - + # Include protocol in uAgent self.uagent.include(protocol, publish_manifest=True) - + async def generate_rag_response(self, query: str) -> str: """Generate a response using the RAG agent""" try: # First, check if we have documents in the knowledge base try: search_results = self.knowledge.search(query, num_documents=3) - logger.info(f"RAG Search found {len(search_results)} relevant documents for query: '{query}'") - + logger.info( + f"RAG Search found {len(search_results)} relevant documents for query: '{query}'" + ) + if len(search_results) == 0: logger.warning("No documents found in knowledge base for RAG query") return "I don't have any relevant information from the document to answer your question. Please make sure the PDF has been properly loaded into my knowledge base." - + except Exception as search_error: logger.error(f"Error searching knowledge base: {search_error}") return "I encountered an error while searching my knowledge base. Please try again." - + # Use the Agno agent to generate a response response = self.rag_agent.run(query) - response_content = response.content if hasattr(response, 'content') else str(response) - + response_content = ( + response.content if hasattr(response, "content") else str(response) + ) + # Add a note if the response seems generic - if not any(word in response_content.lower() for word in ['document', 'pdf', 'text', 'according to', 'based on']): + if not any( + word in response_content.lower() + for word in ["document", "pdf", "text", "according to", "based on"] + ): response_content += "\n\n[Note: This response is based on my knowledge of the loaded PDF document.]" - + return response_content - + except Exception as e: logger.error(f"Error generating RAG response: {e}") import traceback + traceback.print_exc() return f"I'm sorry, I couldn't process your request. Error: {str(e)}" - + def run(self): """Start the uAgent""" logger.info("Starting RAG Chat Agent...") logger.info(f"Agent address: {self.uagent.address}") self.uagent.run() + # Create and run the integrated agent if __name__ == "__main__": try: @@ -225,4 +245,4 @@ def run(self): except KeyboardInterrupt: logger.info("Agent stopped by user") except Exception as e: - logger.error(f"Failed to start agent: {e}") \ No newline at end of file + logger.error(f"Failed to start agent: {e}") diff --git a/a2a-cart-store/__init__.py b/a2a-cart-store/__init__.py index 139597f9..8b137891 100644 --- a/a2a-cart-store/__init__.py +++ b/a2a-cart-store/__init__.py @@ -1,2 +1 @@ - diff --git a/a2a-cart-store/av_adapter.py b/a2a-cart-store/av_adapter.py index 639c05d5..eb2ad596 100644 --- a/a2a-cart-store/av_adapter.py +++ b/a2a-cart-store/av_adapter.py @@ -5,6 +5,7 @@ # Load env if available try: from dotenv import load_dotenv + # Load from examples root if present, then local folder as fallback ROOT = Path(__file__).resolve().parents[1] load_dotenv(ROOT / ".env", override=True) @@ -37,5 +38,3 @@ def main(): if __name__ == "__main__": main() - - diff --git a/a2a-cart-store/skyfire_payment.py b/a2a-cart-store/skyfire_payment.py index ddfdc628..4ae6a07b 100644 --- a/a2a-cart-store/skyfire_payment.py +++ b/a2a-cart-store/skyfire_payment.py @@ -140,5 +140,3 @@ def detect_skyfire_token(payment_response: PaymentResponse) -> Optional[str]: def is_skyfire_payment(payment_response: PaymentResponse) -> bool: return (payment_response.method_name or "").lower() == "skyfire" - - diff --git a/a2a-cart-store/store_executor.py b/a2a-cart-store/store_executor.py index 0a9e68f4..76d3c7aa 100644 --- a/a2a-cart-store/store_executor.py +++ b/a2a-cart-store/store_executor.py @@ -31,6 +31,7 @@ PAYMENT_MANDATE_KEY, PAYMENT_SUCCESS_KEY, ) + try: from .skyfire_payment import ( is_skyfire_payment, @@ -125,14 +126,22 @@ async def execute(self, context: RequestContext, event_queue: EventQueue) -> Non Message( message_id=uuid.uuid4().hex, role=Role.agent, - parts=[Part(root=DataPart(data={PAYMENT_SUCCESS_KEY: success.dict()}))], + parts=[ + Part( + root=DataPart( + data={PAYMENT_SUCCESS_KEY: success.dict()} + ) + ) + ], context_id=context.context_id, task_id=context.task_id, ) ) # Clear cart and deliver cart = self._carts.pop("global", {}) - summary = ", ".join(f"{sku} x{qty}" for sku, qty in cart.items()) or "(empty)" + summary = ( + ", ".join(f"{sku} x{qty}" for sku, qty in cart.items()) or "(empty)" + ) await event_queue.enqueue_event( new_agent_text_message( f"🧾 Payment received (tx={tx_id}).\n📦 Your items: {summary}.\n✅ Delivered." @@ -152,7 +161,9 @@ async def execute(self, context: RequestContext, event_queue: EventQueue) -> Non return if lowered in ("list", "catalog"): - await event_queue.enqueue_event(new_agent_text_message(self._catalog_text())) + await event_queue.enqueue_event( + new_agent_text_message(self._catalog_text()) + ) return if lowered.startswith("add "): @@ -190,11 +201,15 @@ async def execute(self, context: RequestContext, event_queue: EventQueue) -> Non async def cancel(self, context: RequestContext, event_queue: EventQueue) -> None: return - async def _cmd_add(self, context: RequestContext, event_queue: EventQueue, lowered: str) -> None: + async def _cmd_add( + self, context: RequestContext, event_queue: EventQueue, lowered: str + ) -> None: # Supports: add [qty] [ [qty]] ... body = lowered[len("add ") :].strip() if not body: - await event_queue.enqueue_event(new_agent_text_message("Usage: add [qty] [ [qty]] ...")) + await event_queue.enqueue_event( + new_agent_text_message("Usage: add [qty] [ [qty]] ...") + ) return tokens = body.split() @@ -235,25 +250,35 @@ async def _cmd_add(self, context: RequestContext, event_queue: EventQueue, lower await self._emit_cart_text(context, event_queue) return - async def _cmd_remove(self, context: RequestContext, event_queue: EventQueue, lowered: str) -> None: + async def _cmd_remove( + self, context: RequestContext, event_queue: EventQueue, lowered: str + ) -> None: # remove m = re.match(r"remove\s+(\w+)", lowered) if not m: - await event_queue.enqueue_event(new_agent_text_message("Usage: remove ")) + await event_queue.enqueue_event( + new_agent_text_message("Usage: remove ") + ) return sku = m.group(1) cart = self._get_cart(context.context_id) if sku in cart: cart.pop(sku) - await event_queue.enqueue_event(new_agent_text_message(f"➖ Removed {sku}.")) + await event_queue.enqueue_event( + new_agent_text_message(f"➖ Removed {sku}.") + ) else: await event_queue.enqueue_event(new_agent_text_message("Item not in cart.")) await self._emit_cart_text(context, event_queue) - async def _emit_cart_text(self, context: RequestContext, event_queue: EventQueue) -> None: + async def _emit_cart_text( + self, context: RequestContext, event_queue: EventQueue + ) -> None: cart = self._get_cart(context.context_id) if not cart: - await event_queue.enqueue_event(new_agent_text_message("🧺 Cart is empty. Type 'list' to see items.")) + await event_queue.enqueue_event( + new_agent_text_message("🧺 Cart is empty. Type 'list' to see items.") + ) return lines: List[str] = ["🧺 Cart:"] total = 0.0 @@ -265,10 +290,14 @@ async def _emit_cart_text(self, context: RequestContext, event_queue: EventQueue lines.append(f"Total: {total:.3f} USDC\nType 'checkout' to pay.") await event_queue.enqueue_event(new_agent_text_message("\n".join(lines))) - async def _emit_cart_mandate(self, context: RequestContext, event_queue: EventQueue) -> None: + async def _emit_cart_mandate( + self, context: RequestContext, event_queue: EventQueue + ) -> None: cart = self._get_cart(context.context_id) if not cart: - await event_queue.enqueue_event(new_agent_text_message("🧺 Cart is empty. Add items before checkout.")) + await event_queue.enqueue_event( + new_agent_text_message("🧺 Cart is empty. Add items before checkout.") + ) return display_items: List[PaymentItem] = [] total_value = 0.0 @@ -277,9 +306,15 @@ async def _emit_cart_mandate(self, context: RequestContext, event_queue: EventQu line_total = ci.price * qty total_value += line_total display_items.append( - PaymentItem(label=f"{ci.title} x{qty}", amount=PaymentCurrencyAmount(currency="USDC", value=line_total)) + PaymentItem( + label=f"{ci.title} x{qty}", + amount=PaymentCurrencyAmount(currency="USDC", value=line_total), + ) ) - total_item = PaymentItem(label="Total", amount=PaymentCurrencyAmount(currency="USDC", value=total_value)) + total_item = PaymentItem( + label="Total", + amount=PaymentCurrencyAmount(currency="USDC", value=total_value), + ) cart_id = "cart-" + uuid.uuid4().hex[:10] pr = PaymentRequest( # Showcase Skyfire as the payment method so UI offers Skyfire pay @@ -291,30 +326,39 @@ async def _emit_cart_mandate(self, context: RequestContext, event_queue: EventQu }, ), ], - details=PaymentDetailsInit(id=cart_id, display_items=display_items, total=total_item), + details=PaymentDetailsInit( + id=cart_id, display_items=display_items, total=total_item + ), options=PaymentOptions(request_shipping=False), ) contents = CartContents( id=cart_id, payment_request=pr, user_cart_confirmation_required=True, - cart_expiry=(datetime.now(timezone.utc) + timedelta(minutes=10)).isoformat() + "Z", + cart_expiry=(datetime.now(timezone.utc) + timedelta(minutes=10)).isoformat() + + "Z", merchant_name="Demo Store", ) cart_mandate = CartMandate( - contents=contents, merchant_authorization="demo_auth", cart_hash=compute_cart_hash(contents) + contents=contents, + merchant_authorization="demo_auth", + cart_hash=compute_cart_hash(contents), ) await event_queue.enqueue_event( Message( message_id=uuid.uuid4().hex, role=Role.agent, - parts=[Part(root=DataPart(data={CART_MANDATE_KEY: cart_mandate.dict()}))], + parts=[ + Part(root=DataPart(data={CART_MANDATE_KEY: cart_mandate.dict()})) + ], context_id=context.context_id, task_id=context.task_id, ) ) await asyncio.sleep(0.1) - await event_queue.enqueue_event(new_agent_text_message("🧾 Please complete payment to confirm your order.")) + await event_queue.enqueue_event( + new_agent_text_message("🧾 Please complete payment to confirm your order.") + ) def _help_text(self) -> str: return ( @@ -334,5 +378,3 @@ def _catalog_text(self) -> str: lines.append(f"- {ci.title} ({ci.sku}) = {ci.price:.3f} USDC") lines.append("Type 'add [qty]' to add items.") return "\n".join(lines) - - diff --git a/a2a-uAgents-Integration/a2a-Inbound-Communication/Multiagent-Planner/airbnb_agent/airbnb_agent_simple.py b/a2a-uAgents-Integration/a2a-Inbound-Communication/Multiagent-Planner/airbnb_agent/airbnb_agent_simple.py index cb4873a9..0ccdbb7c 100644 --- a/a2a-uAgents-Integration/a2a-Inbound-Communication/Multiagent-Planner/airbnb_agent/airbnb_agent_simple.py +++ b/a2a-uAgents-Integration/a2a-Inbound-Communication/Multiagent-Planner/airbnb_agent/airbnb_agent_simple.py @@ -11,30 +11,38 @@ # Load environment variables from .env file load_dotenv() + def main(): """Start A2A adapter for Airbnb Agent - Simple CLI-like approach.""" - + print("🏠 Starting Airbnb Agent A2A Adapter (Simple)") print("=" * 50) - + # Create adapter tool adapter = A2ARegisterTool() - + # Airbnb Agent configuration - same as CLI config = { "agent_address": "agent1qv4zyd9sta4f5ksyhjp900k8kenp9vczlwqvr00xmmqmj2yetdt4se9ypat", "name": "Airbnb Search Agent", "description": "AI-powered vacation rental search and property details assistant", - "skill_tags": ["airbnb", "vacation", "rental", "travel", "accommodation", "booking"], + "skill_tags": [ + "airbnb", + "vacation", + "rental", + "travel", + "accommodation", + "booking", + ], "port": 9001, - "host": "localhost" + "host": "localhost", } - + print(f"🔧 Agent Address: {config['agent_address']}") print(f"🏷️ Agent Name: {config['name']}") print(f"🌐 Port: {config['port']}") print("") - + # Start adapter - this blocks just like CLI does try: result = adapter.invoke(config) @@ -44,33 +52,36 @@ def main(): print("") print("🧪 Test with cURL:") print("curl -X POST http://localhost:9001 \\") - print(" -H \"Content-Type: application/json\" \\") + print(' -H "Content-Type: application/json" \\') print(" -d '{") - print(" \"jsonrpc\": \"2.0\",") - print(" \"id\": \"airbnb-test-1\",") - print(" \"method\": \"message/send\",") - print(" \"params\": {") - print(" \"message\": {") - print(" \"role\": \"user\",") - print(" \"parts\": [{\"kind\": \"text\", \"text\": \"Find me vacation rentals in San Francisco for 2 guests\"}],") - print(" \"messageId\": \"msg-1\"") + print(' "jsonrpc": "2.0",') + print(' "id": "airbnb-test-1",') + print(' "method": "message/send",') + print(' "params": {') + print(' "message": {') + print(' "role": "user",') + print( + ' "parts": [{"kind": "text", "text": "Find me vacation rentals in San Francisco for 2 guests"}],' + ) + print(' "messageId": "msg-1"') print(" },") - print(" \"contextId\": \"user-booking-session\"") + print(' "contextId": "user-booking-session"') print(" }") print(" }'") print("") print("🏠 Ready for vacation rental queries!") print("Press Ctrl+C to stop...") - + # This blocks just like the CLI does - uvicorn handles Ctrl+C naturally - + else: print(f"❌ Failed to start adapter: {result}") - + except KeyboardInterrupt: print("\n👋 Shutting down...") except Exception as e: print(f"❌ Error: {e}") + if __name__ == "__main__": main() diff --git a/a2a-uAgents-Integration/a2a-Inbound-Communication/Multiagent-Planner/finance_agent/finance_agent_simple.py b/a2a-uAgents-Integration/a2a-Inbound-Communication/Multiagent-Planner/finance_agent/finance_agent_simple.py index 9ecec5fa..dad92326 100644 --- a/a2a-uAgents-Integration/a2a-Inbound-Communication/Multiagent-Planner/finance_agent/finance_agent_simple.py +++ b/a2a-uAgents-Integration/a2a-Inbound-Communication/Multiagent-Planner/finance_agent/finance_agent_simple.py @@ -12,31 +12,37 @@ # Load environment variables from .env file load_dotenv() + def main(): """Start A2A adapter for Perplexity Agent - Simple CLI-like approach.""" - + print("🔍 Starting Perplexity Agent A2A Adapter (Simple)") print("=" * 50) - + # Create adapter tool adapter = A2ARegisterTool() - + # Perplexity Agent configuration - same as CLI config = { "agent_address": "agent1qdv2qgxucvqatam6nv28qp202f3pw8xqpfm8man6zyegztuzd2t6yem9evl", "name": "Finance Q&A Agent", "description": "AI-powered financial advisor and Q&A assistant for investment, budgeting, and financial planning guidance", - "skill_tags": ["finance", "investment", "budgeting", "financial_planning", "assistance"], + "skill_tags": [ + "finance", + "investment", + "budgeting", + "financial_planning", + "assistance", + ], "port": 9009, - "host": "localhost" + "host": "localhost", } - + print(f"🔧 Agent Address: {config['agent_address']}") print(f"🏷️ Agent Name: {config['name']}") print(f"🌐 Port: {config['port']}") print("") - - + # Start adapter - this blocks just like CLI does try: result = adapter.invoke(config) @@ -44,16 +50,17 @@ def main(): print("✅ A2A Adapter Started Successfully!") print(f"🌐 Endpoint: http://localhost:{config['port']}") print("") - + # This blocks just like the CLI does - uvicorn handles Ctrl+C naturally - + else: print(f"❌ Failed to start adapter: {result}") - + except KeyboardInterrupt: print("\n👋 Shutting down...") except Exception as e: print(f"❌ Error: {e}") + if __name__ == "__main__": main() diff --git a/a2a-uAgents-Integration/a2a-Inbound-Communication/Multiagent-Planner/host_agent/__main__.py b/a2a-uAgents-Integration/a2a-Inbound-Communication/Multiagent-Planner/host_agent/__main__.py index b61f1302..480d2703 100644 --- a/a2a-uAgents-Integration/a2a-Inbound-Communication/Multiagent-Planner/host_agent/__main__.py +++ b/a2a-uAgents-Integration/a2a-Inbound-Communication/Multiagent-Planner/host_agent/__main__.py @@ -1,4 +1,3 @@ - import asyncio import traceback # Import the traceback module @@ -16,9 +15,9 @@ from google.genai import types -APP_NAME = 'routing_app' -USER_ID = 'default_user' -SESSION_ID = 'default_session' +APP_NAME = "routing_app" +USER_ID = "default_user" +SESSION_ID = "default_session" SESSION_SERVICE = InMemorySessionService() ROUTING_AGENT_RUNNER = Runner( @@ -37,71 +36,63 @@ async def get_response_from_agent( event_iterator: AsyncIterator[Event] = ROUTING_AGENT_RUNNER.run_async( user_id=USER_ID, session_id=SESSION_ID, - new_message=types.Content( - role='user', parts=[types.Part(text=message)] - ), + new_message=types.Content(role="user", parts=[types.Part(text=message)]), ) async for event in event_iterator: if event.content and event.content.parts: for part in event.content.parts: if part.function_call: - formatted_call = f'```python\n{pformat(part.function_call.model_dump(exclude_none=True), indent=2, width=80)}\n```' + formatted_call = f"```python\n{pformat(part.function_call.model_dump(exclude_none=True), indent=2, width=80)}\n```" yield gr.ChatMessage( - role='assistant', - content=f'🛠️ **Tool Call: {part.function_call.name}**\n{formatted_call}', + role="assistant", + content=f"🛠️ **Tool Call: {part.function_call.name}**\n{formatted_call}", ) elif part.function_response: response_content = part.function_response.response if ( isinstance(response_content, dict) - and 'response' in response_content + and "response" in response_content ): - formatted_response_data = response_content[ - 'response' - ] + formatted_response_data = response_content["response"] else: formatted_response_data = response_content - formatted_response = f'```json\n{pformat(formatted_response_data, indent=2, width=80)}\n```' + formatted_response = f"```json\n{pformat(formatted_response_data, indent=2, width=80)}\n```" yield gr.ChatMessage( - role='assistant', - content=f'⚡ **Tool Response from {part.function_response.name}**\n{formatted_response}', + role="assistant", + content=f"⚡ **Tool Response from {part.function_response.name}**\n{formatted_response}", ) if event.is_final_response(): - final_response_text = '' + final_response_text = "" if event.content and event.content.parts: - final_response_text = ''.join( + final_response_text = "".join( [p.text for p in event.content.parts if p.text] ) elif event.actions and event.actions.escalate: - final_response_text = f'Agent escalated: {event.error_message or "No specific message."}' + final_response_text = f"Agent escalated: {event.error_message or 'No specific message.'}" if final_response_text: - yield gr.ChatMessage( - role='assistant', content=final_response_text - ) + yield gr.ChatMessage(role="assistant", content=final_response_text) break except Exception as e: - print(f'Error in get_response_from_agent (Type: {type(e)}): {e}') + print(f"Error in get_response_from_agent (Type: {type(e)}): {e}") traceback.print_exc() # This will print the full traceback yield gr.ChatMessage( - role='assistant', - content='An error occurred while processing your request. Please check the server logs for details.', + role="assistant", + content="An error occurred while processing your request. Please check the server logs for details.", ) async def main(): """Main gradio app.""" - print('Creating ADK session...') + print("Creating ADK session...") await SESSION_SERVICE.create_session( app_name=APP_NAME, user_id=USER_ID, session_id=SESSION_ID ) - print('ADK session created successfully.') + print("ADK session created successfully.") - with gr.Blocks( - theme=gr.themes.Ocean(), title='A2A Host Agent with Logo' - ) as demo: + with gr.Blocks(theme=gr.themes.Ocean(), title="A2A Host Agent with Logo") as demo: gr.Image( - 'static/a2a.png', + "static/a2a.png", width=100, height=100, scale=0, @@ -112,17 +103,17 @@ async def main(): ) gr.ChatInterface( get_response_from_agent, - title='A2A Host Agent', # Title can be handled by Markdown above + title="A2A Host Agent", # Title can be handled by Markdown above description="Ask me about weather, Airbnb accommodations, news and developments, or financial guidance!", ) - print('Launching Gradio interface...') + print("Launching Gradio interface...") demo.queue().launch( - server_name='0.0.0.0', + server_name="0.0.0.0", server_port=8083, ) - print('Gradio application has been shut down.') + print("Gradio application has been shut down.") -if __name__ == '__main__': - asyncio.run(main()) \ No newline at end of file +if __name__ == "__main__": + asyncio.run(main()) diff --git a/a2a-uAgents-Integration/a2a-Inbound-Communication/Multiagent-Planner/host_agent/remote_agent_connection.py b/a2a-uAgents-Integration/a2a-Inbound-Communication/Multiagent-Planner/host_agent/remote_agent_connection.py index ed54aa25..069cfe2b 100644 --- a/a2a-uAgents-Integration/a2a-Inbound-Communication/Multiagent-Planner/host_agent/remote_agent_connection.py +++ b/a2a-uAgents-Integration/a2a-Inbound-Communication/Multiagent-Planner/host_agent/remote_agent_connection.py @@ -1,4 +1,3 @@ - from collections.abc import Callable import httpx @@ -25,12 +24,10 @@ class RemoteAgentConnections: """A class to hold the connections to the remote agents.""" def __init__(self, agent_card: AgentCard, agent_url: str): - print(f'agent_card: {agent_card}') - print(f'agent_url: {agent_url}') + print(f"agent_card: {agent_card}") + print(f"agent_url: {agent_url}") self._httpx_client = httpx.AsyncClient(timeout=30) - self.agent_client = A2AClient( - self._httpx_client, agent_card, url=agent_url - ) + self.agent_client = A2AClient(self._httpx_client, agent_card, url=agent_url) self.card = agent_card def get_agent(self) -> AgentCard: @@ -39,4 +36,4 @@ def get_agent(self) -> AgentCard: async def send_message( self, message_request: SendMessageRequest ) -> SendMessageResponse: - return await self.agent_client.send_message(message_request) \ No newline at end of file + return await self.agent_client.send_message(message_request) diff --git a/a2a-uAgents-Integration/a2a-Inbound-Communication/Multiagent-Planner/host_agent/routing_agent.py b/a2a-uAgents-Integration/a2a-Inbound-Communication/Multiagent-Planner/host_agent/routing_agent.py index 1727ae37..29b21cf2 100644 --- a/a2a-uAgents-Integration/a2a-Inbound-Communication/Multiagent-Planner/host_agent/routing_agent.py +++ b/a2a-uAgents-Integration/a2a-Inbound-Communication/Multiagent-Planner/host_agent/routing_agent.py @@ -35,10 +35,10 @@ def convert_part(part: Part, tool_context: ToolContext): """Convert a part to text. Only text parts are supported.""" - if part.type == 'text': + if part.type == "text": return part.text - return f'Unknown type: {part.type}' + return f"Unknown type: {part.type}" def convert_parts(parts: list[Part], tool_context: ToolContext): @@ -54,18 +54,18 @@ def create_send_message_payload( ) -> dict[str, Any]: """Helper function to create the payload for sending a task.""" payload: dict[str, Any] = { - 'message': { - 'role': 'user', - 'parts': [{'type': 'text', 'text': text}], - 'messageId': uuid.uuid4().hex, + "message": { + "role": "user", + "parts": [{"type": "text", "text": text}], + "messageId": uuid.uuid4().hex, }, } if task_id: - payload['message']['taskId'] = task_id + payload["message"]["taskId"] = task_id if context_id: - payload['message']['contextId'] = context_id + payload["message"]["contextId"] = context_id return payload @@ -83,18 +83,14 @@ def __init__( self.task_callback = task_callback self.remote_agent_connections: dict[str, RemoteAgentConnections] = {} self.cards: dict[str, AgentCard] = {} - self.agents: str = '' + self.agents: str = "" - async def _async_init_components( - self, remote_agent_addresses: list[str] - ) -> None: + async def _async_init_components(self, remote_agent_addresses: list[str]) -> None: """Asynchronous part of initialization.""" # Use a single httpx.AsyncClient for all card resolutions for efficiency async with httpx.AsyncClient(timeout=30) as client: for address in remote_agent_addresses: - card_resolver = A2ACardResolver( - client, address - ) # Constructor is sync + card_resolver = A2ACardResolver(client, address) # Constructor is sync try: card = ( await card_resolver.get_agent_card() @@ -106,26 +102,22 @@ async def _async_init_components( self.remote_agent_connections[card.name] = remote_connection self.cards[card.name] = card except httpx.ConnectError as e: - print( - f'ERROR: Failed to get agent card from {address}: {e}' - ) + print(f"ERROR: Failed to get agent card from {address}: {e}") except Exception as e: # Catch other potential errors - print( - f'ERROR: Failed to initialize connection for {address}: {e}' - ) + print(f"ERROR: Failed to initialize connection for {address}: {e}") # Populate self.agents using the logic from original __init__ (via list_remote_agents) agent_info = [] for agent_detail_dict in self.list_remote_agents(): agent_info.append(json.dumps(agent_detail_dict)) - self.agents = '\n'.join(agent_info) + self.agents = "\n".join(agent_info) @classmethod async def create( cls, remote_agent_addresses: list[str], task_callback: TaskUpdateCallback | None = None, - ) -> 'RoutingAgent': + ) -> "RoutingAgent": """Create and asynchronously initialize an instance of the RoutingAgent.""" instance = cls(task_callback) await instance._async_init_components(remote_agent_addresses) @@ -133,11 +125,11 @@ async def create( def create_agent(self) -> Agent: """Create an instance of the RoutingAgent.""" - model_id = 'gemini-2.5-flash-preview-04-17' - print(f'Using hardcoded model: {model_id}') + model_id = "gemini-2.5-flash-preview-04-17" + print(f"Using hardcoded model: {model_id}") return Agent( model=model_id, - name='Routing_agent', + name="Routing_agent", instruction=self.root_instruction, before_model_callback=self.before_model_callback, description=( @@ -170,28 +162,26 @@ def root_instruction(self, context: ReadonlyContext) -> str: **Agent Roster:** * Available Agents: `{self.agents}` - * Currently Active Seller Agent: `{current_agent['active_agent']}` + * Currently Active Seller Agent: `{current_agent["active_agent"]}` """ def check_active_agent(self, context: ReadonlyContext): state = context.state if ( - 'session_id' in state - and 'session_active' in state - and state['session_active'] - and 'active_agent' in state + "session_id" in state + and "session_active" in state + and state["session_active"] + and "active_agent" in state ): - return {'active_agent': f'{state["active_agent"]}'} - return {'active_agent': 'None'} + return {"active_agent": f"{state['active_agent']}"} + return {"active_agent": "None"} - def before_model_callback( - self, callback_context: CallbackContext, llm_request - ): + def before_model_callback(self, callback_context: CallbackContext, llm_request): state = callback_context.state - if 'session_active' not in state or not state['session_active']: - if 'session_id' not in state: - state['session_id'] = str(uuid.uuid4()) - state['session_active'] = True + if "session_active" not in state or not state["session_active"]: + if "session_id" not in state: + state["session_id"] = str(uuid.uuid4()) + state["session_active"] = True def list_remote_agents(self): """List the available remote agents you can use to delegate the task.""" @@ -200,16 +190,14 @@ def list_remote_agents(self): remote_agent_info = [] for card in self.cards.values(): - print(f'Found agent card: {card.model_dump(exclude_none=True)}') - print('=' * 100) + print(f"Found agent card: {card.model_dump(exclude_none=True)}") + print("=" * 100) remote_agent_info.append( - {'name': card.name, 'description': card.description} + {"name": card.name, "description": card.description} ) return remote_agent_info - async def send_message( - self, agent_name: str, task: str, tool_context: ToolContext - ): + async def send_message(self, agent_name: str, task: str, tool_context: ToolContext): """Sends a task to remote seller agent. This will send a message to the remote agent named agent_name. @@ -224,44 +212,44 @@ async def send_message( A dictionary of JSON data. """ if agent_name not in self.remote_agent_connections: - raise ValueError(f'Agent {agent_name} not found') + raise ValueError(f"Agent {agent_name} not found") state = tool_context.state - state['active_agent'] = agent_name + state["active_agent"] = agent_name client = self.remote_agent_connections[agent_name] if not client: - raise ValueError(f'Client not available for {agent_name}') - task_id = state['task_id'] if 'task_id' in state else str(uuid.uuid4()) + raise ValueError(f"Client not available for {agent_name}") + task_id = state["task_id"] if "task_id" in state else str(uuid.uuid4()) - if 'context_id' in state: - context_id = state['context_id'] + if "context_id" in state: + context_id = state["context_id"] else: context_id = str(uuid.uuid4()) - message_id = '' + message_id = "" metadata = {} - if 'input_message_metadata' in state: - metadata.update(**state['input_message_metadata']) - if 'message_id' in state['input_message_metadata']: - message_id = state['input_message_metadata']['message_id'] + if "input_message_metadata" in state: + metadata.update(**state["input_message_metadata"]) + if "message_id" in state["input_message_metadata"]: + message_id = state["input_message_metadata"]["message_id"] if not message_id: message_id = str(uuid.uuid4()) payload = { - 'message': { - 'role': 'user', - 'parts': [ - {'type': 'text', 'text': task} + "message": { + "role": "user", + "parts": [ + {"type": "text", "text": task} ], # Use the 'task' argument here - 'messageId': message_id, + "messageId": message_id, }, } if task_id: - payload['message']['taskId'] = task_id + payload["message"]["taskId"] = task_id if context_id: - payload['message']['contextId'] = context_id + payload["message"]["contextId"] = context_id message_request = SendMessageRequest( id=message_id, params=MessageSendParams.model_validate(payload) @@ -270,16 +258,16 @@ async def send_message( message_request=message_request ) print( - 'send_response', + "send_response", send_response.model_dump_json(exclude_none=True, indent=2), ) if not isinstance(send_response.root, SendMessageSuccessResponse): - print('received non-success response. Aborting get task ') + print("received non-success response. Aborting get task ") return None if not isinstance(send_response.root.result, Task): - print('received non-task response. Aborting get task ') + print("received non-task response. Aborting get task ") return None return send_response.root.result @@ -291,10 +279,10 @@ def _get_initialized_routing_agent_sync() -> Agent: async def _async_main() -> Agent: routing_agent_instance = await RoutingAgent.create( remote_agent_addresses=[ - os.getenv('AIR_AGENT_URL', 'http://localhost:9001'), - os.getenv('WEA_AGENT_URL', 'http://localhost:10001'), - os.getenv('PER_AGENT_URL', 'http://localhost:9002'), - os.getenv('FIN_AGENT_URL', 'http://localhost:8002'), + os.getenv("AIR_AGENT_URL", "http://localhost:9001"), + os.getenv("WEA_AGENT_URL", "http://localhost:10001"), + os.getenv("PER_AGENT_URL", "http://localhost:9002"), + os.getenv("FIN_AGENT_URL", "http://localhost:8002"), ] ) return routing_agent_instance.create_agent() @@ -302,13 +290,13 @@ async def _async_main() -> Agent: try: return asyncio.run(_async_main()) except RuntimeError as e: - if 'asyncio.run() cannot be called from a running event loop' in str(e): + if "asyncio.run() cannot be called from a running event loop" in str(e): print( - f'Warning: Could not initialize RoutingAgent with asyncio.run(): {e}. ' - 'This can happen if an event loop is already running (e.g., in Jupyter). ' - 'Consider initializing RoutingAgent within an async function in your application.' + f"Warning: Could not initialize RoutingAgent with asyncio.run(): {e}. " + "This can happen if an event loop is already running (e.g., in Jupyter). " + "Consider initializing RoutingAgent within an async function in your application." ) raise -root_agent = _get_initialized_routing_agent_sync() \ No newline at end of file +root_agent = _get_initialized_routing_agent_sync() diff --git a/a2a-uAgents-Integration/a2a-Inbound-Communication/Multiagent-Planner/perplexity_agent/perplexity_agent_simple.py b/a2a-uAgents-Integration/a2a-Inbound-Communication/Multiagent-Planner/perplexity_agent/perplexity_agent_simple.py index 992078e4..00097721 100644 --- a/a2a-uAgents-Integration/a2a-Inbound-Communication/Multiagent-Planner/perplexity_agent/perplexity_agent_simple.py +++ b/a2a-uAgents-Integration/a2a-Inbound-Communication/Multiagent-Planner/perplexity_agent/perplexity_agent_simple.py @@ -12,15 +12,16 @@ # Load environment variables from .env file load_dotenv() + def main(): """Start A2A adapter for Perplexity Agent - Simple CLI-like approach.""" - + print("🔍 Starting Perplexity Agent A2A Adapter (Simple)") print("=" * 50) - + # Create adapter tool adapter = A2ARegisterTool() - + # Perplexity Agent configuration - same as CLI config = { "agent_address": "agent1qgzd0c60d4c5n37m4pzuclv5p9vwsftmfkznksec3drux8qnhmvuymsmshp", @@ -28,9 +29,9 @@ def main(): "description": "AI-powered web search and research assistant with real-time information access", "skill_tags": ["search", "research", "web", "ai", "information", "news"], "port": 9002, - "host": "localhost" + "host": "localhost", } - + print(f"🔧 Agent Address: {config['agent_address']}") print(f"🏷️ Agent Name: {config['name']}") print(f"🌐 Port: {config['port']}") @@ -42,7 +43,7 @@ def main(): print(" ✅ Cited sources and references") print(" ✅ Current information access") print("") - + # Start adapter - this blocks just like CLI does try: result = adapter.invoke(config) @@ -52,33 +53,36 @@ def main(): print("") print("🧪 Test with cURL:") print("curl -X POST http://localhost:9002 \\") - print(" -H \"Content-Type: application/json\" \\") + print(' -H "Content-Type: application/json" \\') print(" -d '{") - print(" \"jsonrpc\": \"2.0\",") - print(" \"id\": \"perplexity-test-1\",") - print(" \"method\": \"message/send\",") - print(" \"params\": {") - print(" \"message\": {") - print(" \"role\": \"user\",") - print(" \"parts\": [{\"kind\": \"text\", \"text\": \"What are the latest developments in AI agents?\"}],") - print(" \"messageId\": \"msg-1\"") + print(' "jsonrpc": "2.0",') + print(' "id": "perplexity-test-1",') + print(' "method": "message/send",') + print(' "params": {') + print(' "message": {') + print(' "role": "user",') + print( + ' "parts": [{"kind": "text", "text": "What are the latest developments in AI agents?"}],' + ) + print(' "messageId": "msg-1"') print(" },") - print(" \"contextId\": \"user-research-session\"") + print(' "contextId": "user-research-session"') print(" }") print(" }'") print("") print("🔍 Ready for research queries!") print("Press Ctrl+C to stop...") - + # This blocks just like the CLI does - uvicorn handles Ctrl+C naturally - + else: print(f"❌ Failed to start adapter: {result}") - + except KeyboardInterrupt: print("\n👋 Shutting down...") except Exception as e: print(f"❌ Error: {e}") + if __name__ == "__main__": main() diff --git a/a2a-uAgents-Integration/a2a-Inbound-Communication/Multiagent-Planner/weather_agent/__main__.py b/a2a-uAgents-Integration/a2a-Inbound-Communication/Multiagent-Planner/weather_agent/__main__.py index 75cf7586..78833c1d 100644 --- a/a2a-uAgents-Integration/a2a-Inbound-Communication/Multiagent-Planner/weather_agent/__main__.py +++ b/a2a-uAgents-Integration/a2a-Inbound-Communication/Multiagent-Planner/weather_agent/__main__.py @@ -29,36 +29,36 @@ logging.basicConfig() -DEFAULT_HOST = 'localhost' +DEFAULT_HOST = "localhost" DEFAULT_PORT = 10001 def main(host: str = DEFAULT_HOST, port: int = DEFAULT_PORT): # Verify an API key is set. # Not required if using Vertex AI APIs. - if os.getenv('GOOGLE_GENAI_USE_VERTEXAI') != 'TRUE' and not os.getenv( - 'GOOGLE_API_KEY' + if os.getenv("GOOGLE_GENAI_USE_VERTEXAI") != "TRUE" and not os.getenv( + "GOOGLE_API_KEY" ): raise ValueError( - 'GOOGLE_API_KEY environment variable not set and ' - 'GOOGLE_GENAI_USE_VERTEXAI is not TRUE.' + "GOOGLE_API_KEY environment variable not set and " + "GOOGLE_GENAI_USE_VERTEXAI is not TRUE." ) skill = AgentSkill( - id='weather_search', - name='Search weather', - description='Helps with weather in city, or states', - tags=['weather'], - examples=['weather in LA, CA'], + id="weather_search", + name="Search weather", + description="Helps with weather in city, or states", + tags=["weather"], + examples=["weather in LA, CA"], ) agent_card = AgentCard( - name='Weather Agent', - description='Helps with weather', - url=f'http://{host}:{port}/', - version='1.0.0', - defaultInputModes=['text'], - defaultOutputModes=['text'], + name="Weather Agent", + description="Helps with weather", + url=f"http://{host}:{port}/", + version="1.0.0", + defaultInputModes=["text"], + defaultOutputModes=["text"], capabilities=AgentCapabilities(streaming=True), skills=[skill], ) @@ -85,11 +85,11 @@ def main(host: str = DEFAULT_HOST, port: int = DEFAULT_PORT): @click.command() -@click.option('--host', 'host', default=DEFAULT_HOST) -@click.option('--port', 'port', default=DEFAULT_PORT) +@click.option("--host", "host", default=DEFAULT_HOST) +@click.option("--port", "port", default=DEFAULT_PORT) def cli(host: str, port: int): main(host, port) -if __name__ == '__main__': - main() \ No newline at end of file +if __name__ == "__main__": + main() diff --git a/a2a-uAgents-Integration/a2a-Inbound-Communication/Multiagent-Planner/weather_agent/weather_agent.py b/a2a-uAgents-Integration/a2a-Inbound-Communication/Multiagent-Planner/weather_agent/weather_agent.py index 8c7cee78..389deca3 100644 --- a/a2a-uAgents-Integration/a2a-Inbound-Communication/Multiagent-Planner/weather_agent/weather_agent.py +++ b/a2a-uAgents-Integration/a2a-Inbound-Communication/Multiagent-Planner/weather_agent/weather_agent.py @@ -10,19 +10,19 @@ def create_weather_agent() -> LlmAgent: """Constructs the ADK agent.""" # Get the directory of this file to construct absolute path current_dir = os.path.dirname(os.path.abspath(__file__)) - weather_mcp_path = os.path.join(current_dir, 'weather_mcp.py') - + weather_mcp_path = os.path.join(current_dir, "weather_mcp.py") + return LlmAgent( - model='gemini-2.5-flash-preview-04-17', - name='weather_agent', - description='An agent that can help questions about weather', + model="gemini-2.5-flash-preview-04-17", + name="weather_agent", + description="An agent that can help questions about weather", instruction="""You are a specialized weather forecast assistant. Your primary function is to utilize the provided tools to retrieve and relay weather information in response to user queries. You must rely exclusively on these tools for data and refrain from inventing information. Ensure that all responses include the detailed output from the tools used and are formatted in Markdown""", tools=[ MCPToolset( connection_params=StdioServerParameters( - command='python', + command="python", args=[weather_mcp_path], ), ) ], - ) \ No newline at end of file + ) diff --git a/a2a-uAgents-Integration/a2a-Inbound-Communication/Multiagent-Planner/weather_agent/weather_executor.py b/a2a-uAgents-Integration/a2a-Inbound-Communication/Multiagent-Planner/weather_agent/weather_executor.py index 215c0ecf..ac263787 100644 --- a/a2a-uAgents-Integration/a2a-Inbound-Communication/Multiagent-Planner/weather_agent/weather_executor.py +++ b/a2a-uAgents-Integration/a2a-Inbound-Communication/Multiagent-Planner/weather_agent/weather_executor.py @@ -29,7 +29,7 @@ logger.setLevel(logging.DEBUG) # Constants -DEFAULT_USER_ID = 'self' +DEFAULT_USER_ID = "self" class WeatherExecutor(AgentExecutor): @@ -67,30 +67,24 @@ async def _process_request( for part in event.content.parts if (part.text or part.file_data or part.inline_data) ] - logger.debug('Yielding final response: %s', parts) + logger.debug("Yielding final response: %s", parts) await task_updater.add_artifact(parts) - await task_updater.update_status( - TaskState.completed, final=True - ) + await task_updater.update_status(TaskState.completed, final=True) break if not event.get_function_calls(): - logger.debug('Yielding update response') + logger.debug("Yielding update response") await task_updater.update_status( TaskState.working, message=task_updater.new_agent_message( [ convert_genai_part_to_a2a(part) for part in event.content.parts - if ( - part.text - or part.file_data - or part.inline_data - ) + if (part.text or part.file_data or part.inline_data) ], ), ) else: - logger.debug('Skipping event') + logger.debug("Skipping event") finally: # Remove from active sessions when done self._active_sessions.discard(session_id) @@ -109,14 +103,13 @@ async def execute( await self._process_request( types.UserContent( parts=[ - convert_a2a_part_to_genai(part) - for part in context.message.parts + convert_a2a_part_to_genai(part) for part in context.message.parts ], ), context.context_id, updater, ) - logger.debug('[weather] execute exiting') + logger.debug("[weather] execute exiting") async def cancel(self, context: RequestContext, event_queue: EventQueue): """Cancel the execution for the given context. @@ -127,18 +120,18 @@ async def cancel(self, context: RequestContext, event_queue: EventQueue): session_id = context.context_id if session_id in self._active_sessions: logger.info( - f'Cancellation requested for active weather session: {session_id}' + f"Cancellation requested for active weather session: {session_id}" ) # TODO: Implement proper cancellation when ADK supports it self._active_sessions.discard(session_id) else: logger.debug( - f'Cancellation requested for inactive weather session: {session_id}' + f"Cancellation requested for inactive weather session: {session_id}" ) raise ServerError(error=UnsupportedOperationError()) - async def _upsert_session(self, session_id: str) -> 'Session': + async def _upsert_session(self, session_id: str) -> "Session": """Retrieves a session if it exists, otherwise creates a new one. Ensures that async session service methods are properly awaited. @@ -185,8 +178,8 @@ def convert_a2a_part_to_genai(part: Part) -> types.Part: data=part.file.bytes, mime_type=part.file.mime_type ) ) - raise ValueError(f'Unsupported file type: {type(part.file)}') - raise ValueError(f'Unsupported part type: {type(part)}') + raise ValueError(f"Unsupported file type: {type(part.file)}") + raise ValueError(f"Unsupported part type: {type(part)}") def convert_genai_part_to_a2a(part: types.Part) -> Part: @@ -219,4 +212,4 @@ def convert_genai_part_to_a2a(part: types.Part) -> Part: ) ) ) - raise ValueError(f'Unsupported part type: {part}') \ No newline at end of file + raise ValueError(f"Unsupported part type: {part}") diff --git a/a2a-uAgents-Integration/a2a-Inbound-Communication/Multiagent-Planner/weather_agent/weather_mcp.py b/a2a-uAgents-Integration/a2a-Inbound-Communication/Multiagent-Planner/weather_agent/weather_mcp.py index b60e0303..4d05a0a1 100644 --- a/a2a-uAgents-Integration/a2a-Inbound-Communication/Multiagent-Planner/weather_agent/weather_mcp.py +++ b/a2a-uAgents-Integration/a2a-Inbound-Communication/Multiagent-Planner/weather_agent/weather_mcp.py @@ -10,18 +10,18 @@ # Initialize FastMCP server -mcp = FastMCP('weather') +mcp = FastMCP("weather") # --- Configuration & Constants --- -BASE_URL = 'https://api.weather.gov' -USER_AGENT = 'weather-agent' +BASE_URL = "https://api.weather.gov" +USER_AGENT = "weather-agent" REQUEST_TIMEOUT = 20.0 GEOCODE_TIMEOUT = 10.0 # Timeout for geocoding requests # --- Shared HTTP Client --- http_client = httpx.AsyncClient( base_url=BASE_URL, - headers={'User-Agent': USER_AGENT, 'Accept': 'application/geo+json'}, + headers={"User-Agent": USER_AGENT, "Accept": "application/geo+json"}, timeout=REQUEST_TIMEOUT, follow_redirects=True, ) @@ -63,29 +63,29 @@ async def get_weather_response(endpoint: str) -> dict[str, Any] | None: def format_alert(feature: dict[str, Any]) -> str: """Format an alert feature into a readable string.""" - props = feature.get('properties', {}) # Safer access + props = feature.get("properties", {}) # Safer access # Use .get() with default values for robustness return f""" - Event: {props.get('event', 'Unknown Event')} - Area: {props.get('areaDesc', 'N/A')} - Severity: {props.get('severity', 'N/A')} - Certainty: {props.get('certainty', 'N/A')} - Urgency: {props.get('urgency', 'N/A')} - Effective: {props.get('effective', 'N/A')} - Expires: {props.get('expires', 'N/A')} - Description: {props.get('description', 'No description provided.').strip()} - Instructions: {props.get('instruction', 'No instructions provided.').strip()} + Event: {props.get("event", "Unknown Event")} + Area: {props.get("areaDesc", "N/A")} + Severity: {props.get("severity", "N/A")} + Certainty: {props.get("certainty", "N/A")} + Urgency: {props.get("urgency", "N/A")} + Effective: {props.get("effective", "N/A")} + Expires: {props.get("expires", "N/A")} + Description: {props.get("description", "No description provided.").strip()} + Instructions: {props.get("instruction", "No instructions provided.").strip()} """ def format_forecast_period(period: dict[str, Any]) -> str: """Formats a single forecast period into a readable string.""" return f""" - {period.get('name', 'Unknown Period')}: - Temperature: {period.get('temperature', 'N/A')}°{period.get('temperatureUnit', 'F')} - Wind: {period.get('windSpeed', 'N/A')} {period.get('windDirection', 'N/A')} - Short Forecast: {period.get('shortForecast', 'N/A')} - Detailed Forecast: {period.get('detailedForecast', 'No detailed forecast provided.').strip()} + {period.get("name", "Unknown Period")}: + Temperature: {period.get("temperature", "N/A")}°{period.get("temperatureUnit", "F")} + Wind: {period.get("windSpeed", "N/A")} {period.get("windDirection", "N/A")} + Short Forecast: {period.get("shortForecast", "N/A")} + Detailed Forecast: {period.get("detailedForecast", "No detailed forecast provided.").strip()} """ @@ -101,22 +101,22 @@ async def get_alerts(state: str) -> str: """ # Input validation and normalization if not isinstance(state, str) or len(state) != 2 or not state.isalpha(): - return 'Invalid input. Please provide a two-letter US state code (e.g., CA).' + return "Invalid input. Please provide a two-letter US state code (e.g., CA)." state_code = state.upper() - endpoint = f'/alerts/active/area/{state_code}' + endpoint = f"/alerts/active/area/{state_code}" data = await get_weather_response(endpoint) if data is None: # Error occurred during request - return f'Failed to retrieve weather alerts for {state_code}.' + return f"Failed to retrieve weather alerts for {state_code}." - features = data.get('features') + features = data.get("features") if not features: # Handles both null and empty list - return f'No active weather alerts found for {state_code}.' + return f"No active weather alerts found for {state_code}." alerts = [format_alert(feature) for feature in features] - return '\n---\n'.join(alerts) + return "\n---\n".join(alerts) @mcp.tool() @@ -129,20 +129,20 @@ async def get_forecast(latitude: float, longitude: float) -> str: """ # Input validation if not (-90 <= latitude <= 90 and -180 <= longitude <= 180): - return 'Invalid latitude or longitude provided. Latitude must be between -90 and 90, Longitude between -180 and 180.' + return "Invalid latitude or longitude provided. Latitude must be between -90 and 90, Longitude between -180 and 180." # NWS API requires latitude,longitude format with up to 4 decimal places - point_endpoint = f'/points/{latitude:.4f},{longitude:.4f}' + point_endpoint = f"/points/{latitude:.4f},{longitude:.4f}" points_data = await get_weather_response(point_endpoint) - if points_data is None or 'properties' not in points_data: - return f'Unable to retrieve NWS gridpoint information for {latitude:.4f},{longitude:.4f}.' + if points_data is None or "properties" not in points_data: + return f"Unable to retrieve NWS gridpoint information for {latitude:.4f},{longitude:.4f}." # Extract forecast URLs from the gridpoint data - forecast_url = points_data['properties'].get('forecast') + forecast_url = points_data["properties"].get("forecast") if not forecast_url: - return f'Could not find the NWS forecast endpoint for {latitude:.4f},{longitude:.4f}.' + return f"Could not find the NWS forecast endpoint for {latitude:.4f},{longitude:.4f}." # Make the request to the specific forecast URL forecast_data = None @@ -159,17 +159,17 @@ async def get_forecast(latitude: float, longitude: float) -> str: except Exception: pass # Error handled by returning None below - if forecast_data is None or 'properties' not in forecast_data: - return 'Failed to retrieve detailed forecast data from NWS.' + if forecast_data is None or "properties" not in forecast_data: + return "Failed to retrieve detailed forecast data from NWS." - periods = forecast_data['properties'].get('periods') + periods = forecast_data["properties"].get("periods") if not periods: - return 'No forecast periods found for this location from NWS.' + return "No forecast periods found for this location from NWS." # Format the first 5 periods forecasts = [format_forecast_period(period) for period in periods[:5]] - return '\n---\n'.join(forecasts) + return "\n---\n".join(forecasts) # --- NEW: get_forecast_by_city Tool --- @@ -183,19 +183,19 @@ async def get_forecast_by_city(city: str, state: str) -> str: """ # --- Input Validation --- if not city or not isinstance(city, str): - return 'Invalid city name provided.' + return "Invalid city name provided." if ( not state or not isinstance(state, str) or len(state) != 2 or not state.isalpha() ): - return 'Invalid state code. Please provide the two-letter US state abbreviation (e.g., CA).' + return "Invalid state code. Please provide the two-letter US state abbreviation (e.g., CA)." city_name = city.strip() state_code = state.strip().upper() # Construct a query likely to yield a US result - query = f'{city_name}, {state_code}, USA' + query = f"{city_name}, {state_code}, USA" # --- Geocoding --- location = None @@ -228,5 +228,5 @@ async def shutdown_event(): await http_client.aclose() -if __name__ == '__main__': - mcp.run(transport='stdio') \ No newline at end of file +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/a2a-uAgents-Integration/a2a-Outbound-Communication/Multiagent/agents/__init__.py b/a2a-uAgents-Integration/a2a-Outbound-Communication/Multiagent/agents/__init__.py index 7138d49e..a6fc2600 100644 --- a/a2a-uAgents-Integration/a2a-Outbound-Communication/Multiagent/agents/__init__.py +++ b/a2a-uAgents-Integration/a2a-Outbound-Communication/Multiagent/agents/__init__.py @@ -2,8 +2,4 @@ from .coding_agent import CodingAgentExecutor from .analysis_agent import AnalysisAgentExecutor -__all__ = [ - "ResearchAgentExecutor", - "CodingAgentExecutor", - "AnalysisAgentExecutor" -] +__all__ = ["ResearchAgentExecutor", "CodingAgentExecutor", "AnalysisAgentExecutor"] diff --git a/a2a-uAgents-Integration/a2a-Outbound-Communication/Multiagent/agents/analysis_agent.py b/a2a-uAgents-Integration/a2a-Outbound-Communication/Multiagent/agents/analysis_agent.py index 8fdb7047..60033486 100644 --- a/a2a-uAgents-Integration/a2a-Outbound-Communication/Multiagent/agents/analysis_agent.py +++ b/a2a-uAgents-Integration/a2a-Outbound-Communication/Multiagent/agents/analysis_agent.py @@ -11,17 +11,18 @@ # Load environment variables load_dotenv() + class AnalysisAgentExecutor(AgentExecutor): """Analysis agent that specializes in data analysis and insights generation.""" - + def __init__(self): self.url = "https://api.asi1.ai/v1/chat/completions" self.api_key = os.getenv("ASI1_API_KEY") self.model = "asi1-mini" self.headers = { - 'Content-Type': 'application/json', - 'Accept': 'application/json', - 'Authorization': f'Bearer {self.api_key}' + "Content-Type": "application/json", + "Accept": "application/json", + "Authorization": f"Bearer {self.api_key}", } self.system_prompt = """You are a Senior Data Analyst AI agent. Your expertise includes: 1. Data analysis and interpretation @@ -41,7 +42,7 @@ def __init__(self): Always provide structured, data-driven insights with clear recommendations. """ - + @override async def execute(self, context: RequestContext, event_queue: EventQueue) -> None: message_content = "" @@ -49,7 +50,7 @@ async def execute(self, context: RequestContext, event_queue: EventQueue) -> Non if isinstance(part, Part) and isinstance(part.root, TextPart): message_content = part.root.text break - + try: # Parse command if it's a structured analysis request if message_content.startswith("ANALYZE:"): @@ -67,33 +68,35 @@ async def execute(self, context: RequestContext, event_queue: EventQueue) -> Non else: # General analysis request await self._handle_general_request(message_content, event_queue) - + except Exception as e: await event_queue.enqueue_event( new_agent_text_message(f"❌ Analysis error: {str(e)}") ) - + async def _handle_analyze_command(self, command: str, event_queue: EventQueue): """Handle ANALYZE:data/topic commands.""" data_or_topic = command.replace("ANALYZE:", "", 1) - + prompt = f"Perform a comprehensive analysis of: {data_or_topic}. Include key findings, patterns, and recommendations." - - payload = json.dumps({ - "model": self.model, - "messages": [ - {"role": "system", "content": self.system_prompt}, - {"role": "user", "content": prompt} - ], - "max_tokens": 2000, - "temperature": 0.3, - "stream": False - }) - + + payload = json.dumps( + { + "model": self.model, + "messages": [ + {"role": "system", "content": self.system_prompt}, + {"role": "user", "content": prompt}, + ], + "max_tokens": 2000, + "temperature": 0.3, + "stream": False, + } + ) + response = requests.post(self.url, headers=self.headers, data=payload) response.raise_for_status() - analysis = response.json()['choices'][0]['message']['content'] - + analysis = response.json()["choices"][0]["message"]["content"] + formatted_response = f"""📊 Analysis Agent - Comprehensive Analysis ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 🎯 Subject: {data_or_topic} @@ -102,26 +105,30 @@ async def _handle_analyze_command(self, command: str, event_queue: EventQueue): ✅ Analysis completed by AI Senior Data Analyst """ - + await event_queue.enqueue_event(new_agent_text_message(formatted_response)) - - async def _handle_general_request(self, message_content: str, event_queue: EventQueue): + + async def _handle_general_request( + self, message_content: str, event_queue: EventQueue + ): """Handle general analysis requests.""" - payload = json.dumps({ - "model": self.model, - "messages": [ - {"role": "system", "content": self.system_prompt}, - {"role": "user", "content": f"Analysis Request: {message_content}"} - ], - "max_tokens": 1500, - "temperature": 0.4, - "stream": False - }) - + payload = json.dumps( + { + "model": self.model, + "messages": [ + {"role": "system", "content": self.system_prompt}, + {"role": "user", "content": f"Analysis Request: {message_content}"}, + ], + "max_tokens": 1500, + "temperature": 0.4, + "stream": False, + } + ) + response = requests.post(self.url, headers=self.headers, data=payload) response.raise_for_status() - content = response.json()['choices'][0]['message']['content'] - + content = response.json()["choices"][0]["message"]["content"] + formatted_response = f"""📊 Analysis Agent Response ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 📋 Request: {message_content} @@ -130,25 +137,27 @@ async def _handle_general_request(self, message_content: str, event_queue: Event ✅ Response by AI Senior Data Analyst """ - + await event_queue.enqueue_event(new_agent_text_message(formatted_response)) - + @override async def cancel(self, context: RequestContext, event_queue: EventQueue) -> None: - await event_queue.enqueue_event(new_agent_text_message("Analysis task cancelled.")) - + await event_queue.enqueue_event( + new_agent_text_message("Analysis task cancelled.") + ) + # Placeholder methods for other commands (same structure, omitted for brevity) async def _handle_trends_command(self, command: str, event_queue: EventQueue): pass - + async def _handle_compare_command(self, command: str, event_queue: EventQueue): pass - + async def _handle_metrics_command(self, command: str, event_queue: EventQueue): pass - + async def _handle_insights_command(self, command: str, event_queue: EventQueue): pass - + async def _handle_forecast_command(self, command: str, event_queue: EventQueue): - pass \ No newline at end of file + pass diff --git a/a2a-uAgents-Integration/a2a-Outbound-Communication/Multiagent/agents/coding_agent.py b/a2a-uAgents-Integration/a2a-Outbound-Communication/Multiagent/agents/coding_agent.py index 8ad8e69b..b31d0ab0 100644 --- a/a2a-uAgents-Integration/a2a-Outbound-Communication/Multiagent/agents/coding_agent.py +++ b/a2a-uAgents-Integration/a2a-Outbound-Communication/Multiagent/agents/coding_agent.py @@ -11,17 +11,18 @@ # Load environment variables load_dotenv() + class CodingAgentExecutor(AgentExecutor): """Coding agent that specializes in code generation, debugging, and analysis.""" - + def __init__(self): self.url = "https://api.asi1.ai/v1/chat/completions" self.api_key = os.getenv("ASI1_API_KEY") self.model = "asi1-mini" self.headers = { - 'Content-Type': 'application/json', - 'Accept': 'application/json', - 'Authorization': f'Bearer {self.api_key}' + "Content-Type": "application/json", + "Accept": "application/json", + "Authorization": f"Bearer {self.api_key}", } self.system_prompt = """You are a Senior Software Engineer AI agent. Your expertise includes: 1. Code generation in multiple programming languages @@ -41,7 +42,7 @@ def __init__(self): Always provide clean, well-commented, production-ready code with explanations. """ - + @override async def execute(self, context: RequestContext, event_queue: EventQueue) -> None: message_content = "" @@ -49,7 +50,7 @@ async def execute(self, context: RequestContext, event_queue: EventQueue) -> Non if isinstance(part, Part) and isinstance(part.root, TextPart): message_content = part.root.text break - + try: # Parse command if it's a structured coding request if message_content.startswith("CODE:"): @@ -67,41 +68,45 @@ async def execute(self, context: RequestContext, event_queue: EventQueue) -> Non else: # General coding request await self._handle_general_request(message_content, event_queue) - + except Exception as e: await event_queue.enqueue_event( new_agent_text_message(f"❌ Coding error: {str(e)}") ) - + async def _handle_code_command(self, command: str, event_queue: EventQueue): """Handle CODE:language:description commands.""" parts = command.split(":", 2) if len(parts) < 3: await event_queue.enqueue_event( - new_agent_text_message("❌ Usage: CODE:language:description (e.g., CODE:python:sort algorithm)") + new_agent_text_message( + "❌ Usage: CODE:language:description (e.g., CODE:python:sort algorithm)" + ) ) return - + language = parts[1] description = parts[2] - + prompt = f"Generate {language} code for: {description}. Include comments and explanations." - - payload = json.dumps({ - "model": self.model, - "messages": [ - {"role": "system", "content": self.system_prompt}, - {"role": "user", "content": prompt} - ], - "max_tokens": 2000, - "temperature": 0.3, - "stream": False - }) - + + payload = json.dumps( + { + "model": self.model, + "messages": [ + {"role": "system", "content": self.system_prompt}, + {"role": "user", "content": prompt}, + ], + "max_tokens": 2000, + "temperature": 0.3, + "stream": False, + } + ) + response = requests.post(self.url, headers=self.headers, data=payload) response.raise_for_status() - code = response.json()['choices'][0]['message']['content'] - + code = response.json()["choices"][0]["message"]["content"] + formatted_response = f"""💻 Coding Agent - Code Generation ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 🔧 Language: {language.title()} @@ -111,26 +116,30 @@ async def _handle_code_command(self, command: str, event_queue: EventQueue): ✅ Code generated by AI Senior Software Engineer """ - + await event_queue.enqueue_event(new_agent_text_message(formatted_response)) - - async def _handle_general_request(self, message_content: str, event_queue: EventQueue): + + async def _handle_general_request( + self, message_content: str, event_queue: EventQueue + ): """Handle general coding requests.""" - payload = json.dumps({ - "model": self.model, - "messages": [ - {"role": "system", "content": self.system_prompt}, - {"role": "user", "content": f"Coding Request: {message_content}"} - ], - "max_tokens": 1500, - "temperature": 0.4, - "stream": False - }) - + payload = json.dumps( + { + "model": self.model, + "messages": [ + {"role": "system", "content": self.system_prompt}, + {"role": "user", "content": f"Coding Request: {message_content}"}, + ], + "max_tokens": 1500, + "temperature": 0.4, + "stream": False, + } + ) + response = requests.post(self.url, headers=self.headers, data=payload) response.raise_for_status() - content = response.json()['choices'][0]['message']['content'] - + content = response.json()["choices"][0]["message"]["content"] + formatted_response = f"""💻 Coding Agent Response ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 📋 Request: {message_content} @@ -139,25 +148,27 @@ async def _handle_general_request(self, message_content: str, event_queue: Event ✅ Response by AI Senior Software Engineer """ - + await event_queue.enqueue_event(new_agent_text_message(formatted_response)) - + @override async def cancel(self, context: RequestContext, event_queue: EventQueue) -> None: - await event_queue.enqueue_event(new_agent_text_message("Coding task cancelled.")) - + await event_queue.enqueue_event( + new_agent_text_message("Coding task cancelled.") + ) + # Placeholder methods for other commands (same structure, omitted for brevity) async def _handle_debug_command(self, command: str, event_queue: EventQueue): pass - + async def _handle_review_command(self, command: str, event_queue: EventQueue): pass - + async def _handle_optimize_command(self, command: str, event_queue: EventQueue): pass - + async def _handle_explain_command(self, command: str, event_queue: EventQueue): pass - + async def _handle_test_command(self, command: str, event_queue: EventQueue): - pass \ No newline at end of file + pass diff --git a/a2a-uAgents-Integration/a2a-Outbound-Communication/Multiagent/agents/research_agent.py b/a2a-uAgents-Integration/a2a-Outbound-Communication/Multiagent/agents/research_agent.py index d1578f73..e65dd7e8 100644 --- a/a2a-uAgents-Integration/a2a-Outbound-Communication/Multiagent/agents/research_agent.py +++ b/a2a-uAgents-Integration/a2a-Outbound-Communication/Multiagent/agents/research_agent.py @@ -16,6 +16,7 @@ # Load environment variables load_dotenv() + class ResearchAgentExecutor(AgentExecutor): """Research agent that specializes in information gathering and analysis.""" @@ -26,9 +27,9 @@ def __init__(self): raise ValueError("ASI1_API_KEY environment variable is not set") self.model = os.getenv("ASI1_MODEL", "asi1-mini") self.headers = { - 'Content-Type': 'application/json', - 'Accept': 'application/json', - 'Authorization': f'Bearer {self.api_key}' + "Content-Type": "application/json", + "Accept": "application/json", + "Authorization": f"Bearer {self.api_key}", } self.system_prompt = """You are a Research Specialist AI agent. Your role is to: 1. Conduct thorough research on any given topic @@ -62,18 +63,22 @@ async def execute(self, context: RequestContext, event_queue: EventQueue) -> Non "model": self.model, "messages": [ {"role": "system", "content": self.system_prompt}, - {"role": "user", "content": f"Research Request: {message_content}"} + {"role": "user", "content": f"Research Request: {message_content}"}, ], "max_tokens": 1500, "temperature": 0.3, - "stream": False + "stream": False, } - logger.info(f"Sending request to {self.url} with payload: {json.dumps(payload, indent=2)}") - response = await self.http_client.post(self.url, headers=self.headers, json=payload) + logger.info( + f"Sending request to {self.url} with payload: {json.dumps(payload, indent=2)}" + ) + response = await self.http_client.post( + self.url, headers=self.headers, json=payload + ) logger.info(f"Received response: {response.status_code}") response.raise_for_status() - research_result = response.json()['choices'][0]['message']['content'] + research_result = response.json()["choices"][0]["message"]["content"] formatted_response = f"""🔍 Research Agent Analysis ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ @@ -88,11 +93,13 @@ async def execute(self, context: RequestContext, event_queue: EventQueue) -> Non except Exception as e: logger.error(f"Research error: {e}", exc_info=True) - await event_queue.enqueue_event(new_agent_text_message(f"❌ Research error: {str(e)}")) + await event_queue.enqueue_event( + new_agent_text_message(f"❌ Research error: {str(e)}") + ) @override async def cancel(self, context: RequestContext, event_queue: EventQueue) -> None: await event_queue.enqueue_event(new_agent_text_message("Research cancelled.")) async def close(self): - await self.http_client.aclose() # Clean up HTTP client \ No newline at end of file + await self.http_client.aclose() # Clean up HTTP client diff --git a/a2a-uAgents-Integration/a2a-Outbound-Communication/Multiagent/main.py b/a2a-uAgents-Integration/a2a-Outbound-Communication/Multiagent/main.py index c389fa86..7edb81c4 100644 --- a/a2a-uAgents-Integration/a2a-Outbound-Communication/Multiagent/main.py +++ b/a2a-uAgents-Integration/a2a-Outbound-Communication/Multiagent/main.py @@ -8,6 +8,7 @@ load_dotenv() + class MultiAgentOrchestrator: def __init__(self): self.coordinator = None @@ -24,7 +25,7 @@ def setup_agents(self): url="http://localhost:10020", port=10020, specialties=["research", "analysis", "fact-finding", "summarization"], - priority=3 + priority=3, ), A2AAgentConfig( name="coding_specialist", @@ -32,7 +33,7 @@ def setup_agents(self): url="http://localhost:10022", port=10022, specialties=["coding", "debugging", "programming"], - priority=3 + priority=3, ), A2AAgentConfig( name="analysis_specialist", @@ -40,13 +41,13 @@ def setup_agents(self): url="http://localhost:10023", port=10023, specialties=["data analysis", "insights", "forecasting"], - priority=2 - ) + priority=2, + ), ] self.executors = { "research_specialist": ResearchAgentExecutor(), "coding_specialist": CodingAgentExecutor(), - "analysis_specialist": AnalysisAgentExecutor() + "analysis_specialist": AnalysisAgentExecutor(), } print("✅ Agent configurations created") @@ -60,13 +61,13 @@ def create_coordinator(self): self.coordinator = MultiA2AAdapter( name="coordinator----", description="Routes queries to AI specialists", - llm_api_key= os.getenv("ASI1_API_KEY"), - model= os.getenv("MODEL", "asi1-mini"), - base_url= os.getenv("BASE_URL", "https://api.asi1.ai/v1/chat/completions"), + llm_api_key=os.getenv("ASI1_API_KEY"), + model=os.getenv("MODEL", "asi1-mini"), + base_url=os.getenv("BASE_URL", "https://api.asi1.ai/v1/chat/completions"), port=8200, mailbox=True, agent_configs=self.agent_configs, - routing_strategy="llm" # Changed from "keyword_match" to "llm" + routing_strategy="llm", # Changed from "keyword_match" to "llm" ) print("✅ Coordinator created!") return self.coordinator @@ -87,6 +88,7 @@ def start_system(self): print(f"❌ Error: {e}") self.running = False + def main(): try: system = MultiAgentOrchestrator() @@ -96,5 +98,6 @@ def main(): except Exception as e: print(f"❌ Error: {e}") + if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/a2a-uAgents-Integration/a2a-Outbound-Communication/book-agent/books_recommender_agent.py b/a2a-uAgents-Integration/a2a-Outbound-Communication/book-agent/books_recommender_agent.py index 2f27d96c..9998fb36 100644 --- a/a2a-uAgents-Integration/a2a-Outbound-Communication/book-agent/books_recommender_agent.py +++ b/a2a-uAgents-Integration/a2a-Outbound-Communication/book-agent/books_recommender_agent.py @@ -1,5 +1,4 @@ import asyncio -from typing import List from a2a.server.agent_execution import AgentExecutor, RequestContext from a2a.server.events import EventQueue from a2a.types import Part, TextPart @@ -40,15 +39,17 @@ "**After listing individual book details, provide a comparative analysis section.**", " - Compare the top 3-5 recommended books based on key criteria (e.g., price, rating, genre suitability, reader feedback).", " - Highlight their similarities and differences to help the user make an informed decision.", - "Format the recommendations neatly and ensure clarity for ease of user understanding, presenting them as a structured report with clear headings and bullet points. Use a table for the comparative analysis if appropriate. Include the book cover image URLs in the output, ensuring they are valid and accessible." + "Format the recommendations neatly and ensure clarity for ease of user understanding, presenting them as a structured report with clear headings and bullet points. Use a table for the comparative analysis if appropriate. Include the book cover image URLs in the output, ensuring they are valid and accessible.", ], tools=[ExaTools()], ) + class BooksRecommenderAgentExecutor(AgentExecutor): """ AgentExecutor wrapper for the agno.agent books recommender. """ + def __init__(self): self.agent = books_recommender_agno_agent @@ -67,19 +68,25 @@ async def execute( if isinstance(part.root, TextPart): message_content = part.root.text break - + if not message_content: - await event_queue.enqueue_event(new_agent_text_message("Error: No message content received.")) + await event_queue.enqueue_event( + new_agent_text_message("Error: No message content received.") + ) return message: Message = Message(role="user", content=message_content) logger.info(f"Received message: {message.content}") - + try: logger.info("Starting agno agent run with timeout...") - result: RunOutput = await asyncio.wait_for(self.agent.arun(message), timeout=180) - logger.info(f"Agno agent finished run. Response content type: {type(result.content)}") - + result: RunOutput = await asyncio.wait_for( + self.agent.arun(message), timeout=180 + ) + logger.info( + f"Agno agent finished run. Response content type: {type(result.content)}" + ) + response_text = str(result.content) await event_queue.enqueue_event(new_agent_text_message(response_text)) logger.info("Event enqueued successfully.") @@ -87,12 +94,20 @@ async def execute( except asyncio.TimeoutError: error_message = "Agno agent execution timed out after 180 seconds. The query might be too complex or require more time." logger.error(error_message) - await event_queue.enqueue_event(new_agent_text_message(f"Error: {error_message}. Please try again or simplify your query.")) + await event_queue.enqueue_event( + new_agent_text_message( + f"Error: {error_message}. Please try again or simplify your query." + ) + ) except Exception as e: error_message = f"Error during agno agent execution: {e}" logger.error(error_message, exc_info=True) - await event_queue.enqueue_event(new_agent_text_message(f"Error: {error_message}. Please check logs for details.")) - + await event_queue.enqueue_event( + new_agent_text_message( + f"Error: {error_message}. Please check logs for details." + ) + ) + logger.info("execute method finished.") @override @@ -100,4 +115,4 @@ async def cancel(self, context: RequestContext, event_queue: EventQueue) -> None """ Cancels the agent's execution. """ - raise Exception("Cancel not supported for this agent executor.") \ No newline at end of file + raise Exception("Cancel not supported for this agent executor.") diff --git a/a2a-uAgents-Integration/a2a-Outbound-Communication/book-agent/main.py b/a2a-uAgents-Integration/a2a-Outbound-Communication/book-agent/main.py index 75fb025c..d258b287 100644 --- a/a2a-uAgents-Integration/a2a-Outbound-Communication/book-agent/main.py +++ b/a2a-uAgents-Integration/a2a-Outbound-Communication/book-agent/main.py @@ -1,4 +1,3 @@ -import os from threading import Thread from typing import Dict, List from uagents_adapter import SingleA2AAdapter, A2AAgentConfig, a2a_servers @@ -14,10 +13,12 @@ # Load environment variables from .env file load_dotenv() + class BooksRecommenderSystem: """ Manages the setup and execution of the A2A Books Recommender agent. """ + def __init__(self): self.coordinator = None self.agent_configs: List[A2AAgentConfig] = [] @@ -36,10 +37,16 @@ def setup_agents(self): url="http://localhost:10022", port=10022, specialties=[ - "book recommendations", "literature", "fiction", "non-fiction", - "genres", "authors", "reading lists", "book reviews" + "book recommendations", + "literature", + "fiction", + "non-fiction", + "genres", + "authors", + "reading lists", + "book reviews", ], - priority=3 + priority=3, ) ] self.executors = { @@ -62,7 +69,9 @@ def create_coordinator(self): logger.info("🤖 Creating Books Recommender Coordinator...") books_executor = self.executors.get("books_recommender_specialist") if books_executor is None: - raise ValueError("BooksRecommenderAgentExecutor not found in executors dictionary.") + raise ValueError( + "BooksRecommenderAgentExecutor not found in executors dictionary." + ) self.coordinator = SingleA2AAdapter( agent_executor=books_executor, @@ -70,7 +79,6 @@ def create_coordinator(self): description="Coordinator for routing book-related queries to the Books Recommender Agent.", port=8033, mailbox=True, - ) logger.info("✅ Books Recommender Coordinator created!") return self.coordinator @@ -85,7 +93,9 @@ def start_system(self): self.start_individual_a2a_servers() coordinator = self.create_coordinator() self.running = True - logger.info(f"🎯 Starting Books Recommender coordinator on port {coordinator.port}...") + logger.info( + f"🎯 Starting Books Recommender coordinator on port {coordinator.port}..." + ) coordinator.run() except KeyboardInterrupt: logger.info("👋 Shutting down Books Recommender system...") @@ -94,6 +104,7 @@ def start_system(self): logger.error(f"❌ Error during agent system startup: {e}", exc_info=True) self.running = False + def main(): """ Main function to run the Books Recommender System in a separate thread. @@ -111,6 +122,7 @@ def main(): finally: loop.close() + if __name__ == "__main__": logger.info("🚀 Starting Books Recommender System...") agent_thread = Thread(target=main, daemon=True) @@ -119,4 +131,4 @@ def main(): try: agent_thread.join() except KeyboardInterrupt: - logger.info("👋 Main thread shutdown complete!") \ No newline at end of file + logger.info("👋 Main thread shutdown complete!") diff --git a/a2a-uAgents-Integration/a2a-Outbound-Communication/braveagent/brave/agent.py b/a2a-uAgents-Integration/a2a-Outbound-Communication/braveagent/brave/agent.py index bad32726..d097b4d5 100644 --- a/a2a-uAgents-Integration/a2a-Outbound-Communication/braveagent/brave/agent.py +++ b/a2a-uAgents-Integration/a2a-Outbound-Communication/braveagent/brave/agent.py @@ -10,9 +10,10 @@ # Load environment variables load_dotenv() + class BraveSearchAgentExecutor(AgentExecutor): """Brave Search Agent that specializes in web and local search queries using the Brave Search API.""" - + def __init__(self): self.url = "https://api.search.brave.com/res/v1/web/search" self.local_url = "https://api.search.brave.com/res/v1/local/pois" @@ -40,7 +41,7 @@ def __init__(self): Always provide clear, concise, and well-structured search results. """ - + @override async def execute(self, context: RequestContext, event_queue: EventQueue) -> None: message_content = "" @@ -48,7 +49,7 @@ async def execute(self, context: RequestContext, event_queue: EventQueue) -> Non if isinstance(part, Part) and isinstance(part.root, TextPart): message_content = part.root.text break - + try: # Parse command if it's a structured search request if message_content.startswith("WEB:"): @@ -61,22 +62,26 @@ async def execute(self, context: RequestContext, event_queue: EventQueue) -> Non await self._handle_summarize_command(message_content, event_queue) else: # General search request - await self._handle_general_search_command(f"SEARCH:{message_content}", event_queue) - + await self._handle_general_search_command( + f"SEARCH:{message_content}", event_queue + ) + except Exception as e: await event_queue.enqueue_event( new_agent_text_message(f"❌ Search error: {str(e)}") ) - + async def _handle_web_search_command(self, command: str, event_queue: EventQueue): """Handle WEB:query commands.""" query = command.replace("WEB:", "", 1) if not query: await event_queue.enqueue_event( - new_agent_text_message("❌ Usage: WEB:query (e.g., WEB:Python programming)") + new_agent_text_message( + "❌ Usage: WEB:query (e.g., WEB:Python programming)" + ) ) return - + results = await self._perform_web_search(query) formatted_response = f"""🌐 Brave Search Agent - Web Search ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ @@ -87,16 +92,18 @@ async def _handle_web_search_command(self, command: str, event_queue: EventQueue ✅ Web search by Brave Search Agent """ await event_queue.enqueue_event(new_agent_text_message(formatted_response)) - + async def _handle_local_search_command(self, command: str, event_queue: EventQueue): """Handle LOCAL:query commands.""" query = command.replace("LOCAL:", "", 1) if not query: await event_queue.enqueue_event( - new_agent_text_message("❌ Usage: LOCAL:query (e.g., LOCAL:pizza near Central Park)") + new_agent_text_message( + "❌ Usage: LOCAL:query (e.g., LOCAL:pizza near Central Park)" + ) ) return - + results = await self._perform_local_search(query) formatted_response = f"""📍 Brave Search Agent - Local Search ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ @@ -107,16 +114,20 @@ async def _handle_local_search_command(self, command: str, event_queue: EventQue ✅ Local search by Brave Search Agent """ await event_queue.enqueue_event(new_agent_text_message(formatted_response)) - - async def _handle_general_search_command(self, command: str, event_queue: EventQueue): + + async def _handle_general_search_command( + self, command: str, event_queue: EventQueue + ): """Handle SEARCH:query commands.""" query = command.replace("SEARCH:", "", 1) if not query: await event_queue.enqueue_event( - new_agent_text_message("❌ Usage: SEARCH:query (e.g., SEARCH:latest AI news можем") + new_agent_text_message( + "❌ Usage: SEARCH:query (e.g., SEARCH:latest AI news можем" + ) ) return - + results = await self._perform_web_search(query) formatted_response = f"""🌐 Brave Search Agent - General Search ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ @@ -127,19 +138,23 @@ async def _handle_general_search_command(self, command: str, event_queue: EventQ ✅ General search by Brave Search Agent """ await event_queue.enqueue_event(new_agent_text_message(formatted_response)) - + async def _handle_summarize_command(self, command: str, event_queue: EventQueue): """Handle SUMMARIZE:query commands.""" query = command.replace("SUMMARIZE:", "", 1) if not query: await event_queue.enqueue_event( - new_agent_text_message("❌ Usage: SUMMARIZE:query (e.g., SUMMARIZE:AI advancements 2025)") + new_agent_text_message( + "❌ Usage: SUMMARIZE:query (e.g., SUMMARIZE:AI advancements 2025)" + ) ) return - + results = await self._perform_web_search(query) # Simplified summarization (could be enhanced with NLP if needed) - summary = "\n".join(line for line in results.split("\n") if "Description:" in line) + summary = "\n".join( + line for line in results.split("\n") if "Description:" in line + ) formatted_response = f"""📝 Brave Search Agent - Summary ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 🔍 Query: {query} @@ -149,7 +164,7 @@ async def _handle_summarize_command(self, command: str, event_queue: EventQueue) ✅ Summary by Brave Search Agent """ await event_queue.enqueue_event(new_agent_text_message(formatted_response)) - + async def _perform_web_search(self, query: str, count: int = 10) -> str: """Perform a web search using the Brave Search API.""" if len(query) > 400: @@ -172,14 +187,19 @@ async def _perform_web_search(self, query: str, count: int = 10) -> str: f"Title: {r.get('title', 'N/A')}\nDescription: {r.get('description', 'N/A')}\nURL: {r.get('url', 'N/A')}" for r in results ) - + async def _perform_local_search(self, query: str, count: int = 5) -> str: """Perform a local search using the Brave Search API.""" if len(query) > 400: raise ValueError("Query exceeds 400 characters") count = min(max(count, 1), 20) # Clamp count to 1-20 - params = {"q": query, "search_lang": "en", "result_filter": "locations", "count": count} + params = { + "q": query, + "search_lang": "en", + "result_filter": "locations", + "count": count, + } try: response = requests.get(self.url, params=params, headers=self.headers) response.raise_for_status() @@ -187,16 +207,22 @@ async def _perform_local_search(self, query: str, count: int = 5) -> str: raise ValueError(f"Brave API error: {str(e)}") data = response.json() - location_ids = [r["id"] for r in data.get("locations", {}).get("results", []) if r.get("id")] + location_ids = [ + r["id"] for r in data.get("locations", {}).get("results", []) if r.get("id") + ] if not location_ids: return await self._perform_web_search(query, count) params = {"ids": location_ids} try: - poi_response = requests.get(self.local_url, params=params, headers=self.headers) + poi_response = requests.get( + self.local_url, params=params, headers=self.headers + ) poi_response.raise_for_status() - desc_response = requests.get(self.desc_url, params=params, headers=self.headers) + desc_response = requests.get( + self.desc_url, params=params, headers=self.headers + ) desc_response.raise_for_status() except requests.RequestException as e: raise ValueError(f"Brave API error: {str(e)}") @@ -206,15 +232,23 @@ async def _perform_local_search(self, query: str, count: int = 5) -> str: results = [] for loc in poi_data.get("results", []): - address = ", ".join( - filter( - None, - [ - loc.get("address", {}).get(key, "") - for key in ["streetAddress", "addressLocality", "addressRegion", "postalCode"] - ], + address = ( + ", ".join( + filter( + None, + [ + loc.get("address", {}).get(key, "") + for key in [ + "streetAddress", + "addressLocality", + "addressRegion", + "postalCode", + ] + ], + ) ) - ) or "N/A" + or "N/A" + ) result = ( f"Name: {loc.get('name', 'N/A')}\n" f"Address: {address}\n" @@ -226,7 +260,9 @@ async def _perform_local_search(self, query: str, count: int = 5) -> str: results.append(result) return "\n---\n".join(results) or "No local results found" - + @override async def cancel(self, context: RequestContext, event_queue: EventQueue) -> None: - await event_queue.enqueue_event(new_agent_text_message("Search task cancelled.")) \ No newline at end of file + await event_queue.enqueue_event( + new_agent_text_message("Search task cancelled.") + ) diff --git a/a2a-uAgents-Integration/a2a-Outbound-Communication/braveagent/function.py b/a2a-uAgents-Integration/a2a-Outbound-Communication/braveagent/function.py index 48b1c55e..825f1781 100644 --- a/a2a-uAgents-Integration/a2a-Outbound-Communication/braveagent/function.py +++ b/a2a-uAgents-Integration/a2a-Outbound-Communication/braveagent/function.py @@ -1,6 +1,7 @@ from typing import Dict, List from uagents_adapter import SingleA2AAdapter, A2AAgentConfig, a2a_servers -from brave.agent import BraveSearchAgentExecutor +from brave.agent import BraveSearchAgentExecutor + class BraveSearchAgent: def __init__(self): @@ -17,13 +18,17 @@ def setup_agents(self): description="AI Agent for web and news search using Brave Search API", url="http://localhost:10020", port=10020, - specialties=["web search", "news", "information retrieval", "local business", "site-specific lookup"], - priority=3 + specialties=[ + "web search", + "news", + "information retrieval", + "local business", + "site-specific lookup", + ], + priority=3, ) ] - self.executors = { - "brave_search_specialist": BraveSearchAgentExecutor() - } + self.executors = {"brave_search_specialist": BraveSearchAgentExecutor()} print("✅ Brave Search Agent configuration created") def start_individual_a2a_servers(self): @@ -36,13 +41,15 @@ def create_coordinator(self): # Use a valid agent_executor, e.g., the BraveSearchAgentExecutor or a coordinator logic brave_executor = self.executors.get("brave_search_specialist") if brave_executor is None: - raise ValueError("BraveSearchAgentExecutor not found in executors dictionary.") + raise ValueError( + "BraveSearchAgentExecutor not found in executors dictionary." + ) self.coordinator = SingleA2AAdapter( agent_executor=brave_executor, name="brave-search-coordinator", description="Coordinator for routing Brave Search queries", port=8200, - mailbox=True + mailbox=True, ) print("✅ Brave Coordinator created!") return self.coordinator @@ -63,6 +70,7 @@ def start_system(self): print(f"❌ Error: {e}") self.running = False + def main(): try: system = BraveSearchAgent() @@ -72,5 +80,6 @@ def main(): except Exception as e: print(f"❌ Error: {e}") + if __name__ == "__main__": main() diff --git a/a2a-uAgents-Integration/a2a-Outbound-Communication/collaboration_team/collaboration_team.py b/a2a-uAgents-Integration/a2a-Outbound-Communication/collaboration_team/collaboration_team.py index f98e2d11..85eeb80c 100644 --- a/a2a-uAgents-Integration/a2a-Outbound-Communication/collaboration_team/collaboration_team.py +++ b/a2a-uAgents-Integration/a2a-Outbound-Communication/collaboration_team/collaboration_team.py @@ -1,9 +1,5 @@ - import asyncio -from pathlib import Path from textwrap import dedent -from typing import List -from uuid import uuid4 from a2a.server.agent_execution import AgentExecutor, RequestContext from a2a.server.events import EventQueue @@ -13,15 +9,16 @@ from agno.models.google import Gemini from agno.team.team import Team from agno.tools.arxiv import ArxivTools -from agno.tools.duckduckgo import DuckDuckGoTools from agno.tools.googlesearch import GoogleSearchTools from typing_extensions import override reddit_researcher = Agent( name="Reddit Researcher", role="Research a topic on Reddit", - model=Gemini(id="gemini-2.0-flash"), - tools=[GoogleSearchTools()], # Use Google Search instead of DuckDuckGo for better reliability + model=Gemini(id="gemini-2.0-flash"), + tools=[ + GoogleSearchTools() + ], # Use Google Search instead of DuckDuckGo for better reliability instructions=dedent(""" You are a Reddit researcher specializing in detailed community analysis. You will be given a topic to research on Reddit. @@ -52,7 +49,9 @@ name="HackerNews Researcher", model=Gemini(id="gemini-2.0-flash"), role="Research a topic on HackerNews.", - tools=[GoogleSearchTools()], # Use Google Search instead of DuckDuckGo for better reliability + tools=[ + GoogleSearchTools() + ], # Use Google Search instead of DuckDuckGo for better reliability instructions=dedent(""" You are a HackerNews researcher specializing in technical and industry analysis. You will be given a topic to research on HackerNews. @@ -129,7 +128,9 @@ name="Twitter Researcher", model=Gemini(id="gemini-2.0-flash"), role="Research trending discussions and real-time updates", - tools=[GoogleSearchTools()], # Use Google Search instead of DuckDuckGo for better reliability + tools=[ + GoogleSearchTools() + ], # Use Google Search instead of DuckDuckGo for better reliability instructions=dedent(""" You are a Twitter/X researcher specializing in real-time trend analysis. You will be given a topic to research on Twitter/X. @@ -201,16 +202,18 @@ "6. **Conclusion** - Summary of key takeaways", "**MANDATORY: Ensure ALL agents create their summary tables as specified in their instructions**", "**MANDATORY: Include ALL direct links provided by agents in the final response**", - "**MANDATORY: Create at least 3 summary tables in the final response**" + "**MANDATORY: Create at least 3 summary tables in the final response**", ], markdown=True, show_members_responses=True, ) + class DiscussionTeamExecutor(AgentExecutor): """ AgentExecutor wrapper for the agno.team discussion team. """ + def __init__(self): self.agent_team = discussion_team @@ -226,38 +229,55 @@ async def execute( if isinstance(part.root, TextPart): message_content = part.root.text break - + if not message_content: - await event_queue.enqueue_event(new_agent_text_message("Error: No message content received.")) + await event_queue.enqueue_event( + new_agent_text_message("Error: No message content received.") + ) return message: Message = Message(role="user", content=message_content) print(f"DEBUG: [DiscussionTeamExecutor] Received message: {message.content}") - + try: - print("DEBUG: [DiscussionTeamExecutor] Starting agno team run with timeout...") + print( + "DEBUG: [DiscussionTeamExecutor] Starting agno team run with timeout..." + ) # Set a very generous timeout for the agno team's execution (e.g., 10 minutes) # Team discussions with multiple tools can take a long time. - result: RunOutput = await asyncio.wait_for(self.agent_team.arun(message), timeout=600) # 10 minutes timeout - print(f"DEBUG: [DiscussionTeamExecutor] Agno team finished run. Response content type: {type(result.content)}") - - response_text = str(result.content) + result: RunOutput = await asyncio.wait_for( + self.agent_team.arun(message), timeout=600 + ) # 10 minutes timeout + print( + f"DEBUG: [DiscussionTeamExecutor] Agno team finished run. Response content type: {type(result.content)}" + ) + + response_text = str(result.content) await event_queue.enqueue_event(new_agent_text_message(response_text)) print("DEBUG: [DiscussionTeamExecutor] Event enqueued successfully.") except asyncio.TimeoutError: error_message = "Agno team execution timed out. The discussion might be too complex or require more time." print(f"❌ {error_message}") - await event_queue.enqueue_event(new_agent_text_message(f"Error: {error_message}. Please try again or simplify your query.")) + await event_queue.enqueue_event( + new_agent_text_message( + f"Error: {error_message}. Please try again or simplify your query." + ) + ) except Exception as e: error_message = f"Error during agno agent execution: {e}" print(f"❌ {error_message}") import traceback + traceback.print_exc() - await event_queue.enqueue_event(new_agent_text_message(f"Error: {error_message}. Please check logs for details.")) - + await event_queue.enqueue_event( + new_agent_text_message( + f"Error: {error_message}. Please check logs for details." + ) + ) + print("DEBUG: [DiscussionTeamExecutor] execute method finished.") @override async def cancel(self, context: RequestContext, event_queue: EventQueue) -> None: - raise Exception("Cancel not supported for this agent executor.") \ No newline at end of file + raise Exception("Cancel not supported for this agent executor.") diff --git a/a2a-uAgents-Integration/a2a-Outbound-Communication/collaboration_team/main.py b/a2a-uAgents-Integration/a2a-Outbound-Communication/collaboration_team/main.py index b63df645..89d2f6d8 100644 --- a/a2a-uAgents-Integration/a2a-Outbound-Communication/collaboration_team/main.py +++ b/a2a-uAgents-Integration/a2a-Outbound-Communication/collaboration_team/main.py @@ -1,16 +1,17 @@ -import os from typing import Dict, List -from uagents_adapter import SingleA2AAdapter, A2AAgentConfig, a2a_servers -from collaboration_team import DiscussionTeamExecutor +from uagents_adapter import SingleA2AAdapter, A2AAgentConfig, a2a_servers +from collaboration_team import DiscussionTeamExecutor from dotenv import load_dotenv # Load environment variables from .env file load_dotenv() + class DiscussionTeamSystem: """ Manages the setup and execution of the A2A Discussion Team agent. """ + def __init__(self): self.coordinator = None self.agent_configs: List[A2AAgentConfig] = [] @@ -26,17 +27,22 @@ def setup_agents(self): A2AAgentConfig( name="discussion_team_specialist", description="AI Team for comprehensive research and discussion across various platforms (Reddit, HackerNews, Academic, Twitter).", - url="http://localhost:10020", # The URL where the A2A server for this agent will run - port=10020, # The port for the A2A server + url="http://localhost:10020", # The URL where the A2A server for this agent will run + port=10020, # The port for the A2A server specialties=[ - "research", "discussion", "consensus building", "social media analysis", - "academic research", "tech news", "community insights" + "research", + "discussion", + "consensus building", + "social media analysis", + "academic research", + "tech news", + "community insights", ], - priority=3 + priority=3, ) ] self.executors = { - "discussion_team_specialist": DiscussionTeamExecutor() # NEW: Use the new executor + "discussion_team_specialist": DiscussionTeamExecutor() # NEW: Use the new executor } print("✅ Discussion Team Agent configuration created") @@ -53,19 +59,21 @@ def create_coordinator(self): Creates the SingleA2AAdapter (uAgent coordinator) for the discussion team. """ print("🤖 Creating Discussion Team Coordinator...") - + # Get the executor instance discussion_executor = self.executors.get("discussion_team_specialist") if discussion_executor is None: - raise ValueError("DiscussionTeamExecutor not found in executors dictionary.") + raise ValueError( + "DiscussionTeamExecutor not found in executors dictionary." + ) self.coordinator = SingleA2AAdapter( agent_executor=discussion_executor, name="discussion-team-coordinator", description="Coordinator for routing discussion topics to the specialist team.", - port=8200, # The port for the uAgent coordinator + port=8200, # The port for the uAgent coordinator mailbox=True, - timeout=2000 + timeout=2000, ) print("✅ Discussion Team Coordinator created!") return self.coordinator @@ -80,7 +88,9 @@ def start_system(self): self.start_individual_a2a_servers() coordinator = self.create_coordinator() self.running = True - print(f"🎯 Starting Discussion Team coordinator on port {coordinator.port}...") + print( + f"🎯 Starting Discussion Team coordinator on port {coordinator.port}..." + ) coordinator.run() except KeyboardInterrupt: print("👋 Shutting down Discussion Team system...") @@ -89,6 +99,7 @@ def start_system(self): print(f"❌ Error during system startup: {e}") self.running = False + def main(): """ Main function to run the Discussion Team A2A system. @@ -101,5 +112,6 @@ def main(): except Exception as e: print(f"❌ An error occurred: {e}") + if __name__ == "__main__": main() diff --git a/a2a-uAgents-Integration/a2a-Outbound-Communication/competitor_analysis_agent/competitor_analysis_executor.py b/a2a-uAgents-Integration/a2a-Outbound-Communication/competitor_analysis_agent/competitor_analysis_executor.py index 090abbee..d3bcb0e0 100644 --- a/a2a-uAgents-Integration/a2a-Outbound-Communication/competitor_analysis_agent/competitor_analysis_executor.py +++ b/a2a-uAgents-Integration/a2a-Outbound-Communication/competitor_analysis_agent/competitor_analysis_executor.py @@ -1,7 +1,5 @@ import asyncio from textwrap import dedent -from typing import List -from uuid import uuid4 import dotenv from a2a.server.agent_execution import AgentExecutor, RequestContext @@ -27,8 +25,8 @@ enable_crawl=True, enable_mapping=True, formats=["markdown", "links", "html"], - search_params={"limit": 1}, # REDUCED: Limit search results to 1 - limit=1, # REDUCED: Limit crawl depth/pages to 1 + search_params={"limit": 1}, # REDUCED: Limit search results to 1 + limit=1, # REDUCED: Limit crawl depth/pages to 1 ), ReasoningTools(), ], @@ -89,7 +87,7 @@ "**MANDATORY TABLES TO INCLUDE:**", "- Market Segmentation Table", "- Market Leaders Table", - "- Challengers Table", + "- Challengers Table", "- SWOT Analysis Tables (for each top competitor)", "- Feature Comparison Matrix", "- Pricing Comparison Table", @@ -231,10 +229,12 @@ markdown=True, ) + class CompetitorAnalysisExecutor(AgentExecutor): """ AgentExecutor wrapper for the agno.agent competitor analysis agent. """ + def __init__(self): self.agent = competitor_analysis_agno_agent @@ -250,35 +250,54 @@ async def execute( if isinstance(part.root, TextPart): message_content = part.root.text break - + if not message_content: - await event_queue.enqueue_event(new_agent_text_message("Error: No message content received.")) + await event_queue.enqueue_event( + new_agent_text_message("Error: No message content received.") + ) return message: Message = Message(role="user", content=message_content) - print(f"DEBUG: [CompetitorAnalysisExecutor] Received message: {message.content}") - + print( + f"DEBUG: [CompetitorAnalysisExecutor] Received message: {message.content}" + ) + try: - print("DEBUG: [CompetitorAnalysisExecutor] Starting agno agent run with timeout...") + print( + "DEBUG: [CompetitorAnalysisExecutor] Starting agno agent run with timeout..." + ) # Set a generous timeout for the agno agent's execution, as it involves web searches/crawling - result: RunOutput = await asyncio.wait_for(self.agent.arun(message), timeout=300) # 5 minutes timeout - print(f"DEBUG: [CompetitorAnalysisExecutor] Agno agent finished run. Response content type: {type(result.content)}") - - response_text = str(result.content) + result: RunOutput = await asyncio.wait_for( + self.agent.arun(message), timeout=300 + ) # 5 minutes timeout + print( + f"DEBUG: [CompetitorAnalysisExecutor] Agno agent finished run. Response content type: {type(result.content)}" + ) + + response_text = str(result.content) await event_queue.enqueue_event(new_agent_text_message(response_text)) print("DEBUG: [CompetitorAnalysisExecutor] Event enqueued successfully.") except asyncio.TimeoutError: error_message = "Agno agent execution timed out. The analysis might be too complex or require more time." print(f"❌ {error_message}") - await event_queue.enqueue_event(new_agent_text_message(f"Error: {error_message}. Please try again or simplify your query.")) + await event_queue.enqueue_event( + new_agent_text_message( + f"Error: {error_message}. Please try again or simplify your query." + ) + ) except Exception as e: error_message = f"Error during agno agent execution: {e}" print(f"❌ {error_message}") import traceback + traceback.print_exc() - await event_queue.enqueue_event(new_agent_text_message(f"Error: {error_message}. Please check logs for details.")) - + await event_queue.enqueue_event( + new_agent_text_message( + f"Error: {error_message}. Please check logs for details." + ) + ) + print("DEBUG: [CompetitorAnalysisExecutor] execute method finished.") @override diff --git a/a2a-uAgents-Integration/a2a-Outbound-Communication/competitor_analysis_agent/main.py b/a2a-uAgents-Integration/a2a-Outbound-Communication/competitor_analysis_agent/main.py index 2c3c1a22..d9e02b6e 100644 --- a/a2a-uAgents-Integration/a2a-Outbound-Communication/competitor_analysis_agent/main.py +++ b/a2a-uAgents-Integration/a2a-Outbound-Communication/competitor_analysis_agent/main.py @@ -1,16 +1,24 @@ import os from typing import Dict, List -from uagents_adapter import SingleA2AAdapter, A2AAgentConfig, a2a_servers # Import from your fixed adapter -from competitor_analysis_executor import CompetitorAnalysisExecutor # NEW: Import the new executor +from uagents_adapter import ( + SingleA2AAdapter, + A2AAgentConfig, + a2a_servers, +) # Import from your fixed adapter +from competitor_analysis_executor import ( + CompetitorAnalysisExecutor, +) # NEW: Import the new executor from dotenv import load_dotenv # Load environment variables from .env file load_dotenv() + class CompetitorAnalysisSystem: """ Manages the setup and execution of the A2A Competitor Analysis agent. """ + def __init__(self): self.coordinator = None self.agent_configs: List[A2AAgentConfig] = [] @@ -26,17 +34,21 @@ def setup_agents(self): A2AAgentConfig( name="competitor_analysis_specialist", description="AI Agent for comprehensive competitor analysis and market intelligence.", - url="http://localhost:10020", # The URL where the A2A server for this agent will run - port=10020, # The port for the A2A server + url="http://localhost:10020", # The URL where the A2A server for this agent will run + port=10020, # The port for the A2A server specialties=[ - "competitor analysis", "market intelligence", "SWOT analysis", - "strategic recommendations", "industry research", "product comparison" + "competitor analysis", + "market intelligence", + "SWOT analysis", + "strategic recommendations", + "industry research", + "product comparison", ], - priority=3 + priority=3, ) ] self.executors = { - "competitor_analysis_specialist": CompetitorAnalysisExecutor() # NEW: Use the new executor + "competitor_analysis_specialist": CompetitorAnalysisExecutor() # NEW: Use the new executor } print("✅ Competitor Analysis Agent configuration created") @@ -53,19 +65,21 @@ def create_coordinator(self): Creates the SingleA2AAdapter (uAgent coordinator) for the competitor analysis agent. """ print("🤖 Creating Competitor Analysis Coordinator...") - + # Get the executor instance competitor_executor = self.executors.get("competitor_analysis_specialist") if competitor_executor is None: - raise ValueError("CompetitorAnalysisExecutor not found in executors dictionary.") + raise ValueError( + "CompetitorAnalysisExecutor not found in executors dictionary." + ) self.coordinator = SingleA2AAdapter( agent_executor=competitor_executor, name="competitor-analysis-coordinator", description="Coordinator for routing competitor analysis queries to the specialist agent.", - port=8200, # The port for the uAgent coordinator + port=8200, # The port for the uAgent coordinator mailbox=True, - timeout=2000 + timeout=2000, ) print("✅ Competitor Analysis Coordinator created!") return self.coordinator @@ -80,7 +94,9 @@ def start_system(self): self.start_individual_a2a_servers() coordinator = self.create_coordinator() self.running = True - print(f"🎯 Starting Competitor Analysis coordinator on port {coordinator.port}...") + print( + f"🎯 Starting Competitor Analysis coordinator on port {coordinator.port}..." + ) coordinator.run() except KeyboardInterrupt: print("👋 Shutting down Competitor Analysis system...") @@ -89,12 +105,15 @@ def start_system(self): print(f"❌ Error during system startup: {e}") self.running = False + def main(): """ Main function to run the Competitor Analysis A2A system. """ # Set the UAGENT_MESSAGE_TIMEOUT environment variable - os.environ["UAGENT_MESSAGE_TIMEOUT"] = os.getenv("UAGENT_MESSAGE_TIMEOUT", "300") # Default to 300 seconds (5 minutes) + os.environ["UAGENT_MESSAGE_TIMEOUT"] = os.getenv( + "UAGENT_MESSAGE_TIMEOUT", "300" + ) # Default to 300 seconds (5 minutes) try: system = CompetitorAnalysisSystem() @@ -104,5 +123,6 @@ def main(): except Exception as e: print(f"❌ An error occurred: {e}") + if __name__ == "__main__": main() diff --git a/a2a-uAgents-Integration/a2a-Outbound-Communication/langgraph/currency.py b/a2a-uAgents-Integration/a2a-Outbound-Communication/langgraph/currency.py index e6af4af1..0ebbebf3 100644 --- a/a2a-uAgents-Integration/a2a-Outbound-Communication/langgraph/currency.py +++ b/a2a-uAgents-Integration/a2a-Outbound-Communication/langgraph/currency.py @@ -1,4 +1,3 @@ -import os import logging from typing import Dict, List from dotenv import load_dotenv @@ -13,6 +12,7 @@ logging.basicConfig(level=logging.INFO) logger = logging.getLogger("CurrencyAgentRunner") + class CurrencyAgent: def __init__(self): self.coordinator = None @@ -29,12 +29,10 @@ def setup_agents(self): url="http://localhost:10000", port=10000, specialties=["currency conversion", "currency exchange", "forex rates"], - priority=2 + priority=2, ) ] - self.executors = { - "currency_specialist": CurrencyAgentExecutor() - } + self.executors = {"currency_specialist": CurrencyAgentExecutor()} logger.info("✅ Agent configuration created") def start_individual_a2a_servers(self): @@ -49,7 +47,7 @@ def create_coordinator(self): name="currency-coordinator", description="Routes queries to the Currency Agent", port=8100, - mailbox=True + mailbox=True, ) logger.info(f"✅ Coordinator created on port {self.coordinator.port}") return self.coordinator @@ -61,7 +59,9 @@ def start_system(self): self.start_individual_a2a_servers() coordinator = self.create_coordinator() self.running = True - logger.info(f"🎯 Starting coordinator on port {coordinator.port}... Press Ctrl+C to stop.") + logger.info( + f"🎯 Starting coordinator on port {coordinator.port}... Press Ctrl+C to stop." + ) coordinator.run() except KeyboardInterrupt: logger.info("👋 System shutdown by user.") @@ -70,6 +70,7 @@ def start_system(self): logger.error(f"❌ Error occurred: {e}") self.running = False + def main(): try: system = CurrencyAgent() @@ -78,5 +79,7 @@ def main(): print("👋 Shutdown complete!") except Exception as e: print(f"❌ Error: {e}") + + if __name__ == "__main__": main() diff --git a/a2a-uAgents-Integration/a2a-Outbound-Communication/langgraph/currency_agent_system/agent.py b/a2a-uAgents-Integration/a2a-Outbound-Communication/langgraph/currency_agent_system/agent.py index f6892c8f..6ed27da4 100644 --- a/a2a-uAgents-Integration/a2a-Outbound-Communication/langgraph/currency_agent_system/agent.py +++ b/a2a-uAgents-Integration/a2a-Outbound-Communication/langgraph/currency_agent_system/agent.py @@ -16,11 +16,12 @@ memory = MemorySaver() + @tool def get_exchange_rate( - currency_from: str = 'USD', - currency_to: str = 'EUR', - currency_date: str = 'latest', + currency_from: str = "USD", + currency_to: str = "EUR", + currency_date: str = "latest", ): """Use this to get current exchange rate. @@ -34,52 +35,55 @@ def get_exchange_rate( """ try: response = httpx.get( - f'https://api.frankfurter.app/{currency_date}', - params={'from': currency_from, 'to': currency_to}, + f"https://api.frankfurter.app/{currency_date}", + params={"from": currency_from, "to": currency_to}, ) response.raise_for_status() data = response.json() - if 'rates' not in data: - return {'error': 'Invalid API response format.'} + if "rates" not in data: + return {"error": "Invalid API response format."} return data except httpx.HTTPError as e: - return {'error': f'API request failed: {e}'} + return {"error": f"API request failed: {e}"} except ValueError: - return {'error': 'Invalid JSON response from API.'} + return {"error": "Invalid JSON response from API."} + class ResponseFormat(BaseModel): """Respond to the user in this format.""" - status: Literal['input_required', 'completed', 'error'] = 'input_required' + + status: Literal["input_required", "completed", "error"] = "input_required" message: str + class CurrencyAgent: """CurrencyAgent - a specialized assistant for currency conversions.""" - + SYSTEM_INSTRUCTION = ( - 'You are a specialized assistant for currency conversions. ' + "You are a specialized assistant for currency conversions. " "Your sole purpose is to use the 'get_exchange_rate' tool to answer questions about currency exchange rates. " - 'If the user asks about anything other than currency conversion or exchange rates, ' - 'politely state that you cannot help with that topic and can only assist with currency-related queries. ' - 'Do not attempt to answer unrelated questions or use tools for other purposes.' + "If the user asks about anything other than currency conversion or exchange rates, " + "politely state that you cannot help with that topic and can only assist with currency-related queries. " + "Do not attempt to answer unrelated questions or use tools for other purposes." ) FORMAT_INSTRUCTION = ( - 'Set response status to input_required if the user needs to provide more information to complete the request.' - 'Set response status to error if there is an error while processing the request.' - 'Set response status to completed if the request is complete.' + "Set response status to input_required if the user needs to provide more information to complete the request." + "Set response status to error if there is an error while processing the request." + "Set response status to completed if the request is complete." ) - SUPPORTED_CONTENT_TYPES = ['text', 'text/plain'] + SUPPORTED_CONTENT_TYPES = ["text", "text/plain"] def __init__(self): - model_source = os.getenv('model_source', 'google') - if model_source == 'google': - self.model = ChatGoogleGenerativeAI(model='gemini-2.0-flash') + model_source = os.getenv("model_source", "google") + if model_source == "google": + self.model = ChatGoogleGenerativeAI(model="gemini-2.0-flash") else: self.model = ChatOpenAI( - model=os.getenv('TOOL_LLM_NAME'), - openai_api_key=os.getenv('API_KEY', 'EMPTY'), - openai_api_base=os.getenv('TOOL_LLM_URL'), + model=os.getenv("TOOL_LLM_NAME"), + openai_api_key=os.getenv("API_KEY", "EMPTY"), + openai_api_base=os.getenv("TOOL_LLM_URL"), temperature=0, ) self.tools = [get_exchange_rate] @@ -92,57 +96,56 @@ def __init__(self): ) async def stream(self, query, context_id) -> AsyncIterable[dict[str, Any]]: - inputs = {'messages': [('user', query)]} - config = {'configurable': {'thread_id': context_id}} + inputs = {"messages": [("user", query)]} + config = {"configurable": {"thread_id": context_id}} - for item in self.graph.stream(inputs, config, stream_mode='values'): - message = item['messages'][-1] + for item in self.graph.stream(inputs, config, stream_mode="values"): + message = item["messages"][-1] if ( isinstance(message, AIMessage) and message.tool_calls and len(message.tool_calls) > 0 ): yield { - 'is_task_complete': False, - 'require_user_input': False, - 'content': 'Looking up the exchange rates...', + "is_task_complete": False, + "require_user_input": False, + "content": "Looking up the exchange rates...", } elif isinstance(message, ToolMessage): yield { - 'is_task_complete': False, - 'require_user_input': False, - 'content': 'Processing the exchange rates...', + "is_task_complete": False, + "require_user_input": False, + "content": "Processing the exchange rates...", } yield self.get_agent_response(config) def get_agent_response(self, config): current_state = self.graph.get_state(config) - structured_response = current_state.values.get('structured_response') + structured_response = current_state.values.get("structured_response") if structured_response and isinstance(structured_response, ResponseFormat): - if structured_response.status == 'input_required': + if structured_response.status == "input_required": return { - 'is_task_complete': False, - 'require_user_input': True, - 'content': structured_response.message, + "is_task_complete": False, + "require_user_input": True, + "content": structured_response.message, } - if structured_response.status == 'error': + if structured_response.status == "error": return { - 'is_task_complete': False, - 'require_user_input': True, - 'content': structured_response.message, + "is_task_complete": False, + "require_user_input": True, + "content": structured_response.message, } - if structured_response.status == 'completed': + if structured_response.status == "completed": return { - 'is_task_complete': True, - 'require_user_input': False, - 'content': structured_response.message, + "is_task_complete": True, + "require_user_input": False, + "content": structured_response.message, } return { - 'is_task_complete': False, - 'require_user_input': True, - 'content': ( - 'We are unable to process your request at the moment. ' - 'Please try again.' + "is_task_complete": False, + "require_user_input": True, + "content": ( + "We are unable to process your request at the moment. Please try again." ), - } \ No newline at end of file + } diff --git a/a2a-uAgents-Integration/a2a-Outbound-Communication/langgraph/currency_agent_system/agent_executor.py b/a2a-uAgents-Integration/a2a-Outbound-Communication/langgraph/currency_agent_system/agent_executor.py index 3f54d1c3..f4cf51cd 100644 --- a/a2a-uAgents-Integration/a2a-Outbound-Communication/langgraph/currency_agent_system/agent_executor.py +++ b/a2a-uAgents-Integration/a2a-Outbound-Communication/langgraph/currency_agent_system/agent_executor.py @@ -2,7 +2,14 @@ from a2a.server.agent_execution import AgentExecutor, RequestContext from a2a.server.events import EventQueue from a2a.server.tasks import TaskUpdater -from a2a.types import InternalError, InvalidParamsError, Part, TaskState, TextPart, UnsupportedOperationError +from a2a.types import ( + InternalError, + InvalidParamsError, + Part, + TaskState, + TextPart, + UnsupportedOperationError, +) from a2a.utils import new_agent_text_message, new_task from a2a.utils.errors import ServerError @@ -11,9 +18,10 @@ logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) + class CurrencyAgentExecutor(AgentExecutor): """Currency Conversion AgentExecutor.""" - + def __init__(self): self.agent = CurrencyAgent() @@ -30,14 +38,14 @@ async def execute(self, context: RequestContext, event_queue: EventQueue) -> Non updater = TaskUpdater(event_queue, task.id, task.contextId) try: async for item in self.agent.stream(query, task.contextId): - is_task_complete = item['is_task_complete'] - require_user_input = item['require_user_input'] + is_task_complete = item["is_task_complete"] + require_user_input = item["require_user_input"] if not is_task_complete and not require_user_input: await updater.update_status( TaskState.working, new_agent_text_message( - item['content'], + item["content"], task.contextId, task.id, ), @@ -46,7 +54,7 @@ async def execute(self, context: RequestContext, event_queue: EventQueue) -> Non await updater.update_status( TaskState.input_required, new_agent_text_message( - item['content'], + item["content"], task.contextId, task.id, ), @@ -55,17 +63,17 @@ async def execute(self, context: RequestContext, event_queue: EventQueue) -> Non break else: await updater.add_artifact( - [Part(root=TextPart(text=item['content']))], - name='conversion_result', + [Part(root=TextPart(text=item["content"]))], + name="conversion_result", ) await updater.complete() break except Exception as e: - logger.error(f'An error occurred while streaming the response: {e}') + logger.error(f"An error occurred while streaming the response: {e}") raise ServerError(error=InternalError()) from e def _validate_request(self, context: RequestContext) -> bool: return False async def cancel(self, context: RequestContext, event_queue: EventQueue) -> None: - raise ServerError(error=UnsupportedOperationError()) \ No newline at end of file + raise ServerError(error=UnsupportedOperationError()) diff --git a/a2a-uAgents-Integration/a2a-Outbound-Communication/shopping_agent/main.py b/a2a-uAgents-Integration/a2a-Outbound-Communication/shopping_agent/main.py index b615e36d..2cc9c91b 100644 --- a/a2a-uAgents-Integration/a2a-Outbound-Communication/shopping_agent/main.py +++ b/a2a-uAgents-Integration/a2a-Outbound-Communication/shopping_agent/main.py @@ -1,11 +1,13 @@ -import os from typing import Dict, List -from uagents_adapter import SingleA2AAdapter, A2AAgentConfig, a2a_servers +from uagents_adapter import SingleA2AAdapter, A2AAgentConfig, a2a_servers from shopping_agent import ShoppingAgentExecutor + + class ShoppingPartnerSystem: """ Manages the setup and execution of the A2A Shopping Partner agent. """ + def __init__(self): self.coordinator = None self.agent_configs: List[A2AAgentConfig] = [] @@ -21,18 +23,21 @@ def setup_agents(self): A2AAgentConfig( name="shopping-partner-specialist", description="AI Agent for product recommendations and shopping assistance.", - url="http://localhost:10020", # The URL where the A2A server for this agent will run - port=10020, # The port for the A2A server + url="http://localhost:10020", # The URL where the A2A server for this agent will run + port=10020, # The port for the A2A server specialties=[ - "product recommendations", "shopping", "e-commerce", - "fashion", "electronics", "home goods", "sports gear" + "product recommendations", + "shopping", + "e-commerce", + "fashion", + "electronics", + "home goods", + "sports gear", ], - priority=3 # Priority can be useful in multi-agent setups + priority=3, # Priority can be useful in multi-agent setups ) ] - self.executors = { - "shopping-partner-specialist": ShoppingAgentExecutor() - } + self.executors = {"shopping-partner-specialist": ShoppingAgentExecutor()} print("✅ Shopping Partner Agent configuration created") def start_individual_a2a_servers(self): @@ -48,7 +53,7 @@ def create_coordinator(self): Creates the SingleA2AAdapter (uAgent coordinator) for the shopping partner. """ print("🤖 Creating Shopping Partner Coordinator...") - + # Get the executor instance shopping_executor = self.executors.get("shopping-partner-specialist") if shopping_executor is None: @@ -58,9 +63,9 @@ def create_coordinator(self): agent_executor=shopping_executor, name="shopping-partner-coordinator", description="Coordinator for routing shopping-related queries to the Shopping Partner Agent.", - port=8200, # The port for the uAgent coordinator + port=8200, # The port for the uAgent coordinator mailbox=True, - timeout=2000 + timeout=2000, ) print("✅ Shopping Partner Coordinator created!") return self.coordinator @@ -75,7 +80,9 @@ def start_system(self): self.start_individual_a2a_servers() coordinator = self.create_coordinator() self.running = True - print(f"🎯 Starting Shopping Partner coordinator on port {coordinator.port}...") + print( + f"🎯 Starting Shopping Partner coordinator on port {coordinator.port}..." + ) coordinator.run() except KeyboardInterrupt: print("👋 Shutting down Shopping Partner system...") @@ -84,6 +91,7 @@ def start_system(self): print(f"❌ Error during system startup: {e}") self.running = False + def main(): """ Main function to run the Shopping Partner A2A system. @@ -96,5 +104,6 @@ def main(): except Exception as e: print(f"❌ An error occurred: {e}") + if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/a2a-uAgents-Integration/a2a-Outbound-Communication/shopping_agent/shopping_agent.py b/a2a-uAgents-Integration/a2a-Outbound-Communication/shopping_agent/shopping_agent.py index fa8baecc..2a87701d 100644 --- a/a2a-uAgents-Integration/a2a-Outbound-Communication/shopping_agent/shopping_agent.py +++ b/a2a-uAgents-Integration/a2a-Outbound-Communication/shopping_agent/shopping_agent.py @@ -1,5 +1,4 @@ import asyncio -from typing import List from a2a.server.agent_execution import AgentExecutor, RequestContext from a2a.server.events import EventQueue from a2a.types import Part, TextPart @@ -38,15 +37,17 @@ "**After listing individual product details, provide a comparative analysis section.**", " - Compare the top 3-5 recommended products based on key criteria (e.g., price, features, rating, best use case).", " - Highlight their similarities and differences to help the user make an informed decision.", - "Format the recommendations neatly and ensure clarity for ease of user understanding, presenting them as a structured report with clear headings and bullet points. Use a table for the comparative analysis if appropriate." + "Format the recommendations neatly and ensure clarity for ease of user understanding, presenting them as a structured report with clear headings and bullet points. Use a table for the comparative analysis if appropriate.", ], tools=[ExaTools()], ) + class ShoppingAgentExecutor(AgentExecutor): """ AgentExecutor wrapper for the agno.agent shopping partner. """ + def __init__(self): self.agent = shopping_partner_agno_agent @@ -65,19 +66,25 @@ async def execute( if isinstance(part.root, TextPart): message_content = part.root.text break - + if not message_content: - await event_queue.enqueue_event(new_agent_text_message("Error: No message content received.")) + await event_queue.enqueue_event( + new_agent_text_message("Error: No message content received.") + ) return message: Message = Message(role="user", content=message_content) logger.info(f"Received message: {message.content}") - + try: logger.info("Starting agno agent run with timeout...") - result: RunOutput = await asyncio.wait_for(self.agent.arun(message), timeout=180) - logger.info(f"Agno agent finished run. Response content type: {type(result.content)}") - + result: RunOutput = await asyncio.wait_for( + self.agent.arun(message), timeout=180 + ) + logger.info( + f"Agno agent finished run. Response content type: {type(result.content)}" + ) + response_text = str(result.content) await event_queue.enqueue_event(new_agent_text_message(response_text)) logger.info("Event enqueued successfully.") @@ -85,12 +92,20 @@ async def execute( except asyncio.TimeoutError: error_message = "Agno agent execution timed out after 180 seconds. The query might be too complex or require more time." logger.error(error_message) - await event_queue.enqueue_event(new_agent_text_message(f"Error: {error_message}. Please try again or simplify your query.")) + await event_queue.enqueue_event( + new_agent_text_message( + f"Error: {error_message}. Please try again or simplify your query." + ) + ) except Exception as e: error_message = f"Error during agno agent execution: {e}" logger.error(error_message, exc_info=True) - await event_queue.enqueue_event(new_agent_text_message(f"Error: {error_message}. Please check logs for details.")) - + await event_queue.enqueue_event( + new_agent_text_message( + f"Error: {error_message}. Please check logs for details." + ) + ) + logger.info("execute method finished.") @override @@ -98,4 +113,4 @@ async def cancel(self, context: RequestContext, event_queue: EventQueue) -> None """ Cancels the agent's execution. """ - raise Exception("Cancel not supported for this agent executor.") \ No newline at end of file + raise Exception("Cancel not supported for this agent executor.") diff --git a/a2a-uAgents-Integration/a2a-Outbound-Communication/youtube_summarizer/agent.py b/a2a-uAgents-Integration/a2a-Outbound-Communication/youtube_summarizer/agent.py index 9043fae4..eae680dd 100644 --- a/a2a-uAgents-Integration/a2a-Outbound-Communication/youtube_summarizer/agent.py +++ b/a2a-uAgents-Integration/a2a-Outbound-Communication/youtube_summarizer/agent.py @@ -16,8 +16,10 @@ # Hardcoded API key (not recommended for production) + class ResponseModel(BaseModel): """Response model for the YouTube Summarizer Agent.""" + text_reply: str closed_captions: str | None status: str = "TERMINATE" @@ -29,7 +31,6 @@ def format(self) -> str: return f"{self.text_reply}\n\nClosed Captions:\n{self.closed_captions}" - def get_api_key() -> str: """Helper method to handle API Key.""" api_key = os.environ.get("OPENAI_API_KEY") @@ -38,74 +39,82 @@ def get_api_key() -> str: raise ValueError("OPENAI_API_KEY is not set") return api_key + class YoutubeSummarizerAgent: """Agent to summarize YouTube videos using transcripts.""" - SUPPORTED_CONTENT_TYPES = ['text', 'text/plain'] + + SUPPORTED_CONTENT_TYPES = ["text", "text/plain"] def __init__(self): try: llm_config = { - "config_list": [{ - "model": "gpt-4o", - "api_key": get_api_key(), - }], + "config_list": [ + { + "model": "gpt-4o", + "api_key": get_api_key(), + } + ], "temperature": 0.7, } self.agent = AssistantAgent( - name='YoutubeSummarizerAgent', + name="YoutubeSummarizerAgent", llm_config=llm_config, system_message=( - 'You are a specialized assistant for summarizing YouTube videos. ' - 'You receive video transcripts and generate concise summaries. ' - 'If a YouTube URL is provided, summarize the video based on its transcript. ' - 'If no URL is provided or the query is unrelated, ' - 'state that you can only summarize YouTube videos.\n\n' - 'Always respond using the ResponseModel format:\n' - '- text_reply: The video summary or response text\n' - '- closed_captions: YouTube captions if available, null if not relevant\n' + "You are a specialized assistant for summarizing YouTube videos. " + "You receive video transcripts and generate concise summaries. " + "If a YouTube URL is provided, summarize the video based on its transcript. " + "If no URL is provided or the query is unrelated, " + "state that you can only summarize YouTube videos.\n\n" + "Always respond using the ResponseModel format:\n" + "- text_reply: The video summary or response text\n" + "- closed_captions: YouTube captions if available, null if not relevant\n" '- status: Always "TERMINATE"\n\n' - 'Example response:\n' - '{\n' + "Example response:\n" + "{\n" ' "text_reply": "Summary of the video...",\n' ' "closed_captions": null,\n' ' "status": "TERMINATE"\n' - '}' + "}" ), ) self.initialized = True - logger.info('YouTube Summarizer Agent initialized successfully') + logger.info("YouTube Summarizer Agent initialized successfully") except Exception as e: - logger.error(f'Failed to initialize agent: {e}') + logger.error(f"Failed to initialize agent: {e}") self.initialized = False def extract_video_id(self, url: str) -> str | None: """Extract video ID from YouTube URL.""" - pattern = r'(?:youtube\.com\/watch\?v=|youtu\.be\/)([a-zA-Z0-9_-]+)' + pattern = r"(?:youtube\.com\/watch\?v=|youtu\.be\/)([a-zA-Z0-9_-]+)" match = re.search(pattern, url) return match.group(1) if match else None async def get_transcript(self, video_id: str) -> str | None: """Fetch transcript for a YouTube video.""" # Try multiple languages - languages = ['en', 'en-US', 'en-GB', 'auto'] - + languages = ["en", "en-US", "en-GB", "auto"] + for lang in languages: try: - transcript = YouTubeTranscriptApi.get_transcript(video_id, languages=[lang]) + transcript = YouTubeTranscriptApi.get_transcript( + video_id, languages=[lang] + ) logger.info(f"Found transcript in language: {lang}") - return ' '.join([entry['text'] for entry in transcript]) + return " ".join([entry["text"] for entry in transcript]) except NoTranscriptFound: - logger.debug(f"No transcript found for video ID: {video_id} in language: {lang}") + logger.debug( + f"No transcript found for video ID: {video_id} in language: {lang}" + ) continue except Exception as e: logger.error(f"Error fetching transcript for language {lang}: {e}") continue - + # Try to get any available transcript try: transcript = YouTubeTranscriptApi.get_transcript(video_id) logger.info("Found transcript using auto-detection") - return ' '.join([entry['text'] for entry in transcript]) + return " ".join([entry["text"] for entry in transcript]) except NoTranscriptFound: logger.warning(f"No transcript found for video ID: {video_id}") return None @@ -121,12 +130,12 @@ async def get_video_info(self, video_id: str) -> dict | None: response = requests.get(url, timeout=10) if response.status_code == 200: # Extract title from HTML (simplified) - title_match = re.search(r'(.*?)', response.text) + title_match = re.search(r"(.*?)", response.text) title = title_match.group(1) if title_match else "Unknown Title" return { - 'title': title.replace(' - YouTube', ''), - 'url': url, - 'video_id': video_id + "title": title.replace(" - YouTube", ""), + "url": url, + "video_id": video_id, } except Exception as e: logger.error(f"Error fetching video info: {e}") @@ -138,42 +147,42 @@ def get_agent_response(self, response: str) -> dict[str, Any]: response_dict = json.loads(response) model = ResponseModel(**response_dict) return { - 'is_task_complete': True, - 'require_user_input': False, - 'content': model.format(), + "is_task_complete": True, + "require_user_input": False, + "content": model.format(), } except Exception as e: - logger.error(f'Error parsing response: {e}, response: {response}') + logger.error(f"Error parsing response: {e}, response: {response}") return { - 'is_task_complete': True, - 'require_user_input': False, - 'content': response, + "is_task_complete": True, + "require_user_input": False, + "content": response, } async def stream(self, query: str, sessionId: str) -> AsyncIterable[dict[str, Any]]: """Stream updates from the summarizer agent.""" if not self.initialized: yield { - 'is_task_complete': False, - 'require_user_input': True, - 'content': 'Agent initialization failed. Please check the dependencies and logs.', + "is_task_complete": False, + "require_user_input": True, + "content": "Agent initialization failed. Please check the dependencies and logs.", } return yield { - 'is_task_complete': False, - 'require_user_input': False, - 'content': 'Processing video summary request...', + "is_task_complete": False, + "require_user_input": False, + "content": "Processing video summary request...", } - logger.info(f'Processing query: {query[:50]}...') + logger.info(f"Processing query: {query[:50]}...") try: video_id = self.extract_video_id(query) if not video_id: response = { - 'text_reply': 'Please provide a valid YouTube video URL.', - 'closed_captions': None, - 'status': 'TERMINATE', + "text_reply": "Please provide a valid YouTube video URL.", + "closed_captions": None, + "status": "TERMINATE", } yield self.get_agent_response(json.dumps(response)) return @@ -184,15 +193,15 @@ async def stream(self, query: str, sessionId: str) -> AsyncIterable[dict[str, An video_info = await self.get_video_info(video_id) if video_info: response = { - 'text_reply': f'No transcript available for "{video_info["title"]}" (ID: {video_id}). This could be because:\n• The video has no captions/subtitles\n• The video is private or restricted\n• The video has been removed\n\nPlease try a different YouTube video with available captions.', - 'closed_captions': None, - 'status': 'TERMINATE', + "text_reply": f'No transcript available for "{video_info["title"]}" (ID: {video_id}). This could be because:\n• The video has no captions/subtitles\n• The video is private or restricted\n• The video has been removed\n\nPlease try a different YouTube video with available captions.', + "closed_captions": None, + "status": "TERMINATE", } else: response = { - 'text_reply': f'No transcript available for this video (ID: {video_id}). This could be because:\n• The video has no captions/subtitles\n• The video is private or restricted\n• The video has been removed\n\nPlease try a different YouTube video with available captions.', - 'closed_captions': None, - 'status': 'TERMINATE', + "text_reply": f"No transcript available for this video (ID: {video_id}). This could be because:\n• The video has no captions/subtitles\n• The video is private or restricted\n• The video has been removed\n\nPlease try a different YouTube video with available captions.", + "closed_captions": None, + "status": "TERMINATE", } yield self.get_agent_response(json.dumps(response)) return @@ -210,22 +219,22 @@ async def stream(self, query: str, sessionId: str) -> AsyncIterable[dict[str, An try: summary = await result.summary response = { - 'text_reply': summary, - 'closed_captions': transcript[:1000], # Limit captions for brevity - 'status': 'TERMINATE', + "text_reply": summary, + "closed_captions": transcript[:1000], # Limit captions for brevity + "status": "TERMINATE", } yield self.get_agent_response(json.dumps(response)) except Exception as e: - logger.error(f'Error extracting response: {e}') + logger.error(f"Error extracting response: {e}") yield { - 'is_task_complete': False, - 'require_user_input': True, - 'content': f'Error processing request: {str(e)}', + "is_task_complete": False, + "require_user_input": True, + "content": f"Error processing request: {str(e)}", } except Exception as e: - logger.error(f'Error in streaming agent: {e}') + logger.error(f"Error in streaming agent: {e}") yield { - 'is_task_complete': False, - 'require_user_input': True, - 'content': f'Error processing request: {str(e)}', - } \ No newline at end of file + "is_task_complete": False, + "require_user_input": True, + "content": f"Error processing request: {str(e)}", + } diff --git a/a2a-uAgents-Integration/a2a-Outbound-Communication/youtube_summarizer/agent_executor.py b/a2a-uAgents-Integration/a2a-Outbound-Communication/youtube_summarizer/agent_executor.py index c73514e5..70f7eb41 100644 --- a/a2a-uAgents-Integration/a2a-Outbound-Communication/youtube_summarizer/agent_executor.py +++ b/a2a-uAgents-Integration/a2a-Outbound-Communication/youtube_summarizer/agent_executor.py @@ -13,8 +13,10 @@ logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) + class SummarizerAgentExecutor(AgentExecutor): """A YouTube Summarizer agent executor.""" + def __init__(self): self.agent = YoutubeSummarizerAgent() @@ -24,24 +26,26 @@ async def execute(self, context: RequestContext, event_queue: EventQueue) -> Non if not task and context.message: task = new_task(context.message) await event_queue.enqueue_event(task) - + if not task: logger.error("No task available for execution") return async for item in self.agent.stream(query, task.contextId): - is_task_complete = item['is_task_complete'] - require_user_input = item['require_user_input'] - content = item['content'] + is_task_complete = item["is_task_complete"] + require_user_input = item["require_user_input"] + content = item["content"] logger.info( - f'Stream item received: complete={is_task_complete}, require_input={require_user_input}, content_len={len(content)}' + f"Stream item received: complete={is_task_complete}, require_input={require_user_input}, content_len={len(content)}" ) if not is_task_complete and not require_user_input: await event_queue.enqueue_event( TaskStatusUpdateEvent( status=TaskStatus( state=TaskState.working, - message=new_agent_text_message(content, task.contextId, task.id), + message=new_agent_text_message( + content, task.contextId, task.id + ), ), final=False, contextId=task.contextId, @@ -53,7 +57,9 @@ async def execute(self, context: RequestContext, event_queue: EventQueue) -> Non TaskStatusUpdateEvent( status=TaskStatus( state=TaskState.input_required, - message=new_agent_text_message(content, task.contextId, task.id), + message=new_agent_text_message( + content, task.contextId, task.id + ), ), final=True, contextId=task.contextId, @@ -68,8 +74,8 @@ async def execute(self, context: RequestContext, event_queue: EventQueue) -> Non taskId=task.id, lastChunk=True, artifact=new_text_artifact( - name='video_summary', - description='Summary of the YouTube video.', + name="video_summary", + description="Summary of the YouTube video.", text=content, ), ) @@ -84,4 +90,4 @@ async def execute(self, context: RequestContext, event_queue: EventQueue) -> Non ) async def cancel(self, context: RequestContext, event_queue: EventQueue) -> None: - raise Exception('cancel not supported') \ No newline at end of file + raise Exception("cancel not supported") diff --git a/a2a-uAgents-Integration/a2a-Outbound-Communication/youtube_summarizer/main.py b/a2a-uAgents-Integration/a2a-Outbound-Communication/youtube_summarizer/main.py index dc3f305b..6793961c 100644 --- a/a2a-uAgents-Integration/a2a-Outbound-Communication/youtube_summarizer/main.py +++ b/a2a-uAgents-Integration/a2a-Outbound-Communication/youtube_summarizer/main.py @@ -6,6 +6,7 @@ logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) + class YoutubeSummarizerSystem: def __init__(self): self.coordinator = None @@ -21,7 +22,12 @@ def setup_agents(self): description="AI Agent for summarizing YouTube videos using closed captions", url="http://localhost:10030", port=10030, - specialties=["youtube", "video summarization", "transcription", "content analysis"], + specialties=[ + "youtube", + "video summarization", + "transcription", + "content analysis", + ], priority=3, examples=[ "Summarize this YouTube video: https://www.youtube.com/watch?v=dQw4w9WgXcQ", @@ -42,7 +48,9 @@ def create_coordinator(self): logger.info("🤖 Creating YouTube Summarizer Coordinator...") executor = self.executors.get("youtube-summarizer-specialist") if executor is None: - raise ValueError("SummarizerAgentExecutor not found in executors dictionary.") + raise ValueError( + "SummarizerAgentExecutor not found in executors dictionary." + ) self.coordinator = SingleA2AAdapter( agent_executor=executor, name="youtube-summarizer-coordinator", @@ -59,7 +67,9 @@ def start_system(self): self.start_individual_a2a_servers() coordinator = self.create_coordinator() self.running = True - logger.info(f"🎯 Starting YouTube Summarizer coordinator on port {coordinator.port}...") + logger.info( + f"🎯 Starting YouTube Summarizer coordinator on port {coordinator.port}..." + ) coordinator.run() except KeyboardInterrupt: logger.info("👋 Shutting down YouTube Summarizer system...") @@ -68,6 +78,7 @@ def start_system(self): logger.error(f"❌ Error: {e}") self.running = False + if __name__ == "__main__": system = YoutubeSummarizerSystem() - system.start_system() \ No newline at end of file + system.start_system() diff --git a/anthropic-quickstart/01-basic-claude-agent/basic_claude_agent.py b/anthropic-quickstart/01-basic-claude-agent/basic_claude_agent.py index 74e0e7ac..da53cf02 100644 --- a/anthropic-quickstart/01-basic-claude-agent/basic_claude_agent.py +++ b/anthropic-quickstart/01-basic-claude-agent/basic_claude_agent.py @@ -20,14 +20,14 @@ ChatMessage, ChatAcknowledgement, TextContent, - chat_protocol_spec + chat_protocol_spec, ) # Load environment variables load_dotenv() # Configure Anthropic Claude -anthropic_api_key = os.getenv('ANTHROPIC_API_KEY') +anthropic_api_key = os.getenv("ANTHROPIC_API_KEY") if not anthropic_api_key: raise ValueError("ANTHROPIC_API_KEY not found in environment variables") @@ -35,7 +35,7 @@ client = Anthropic(api_key=anthropic_api_key) # Model configuration -MODEL_NAME = 'claude-3-5-sonnet-20241022' # Latest Claude 3.5 Sonnet +MODEL_NAME = "claude-3-5-sonnet-20241022" # Latest Claude 3.5 Sonnet MAX_TOKENS = 1024 TEMPERATURE = 0.7 @@ -44,7 +44,7 @@ name="claude_assistant", seed="claude-basic-seed-phrase-12345", # Change this for your agent port=8000, - mailbox=True # Required for Agentverse deployment + mailbox=True, # Required for Agentverse deployment ) # Initialize chat protocol @@ -76,7 +76,7 @@ def create_text_chat(text: str) -> ChatMessage: return ChatMessage( timestamp=datetime.now(timezone.utc), msg_id=uuid4(), - content=[TextContent(text=text, type="text")] + content=[TextContent(text=text, type="text")], ) @@ -85,12 +85,12 @@ async def startup(ctx: Context): """Initialize agent on startup""" ctx.logger.info("🤖 Starting Claude Assistant...") ctx.logger.info(f"📍 Agent address: {agent.address}") - + if anthropic_api_key: ctx.logger.info("✅ Anthropic Claude API configured") else: ctx.logger.error("❌ Anthropic API key not set") - + # Initialize conversation storage ctx.storage.set("total_messages", 0) ctx.storage.set("conversations", {}) @@ -99,7 +99,7 @@ async def startup(ctx: Context): @chat_proto.on_message(ChatMessage) async def handle_chat_message(ctx: Context, sender: str, msg: ChatMessage): """Handle incoming chat messages""" - + try: # Extract text from message content user_text = "" @@ -107,78 +107,74 @@ async def handle_chat_message(ctx: Context, sender: str, msg: ChatMessage): if isinstance(item, TextContent): user_text = item.text break - + if not user_text: ctx.logger.warning("No text content in message") return - + # Log incoming message ctx.logger.info(f"📨 Message from {sender}: {user_text[:50]}...") - + # Send acknowledgement - await ctx.send(sender, ChatAcknowledgement( - timestamp=datetime.now(timezone.utc), - acknowledged_msg_id=msg.msg_id - )) - + await ctx.send( + sender, + ChatAcknowledgement( + timestamp=datetime.now(timezone.utc), acknowledged_msg_id=msg.msg_id + ), + ) + # Get conversation history for context conversations = ctx.storage.get("conversations") or {} history = conversations.get(sender, []) - + # Build messages array for Claude API messages = [] - + # Add conversation history (last 5 exchanges for context) if history: for h in history[-10:]: # Last 10 messages (5 exchanges) - messages.append({ - "role": h['role'], - "content": h['text'] - }) - + messages.append({"role": h["role"], "content": h["text"]}) + # Add current user message - messages.append({ - "role": "user", - "content": user_text - }) - + messages.append({"role": "user", "content": user_text}) + # Generate response from Claude ctx.logger.info("🤔 Generating response with Claude...") - + response = client.messages.create( model=MODEL_NAME, max_tokens=MAX_TOKENS, temperature=TEMPERATURE, system=SYSTEM_PROMPT, - messages=messages + messages=messages, ) - + # Extract response text response_text = response.content[0].text - + ctx.logger.info(f"✅ Response generated: {response_text[:50]}...") - + # Update conversation history - history.append({'role': 'user', 'text': user_text}) - history.append({'role': 'assistant', 'text': response_text}) + history.append({"role": "user", "text": user_text}) + history.append({"role": "assistant", "text": response_text}) conversations[sender] = history[-10:] # Keep last 10 messages ctx.storage.set("conversations", conversations) - + # Track stats total = ctx.storage.get("total_messages") or 0 ctx.storage.set("total_messages", total + 1) - + # Send response back to user await ctx.send(sender, create_text_chat(response_text)) - + ctx.logger.info(f"💬 Response sent to {sender}") - + except Exception as e: ctx.logger.error(f"❌ Error processing message: {e}") - + # Check for specific error types error_str = str(e) - + if "rate_limit" in error_str.lower() or "429" in error_str: error_msg = """⚠️ **Rate Limit Reached** @@ -208,7 +204,7 @@ async def handle_chat_message(ctx: Context, sender: str, msg: ChatMessage): - Simplifying your request - Waiting a moment and trying again """ - + await ctx.send(sender, create_text_chat(error_msg)) @@ -225,7 +221,7 @@ async def handle_acknowledgement(ctx: Context, sender: str, msg: ChatAcknowledge if __name__ == "__main__": print("🤖 Starting Claude Assistant...") print(f"📍 Agent address: {agent.address}") - + if anthropic_api_key: print("✅ Anthropic Claude API configured") print(f" Using model: {MODEL_NAME}") @@ -234,15 +230,17 @@ async def handle_acknowledgement(ctx: Context, sender: str, msg: ChatAcknowledge print(" Please add it to your .env file") print(" Get your key from: https://console.anthropic.com") exit(1) - + print("\n🎯 Agent Features:") print(" • Conversational AI with Claude 3.5 Sonnet") print(" • Advanced reasoning and analysis") print(" • Context-aware responses") print(" • Conversation history tracking") print(" • Ready for Agentverse deployment") - - print("\n✅ Agent is running! Connect via ASI One or send messages programmatically.") + + print( + "\n✅ Agent is running! Connect via ASI One or send messages programmatically." + ) print(" Press Ctrl+C to stop.\n") - + agent.run() diff --git a/anthropic-quickstart/02-claude-vision-agent/claude_vision_agent.py b/anthropic-quickstart/02-claude-vision-agent/claude_vision_agent.py index 1719cc3c..ef2cd336 100644 --- a/anthropic-quickstart/02-claude-vision-agent/claude_vision_agent.py +++ b/anthropic-quickstart/02-claude-vision-agent/claude_vision_agent.py @@ -24,14 +24,14 @@ ChatAcknowledgement, TextContent, ResourceContent, - chat_protocol_spec + chat_protocol_spec, ) # Load environment variables load_dotenv() # Configure Anthropic Claude -anthropic_api_key = os.getenv('ANTHROPIC_API_KEY') +anthropic_api_key = os.getenv("ANTHROPIC_API_KEY") if not anthropic_api_key: raise ValueError("ANTHROPIC_API_KEY not found in environment variables") @@ -39,7 +39,7 @@ client = Anthropic(api_key=anthropic_api_key) # Model configuration - Claude 3.5 Sonnet supports vision -MODEL_NAME = 'claude-3-5-sonnet-20241022' # Latest stable version +MODEL_NAME = "claude-3-5-sonnet-20241022" # Latest stable version MAX_TOKENS = 2048 # Longer responses for detailed image analysis TEMPERATURE = 0.7 @@ -48,7 +48,7 @@ name="claude_vision", seed="claude-vision-seed-phrase-12345", # Change this for your agent port=8002, - mailbox=True # Required for Agentverse deployment + mailbox=True, # Required for Agentverse deployment ) # Initialize chat protocol @@ -80,22 +80,22 @@ def create_text_chat(text: str) -> ChatMessage: return ChatMessage( timestamp=datetime.now(timezone.utc), msg_id=uuid4(), - content=[TextContent(text=text, type="text")] + content=[TextContent(text=text, type="text")], ) async def download_image_from_uri(uri: str, ctx: Context) -> bytes: """Download image from URI and return bytes""" try: - if uri.startswith('http://') or uri.startswith('https://'): + if uri.startswith("http://") or uri.startswith("https://"): # Direct HTTP/HTTPS URL with proper headers headers = { - 'User-Agent': 'Mozilla/5.0 (compatible; ClaudeVisionBot/1.0; +https://fetch.ai)' + "User-Agent": "Mozilla/5.0 (compatible; ClaudeVisionBot/1.0; +https://fetch.ai)" } response = requests.get(uri, timeout=10, headers=headers) response.raise_for_status() return response.content - elif uri.startswith('agent-storage://'): + elif uri.startswith("agent-storage://"): # Agentverse storage - would need proper implementation ctx.logger.warning(f"Agent storage URI not fully implemented: {uri}") # For now, return None - in production, implement proper storage access @@ -110,19 +110,19 @@ async def download_image_from_uri(uri: str, ctx: Context) -> bytes: def image_to_base64(image_bytes: bytes) -> str: """Convert image bytes to base64 string""" - return base64.b64encode(image_bytes).decode('utf-8') + return base64.b64encode(image_bytes).decode("utf-8") def get_image_media_type(image_bytes: bytes) -> str: """Detect image media type from bytes""" # Check magic numbers for common formats - if image_bytes.startswith(b'\xff\xd8\xff'): + if image_bytes.startswith(b"\xff\xd8\xff"): return "image/jpeg" - elif image_bytes.startswith(b'\x89PNG\r\n\x1a\n'): + elif image_bytes.startswith(b"\x89PNG\r\n\x1a\n"): return "image/png" - elif image_bytes.startswith(b'GIF87a') or image_bytes.startswith(b'GIF89a'): + elif image_bytes.startswith(b"GIF87a") or image_bytes.startswith(b"GIF89a"): return "image/gif" - elif image_bytes.startswith(b'RIFF') and b'WEBP' in image_bytes[:12]: + elif image_bytes.startswith(b"RIFF") and b"WEBP" in image_bytes[:12]: return "image/webp" else: return "image/jpeg" # Default fallback @@ -131,7 +131,7 @@ def get_image_media_type(image_bytes: bytes) -> str: def extract_image_urls(text: str) -> list[str]: """Extract image URLs from text""" # Pattern to match common image URLs - url_pattern = r'https?://[^\s]+\.(?:jpg|jpeg|png|gif|webp|bmp)' + url_pattern = r"https?://[^\s]+\.(?:jpg|jpeg|png|gif|webp|bmp)" urls = re.findall(url_pattern, text, re.IGNORECASE) return urls @@ -141,12 +141,12 @@ async def startup(ctx: Context): """Initialize agent on startup""" ctx.logger.info("👁️ Starting Claude Vision Agent...") ctx.logger.info(f"📍 Agent address: {agent.address}") - + if anthropic_api_key: ctx.logger.info("✅ Claude Vision API configured") else: ctx.logger.error("❌ Anthropic API key not set") - + # Initialize storage ctx.storage.set("total_messages", 0) ctx.storage.set("total_images", 0) @@ -155,30 +155,36 @@ async def startup(ctx: Context): @chat_proto.on_message(ChatMessage) async def handle_chat_message(ctx: Context, sender: str, msg: ChatMessage): """Handle incoming chat messages with optional images""" - + try: # Extract text and images from message user_text = "" images = [] - + for item in msg.content: if isinstance(item, TextContent): user_text = item.text elif isinstance(item, ResourceContent): # Handle image resources ctx.logger.info(f"📸 Received image resource: {item.resource_id}") - + # Try to download the image # Handle both single resource and list of resources - resources = item.resource if isinstance(item.resource, list) else [item.resource] - + resources = ( + item.resource + if isinstance(item.resource, list) + else [item.resource] + ) + for resource in resources: - if resource and hasattr(resource, 'uri') and resource.uri: + if resource and hasattr(resource, "uri") and resource.uri: image_bytes = await download_image_from_uri(resource.uri, ctx) if image_bytes: images.append(image_bytes) - ctx.logger.info(f"✅ Downloaded image ({len(image_bytes)} bytes)") - + ctx.logger.info( + f"✅ Downloaded image ({len(image_bytes)} bytes)" + ) + # Extract image URLs from text if present if user_text: image_urls = extract_image_urls(user_text) @@ -187,90 +193,91 @@ async def handle_chat_message(ctx: Context, sender: str, msg: ChatMessage): image_bytes = await download_image_from_uri(url, ctx) if image_bytes: images.append(image_bytes) - ctx.logger.info(f"✅ Downloaded image from URL ({len(image_bytes)} bytes)") - + ctx.logger.info( + f"✅ Downloaded image from URL ({len(image_bytes)} bytes)" + ) + # Default prompt if no text provided if not user_text and images: user_text = "What do you see in this image? Provide a detailed description." - + if not user_text and not images: ctx.logger.warning("No text or images in message") return - + # Log incoming message ctx.logger.info(f"📨 Message from {sender}: {user_text[:50]}...") if images: ctx.logger.info(f"📸 With {len(images)} image(s)") - + # Send acknowledgement - await ctx.send(sender, ChatAcknowledgement( - timestamp=datetime.now(timezone.utc), - acknowledged_msg_id=msg.msg_id - )) - + await ctx.send( + sender, + ChatAcknowledgement( + timestamp=datetime.now(timezone.utc), acknowledged_msg_id=msg.msg_id + ), + ) + # Build messages array for Claude API message_content = [] - + # Add images first (Claude prefers images before text) for img_bytes in images: img_base64 = image_to_base64(img_bytes) media_type = get_image_media_type(img_bytes) - - message_content.append({ - "type": "image", - "source": { - "type": "base64", - "media_type": media_type, - "data": img_base64 + + message_content.append( + { + "type": "image", + "source": { + "type": "base64", + "media_type": media_type, + "data": img_base64, + }, } - }) - + ) + # Add text - message_content.append({ - "type": "text", - "text": user_text - }) - + message_content.append({"type": "text", "text": user_text}) + # Generate response from Claude ctx.logger.info("🤔 Analyzing with Claude Vision...") - + response = client.messages.create( model=MODEL_NAME, max_tokens=MAX_TOKENS, temperature=TEMPERATURE, system=SYSTEM_PROMPT, - messages=[{ - "role": "user", - "content": message_content - }] + messages=[{"role": "user", "content": message_content}], ) - + # Extract response text response_text = response.content[0].text - + ctx.logger.info(f"✅ Response generated: {response_text[:50]}...") - + # Track stats total_msgs = ctx.storage.get("total_messages") or 0 ctx.storage.set("total_messages", total_msgs + 1) - + if images: total_imgs = ctx.storage.get("total_images") or 0 ctx.storage.set("total_images", total_imgs + len(images)) - + # Send response back to user await ctx.send(sender, create_text_chat(response_text)) - + ctx.logger.info(f"💬 Response sent to {sender}") - + except Exception as e: ctx.logger.error(f"❌ Error processing message: {e}") import traceback + ctx.logger.error(traceback.format_exc()) - + # Check for specific error types error_str = str(e) - + if "rate_limit" in error_str.lower() or "429" in error_str: error_msg = """⚠️ **Rate Limit Reached** @@ -311,7 +318,7 @@ async def handle_chat_message(ctx: Context, sender: str, msg: ChatMessage): - Sending a different image - Waiting a moment and trying again """ - + await ctx.send(sender, create_text_chat(error_msg)) @@ -328,7 +335,7 @@ async def handle_acknowledgement(ctx: Context, sender: str, msg: ChatAcknowledge if __name__ == "__main__": print("👁️ Starting Claude Vision Agent...") print(f"📍 Agent address: {agent.address}") - + if anthropic_api_key: print("✅ Claude Vision API configured") print(f" Using model: {MODEL_NAME}") @@ -337,7 +344,7 @@ async def handle_acknowledgement(ctx: Context, sender: str, msg: ChatAcknowledge print(" Please add it to your .env file") print(" Get your key from: https://console.anthropic.com") exit(1) - + print("\n🎯 Agent Features:") print(" • Image analysis with Claude 3.5 Sonnet Vision") print(" • Detailed scene descriptions") @@ -345,13 +352,13 @@ async def handle_acknowledgement(ctx: Context, sender: str, msg: ChatAcknowledge print(" • Object identification") print(" • Visual Q&A") print(" • Multiple image input methods") - + print("\n📸 Supported Image Formats:") print(" • JPEG, PNG, WebP, GIF") print(" • Max size: 5MB per image") print(" • URLs and base64 encoding") - + print("\n✅ Agent is running! Send images via ASI One to analyze them.") print(" Press Ctrl+C to stop.\n") - + agent.run() diff --git a/anthropic-quickstart/03-function-calling-agent/claude_function_agent.py b/anthropic-quickstart/03-function-calling-agent/claude_function_agent.py index 1314f207..d9d6cf7f 100644 --- a/anthropic-quickstart/03-function-calling-agent/claude_function_agent.py +++ b/anthropic-quickstart/03-function-calling-agent/claude_function_agent.py @@ -12,7 +12,6 @@ import os import json -import requests from datetime import datetime, timezone from uuid import uuid4 from dotenv import load_dotenv @@ -23,14 +22,14 @@ ChatMessage, ChatAcknowledgement, TextContent, - chat_protocol_spec + chat_protocol_spec, ) # Load environment variables load_dotenv() # Configure Anthropic Claude -anthropic_api_key = os.getenv('ANTHROPIC_API_KEY') +anthropic_api_key = os.getenv("ANTHROPIC_API_KEY") if not anthropic_api_key: raise ValueError("ANTHROPIC_API_KEY not found in environment variables") @@ -38,7 +37,7 @@ client = Anthropic(api_key=anthropic_api_key) # Model configuration -MODEL_NAME = 'claude-3-5-sonnet-20241022' +MODEL_NAME = "claude-3-5-sonnet-20241022" MAX_TOKENS = 2048 TEMPERATURE = 0.7 @@ -47,7 +46,7 @@ name="claude_functions", seed="claude-functions-seed-phrase-12345", # Change this for your agent port=8003, - mailbox=True # Required for Agentverse deployment + mailbox=True, # Required for Agentverse deployment ) # Initialize chat protocol @@ -77,17 +76,17 @@ "properties": { "city": { "type": "string", - "description": "The city name, e.g., 'San Francisco', 'London', 'Tokyo'" + "description": "The city name, e.g., 'San Francisco', 'London', 'Tokyo'", }, "units": { "type": "string", "enum": ["celsius", "fahrenheit"], "description": "Temperature units (celsius or fahrenheit)", - "default": "fahrenheit" - } + "default": "fahrenheit", + }, }, - "required": ["city"] - } + "required": ["city"], + }, }, { "name": "calculate", @@ -97,11 +96,11 @@ "properties": { "expression": { "type": "string", - "description": "The mathematical expression to evaluate, e.g., '2 + 2', '10 * 5', 'sqrt(16)', '2 ** 8'" + "description": "The mathematical expression to evaluate, e.g., '2 + 2', '10 * 5', 'sqrt(16)', '2 ** 8'", } }, - "required": ["expression"] - } + "required": ["expression"], + }, }, { "name": "get_current_time", @@ -112,11 +111,11 @@ "timezone": { "type": "string", "description": "Timezone name (e.g., 'America/New_York', 'Europe/London', 'Asia/Tokyo') or 'UTC'", - "default": "UTC" + "default": "UTC", } }, - "required": [] - } + "required": [], + }, }, { "name": "search_web", @@ -124,54 +123,57 @@ "input_schema": { "type": "object", "properties": { - "query": { - "type": "string", - "description": "The search query" - }, + "query": {"type": "string", "description": "The search query"}, "num_results": { "type": "integer", "description": "Number of results to return (1-10)", - "default": 5 - } + "default": 5, + }, }, - "required": ["query"] - } - } + "required": ["query"], + }, + }, ] # ========== TOOL IMPLEMENTATIONS ========== + def get_weather(city: str, units: str = "fahrenheit") -> dict: """Get weather for a city (mock implementation - replace with real API)""" # In production, use a real weather API like OpenWeatherMap # For demo purposes, returning mock data - + mock_weather = { - "san francisco": {"temp": 65, "condition": "Partly Cloudy", "humidity": 70, "wind": 12}, + "san francisco": { + "temp": 65, + "condition": "Partly Cloudy", + "humidity": 70, + "wind": 12, + }, "london": {"temp": 55, "condition": "Rainy", "humidity": 85, "wind": 15}, "tokyo": {"temp": 72, "condition": "Clear", "humidity": 60, "wind": 8}, "new york": {"temp": 58, "condition": "Sunny", "humidity": 65, "wind": 10}, - "default": {"temp": 70, "condition": "Clear", "humidity": 50, "wind": 5} + "default": {"temp": 70, "condition": "Clear", "humidity": 50, "wind": 5}, } - + city_lower = city.lower() weather = mock_weather.get(city_lower, mock_weather["default"]) - + # Convert to celsius if needed temp = weather["temp"] if units == "celsius": - temp = round((temp - 32) * 5/9, 1) + temp = round((temp - 32) * 5 / 9, 1) unit_symbol = "°C" else: unit_symbol = "°F" - + return { "city": city, "temperature": f"{temp}{unit_symbol}", "condition": weather["condition"], "humidity": f"{weather['humidity']}%", - "wind_speed": f"{weather['wind']} mph" + "wind_speed": f"{weather['wind']} mph", } @@ -180,37 +182,29 @@ def calculate(expression: str) -> dict: try: # Import math for functions like sqrt, sin, cos, etc. import math - + # Safe evaluation namespace (no exec, eval of arbitrary code) safe_dict = { - 'sqrt': math.sqrt, - 'sin': math.sin, - 'cos': math.cos, - 'tan': math.tan, - 'log': math.log, - 'log10': math.log10, - 'exp': math.exp, - 'pi': math.pi, - 'e': math.e, - 'abs': abs, - 'round': round, - 'pow': pow, + "sqrt": math.sqrt, + "sin": math.sin, + "cos": math.cos, + "tan": math.tan, + "log": math.log, + "log10": math.log10, + "exp": math.exp, + "pi": math.pi, + "e": math.e, + "abs": abs, + "round": round, + "pow": pow, } - + # Evaluate the expression result = eval(expression, {"__builtins__": {}}, safe_dict) - - return { - "expression": expression, - "result": result, - "success": True - } + + return {"expression": expression, "result": result, "success": True} except Exception as e: - return { - "expression": expression, - "error": str(e), - "success": False - } + return {"expression": expression, "error": str(e), "success": False} def get_current_time(timezone: str = "UTC") -> dict: @@ -218,58 +212,54 @@ def get_current_time(timezone: str = "UTC") -> dict: try: from datetime import datetime import pytz - + if timezone == "UTC": tz = pytz.UTC else: tz = pytz.timezone(timezone) - + current_time = datetime.now(tz) - + return { "timezone": timezone, "datetime": current_time.strftime("%Y-%m-%d %H:%M:%S %Z"), "date": current_time.strftime("%Y-%m-%d"), "time": current_time.strftime("%H:%M:%S"), "day_of_week": current_time.strftime("%A"), - "success": True + "success": True, } except Exception as e: - return { - "timezone": timezone, - "error": str(e), - "success": False - } + return {"timezone": timezone, "error": str(e), "success": False} def search_web(query: str, num_results: int = 5) -> dict: """Search the web (mock implementation - replace with real search API)""" # In production, use DuckDuckGo API, Google Custom Search, or similar # For demo purposes, returning mock results - + mock_results = [ { "title": f"Result 1 for '{query}'", "snippet": f"This is a relevant result about {query}. It contains useful information...", - "url": f"https://example.com/result1" + "url": "https://example.com/result1", }, { "title": f"Result 2 for '{query}'", "snippet": f"Another great source discussing {query} in detail...", - "url": f"https://example.com/result2" + "url": "https://example.com/result2", }, { "title": f"Result 3 for '{query}'", "snippet": f"Comprehensive guide to {query} with examples...", - "url": f"https://example.com/result3" - } + "url": "https://example.com/result3", + }, ] - + return { "query": query, "results": mock_results[:num_results], "num_results": min(num_results, len(mock_results)), - "success": True + "success": True, } @@ -278,18 +268,19 @@ def search_web(query: str, num_results: int = 5) -> dict: "get_weather": get_weather, "calculate": calculate, "get_current_time": get_current_time, - "search_web": search_web + "search_web": search_web, } # ========== HELPER FUNCTIONS ========== + def create_text_chat(text: str) -> ChatMessage: """Create a ChatMessage with TextContent""" return ChatMessage( timestamp=datetime.now(timezone.utc), msg_id=uuid4(), - content=[TextContent(text=text, type="text")] + content=[TextContent(text=text, type="text")], ) @@ -297,7 +288,7 @@ def execute_tool(tool_name: str, tool_input: dict) -> str: """Execute a tool and return the result as a string""" if tool_name not in TOOL_FUNCTIONS: return json.dumps({"error": f"Unknown tool: {tool_name}"}) - + try: result = TOOL_FUNCTIONS[tool_name](**tool_input) return json.dumps(result) @@ -307,18 +298,19 @@ def execute_tool(tool_name: str, tool_input: dict) -> str: # ========== AGENT HANDLERS ========== + @agent.on_event("startup") async def startup(ctx: Context): """Initialize agent on startup""" ctx.logger.info("🛠️ Starting Claude Function Calling Agent...") ctx.logger.info(f"📍 Agent address: {agent.address}") - + if anthropic_api_key: ctx.logger.info("✅ Claude Function Calling API configured") ctx.logger.info(f"🔧 Available tools: {', '.join(TOOL_FUNCTIONS.keys())}") else: ctx.logger.error("❌ Anthropic API key not set") - + # Initialize storage ctx.storage.set("total_messages", 0) ctx.storage.set("total_tool_calls", 0) @@ -327,7 +319,7 @@ async def startup(ctx: Context): @chat_proto.on_message(ChatMessage) async def handle_chat_message(ctx: Context, sender: str, msg: ChatMessage): """Handle incoming chat messages with function calling""" - + try: # Extract text from message user_text = "" @@ -335,122 +327,123 @@ async def handle_chat_message(ctx: Context, sender: str, msg: ChatMessage): if isinstance(item, TextContent): user_text = item.text break - + if not user_text: ctx.logger.warning("No text content in message") return - + # Log incoming message ctx.logger.info(f"📨 Message from {sender}: {user_text[:50]}...") - + # Send acknowledgement - await ctx.send(sender, ChatAcknowledgement( - timestamp=datetime.now(timezone.utc), - acknowledged_msg_id=msg.msg_id - )) - + await ctx.send( + sender, + ChatAcknowledgement( + timestamp=datetime.now(timezone.utc), acknowledged_msg_id=msg.msg_id + ), + ) + # Build messages for Claude - messages = [{ - "role": "user", - "content": user_text - }] - + messages = [{"role": "user", "content": user_text}] + # Loop to handle multiple tool calls max_iterations = 5 iteration = 0 - + while iteration < max_iterations: iteration += 1 - + # Call Claude with tools ctx.logger.info(f"🤔 Calling Claude (iteration {iteration})...") - + response = client.messages.create( model=MODEL_NAME, max_tokens=MAX_TOKENS, temperature=TEMPERATURE, system=SYSTEM_PROMPT, tools=TOOLS, - messages=messages + messages=messages, ) - + # Check if Claude wants to use tools if response.stop_reason == "tool_use": # Extract tool use from response - tool_uses = [block for block in response.content if block.type == "tool_use"] - + tool_uses = [ + block for block in response.content if block.type == "tool_use" + ] + ctx.logger.info(f"🔧 Claude wants to use {len(tool_uses)} tool(s)") - + # Add assistant's response to messages - messages.append({ - "role": "assistant", - "content": response.content - }) - + messages.append({"role": "assistant", "content": response.content}) + # Execute each tool tool_results = [] for tool_use in tool_uses: tool_name = tool_use.name tool_input = tool_use.input - + ctx.logger.info(f"⚙️ Executing tool: {tool_name}") - + # Execute the tool result = execute_tool(tool_name, tool_input) - + ctx.logger.info(f"✅ Tool result: {result[:100]}...") - + # Track stats total_calls = ctx.storage.get("total_tool_calls") or 0 ctx.storage.set("total_tool_calls", total_calls + 1) - + # Add tool result - tool_results.append({ - "type": "tool_result", - "tool_use_id": tool_use.id, - "content": result - }) - + tool_results.append( + { + "type": "tool_result", + "tool_use_id": tool_use.id, + "content": result, + } + ) + # Add tool results to messages - messages.append({ - "role": "user", - "content": tool_results - }) - + messages.append({"role": "user", "content": tool_results}) + # Continue the loop to get Claude's final response continue - + else: # Claude provided a final answer response_text = "" for block in response.content: if hasattr(block, "text"): response_text += block.text - + ctx.logger.info(f"✅ Final response: {response_text[:50]}...") - + # Track stats total = ctx.storage.get("total_messages") or 0 ctx.storage.set("total_messages", total + 1) - + # Send response back to user await ctx.send(sender, create_text_chat(response_text)) ctx.logger.info(f"💬 Response sent to {sender}") - + # Exit the loop break - + if iteration >= max_iterations: ctx.logger.warning("⚠️ Max iterations reached") - await ctx.send(sender, create_text_chat( - "I've reached the maximum number of tool calls for this request. Please try breaking it into smaller questions." - )) - + await ctx.send( + sender, + create_text_chat( + "I've reached the maximum number of tool calls for this request. Please try breaking it into smaller questions." + ), + ) + except Exception as e: ctx.logger.error(f"❌ Error processing message: {e}") import traceback + ctx.logger.error(traceback.format_exc()) - + error_msg = f"""❌ **Error Processing Request** {str(e)[:200]} @@ -460,7 +453,7 @@ async def handle_chat_message(ctx: Context, sender: str, msg: ChatMessage): - Breaking complex requests into simpler ones - Waiting a moment and trying again """ - + await ctx.send(sender, create_text_chat(error_msg)) @@ -477,7 +470,7 @@ async def handle_acknowledgement(ctx: Context, sender: str, msg: ChatAcknowledge if __name__ == "__main__": print("🛠️ Starting Claude Function Calling Agent...") print(f"📍 Agent address: {agent.address}") - + if anthropic_api_key: print("✅ Claude Function Calling API configured") print(f" Using model: {MODEL_NAME}") @@ -485,18 +478,18 @@ async def handle_acknowledgement(ctx: Context, sender: str, msg: ChatAcknowledge print("❌ ERROR: ANTHROPIC_API_KEY not set") print(" Please add it to your .env file") exit(1) - + print("\n🔧 Available Tools:") for tool in TOOLS: print(f" • {tool['name']}: {tool['description'][:60]}...") - + print("\n🎯 Example Queries:") print(" • What's the weather in San Francisco?") print(" • Calculate 15 * 23 + 100") print(" • What time is it in Tokyo?") print(" • Search for latest AI news") - + print("\n✅ Agent is running! Send queries via ASI One.") print(" Press Ctrl+C to stop.\n") - + agent.run() diff --git a/anthropic-quickstart/04-mcp-agent/claude_mcp_agent.py b/anthropic-quickstart/04-mcp-agent/claude_mcp_agent.py index bbcfa930..ff968fb4 100644 --- a/anthropic-quickstart/04-mcp-agent/claude_mcp_agent.py +++ b/anthropic-quickstart/04-mcp-agent/claude_mcp_agent.py @@ -10,7 +10,7 @@ - Easy to extend with new MCP servers Architecture: - User (ASI One) + User (ASI One) → Fetch.ai Agent (this code) → Claude (decides which tools to use) → MCP Servers (GitHub, filesystem, etc.) @@ -19,11 +19,10 @@ import os import json -import asyncio from datetime import datetime, timezone from uuid import uuid4 from pathlib import Path -from typing import Dict, List, Any, Optional +from typing import Dict, List, Any from contextlib import AsyncExitStack from dotenv import load_dotenv from anthropic import Anthropic @@ -33,13 +32,14 @@ ChatMessage, ChatAcknowledgement, TextContent, - chat_protocol_spec + chat_protocol_spec, ) # MCP SDK imports try: from mcp import ClientSession, StdioServerParameters from mcp.client.stdio import stdio_client + MCP_AVAILABLE = True except ImportError: MCP_AVAILABLE = False @@ -49,7 +49,7 @@ load_dotenv() # Configure Anthropic Claude -anthropic_api_key = os.getenv('ANTHROPIC_API_KEY') +anthropic_api_key = os.getenv("ANTHROPIC_API_KEY") if not anthropic_api_key: raise ValueError("ANTHROPIC_API_KEY not found in environment variables") @@ -57,7 +57,7 @@ client = Anthropic(api_key=anthropic_api_key) # Model configuration -MODEL_NAME = 'claude-3-5-sonnet-20241022' +MODEL_NAME = "claude-3-5-sonnet-20241022" MAX_TOKENS = 4096 TEMPERATURE = 0.7 @@ -66,7 +66,7 @@ name="claude_mcp", seed="claude-mcp-seed-phrase-12345", # Change this for your agent port=8004, - mailbox=True + mailbox=True, ) # Initialize chat protocol @@ -93,148 +93,143 @@ # ========== MCP SERVER MANAGEMENT ========== + class MCPServerManager: """Manages connections to multiple MCP servers""" - + def __init__(self, config_path: str = "mcp_servers.json"): self.config_path = config_path self.sessions: Dict[str, ClientSession] = {} self.exit_stack = AsyncExitStack() # Proper async context management self.tools_cache: Dict[str, List[Dict]] = {} - + def load_config(self) -> Dict: """Load MCP server configuration from JSON file""" config_file = Path(__file__).parent / self.config_path - + if not config_file.exists(): print(f"⚠️ Config file not found: {config_file}") return {"servers": {}} - - with open(config_file, 'r') as f: + + with open(config_file, "r") as f: config = json.load(f) - + # Substitute environment variables config = self._substitute_env_vars(config) return config - + def _substitute_env_vars(self, config: Dict) -> Dict: """Replace ${VAR_NAME} with environment variable values""" config_str = json.dumps(config) - + # Find all ${VAR_NAME} patterns import re - for match in re.finditer(r'\$\{(\w+)\}', config_str): + + for match in re.finditer(r"\$\{(\w+)\}", config_str): var_name = match.group(1) - var_value = os.getenv(var_name, '') + var_value = os.getenv(var_name, "") config_str = config_str.replace(match.group(0), var_value) - + return json.loads(config_str) - + async def connect_server(self, name: str, server_config: Dict) -> bool: """Connect to a single MCP server""" try: command = server_config.get("command") args = server_config.get("args", []) env = server_config.get("env", {}) - + # Prepare server parameters server_params = StdioServerParameters( - command=command, - args=args, - env=env if env else None + command=command, args=args, env=env if env else None ) - + # Use AsyncExitStack to manage the async context (official MCP pattern) stdio_transport = await self.exit_stack.enter_async_context( stdio_client(server_params) ) read, write = stdio_transport - + # Create and enter session context session = await self.exit_stack.enter_async_context( ClientSession(read, write) ) - + # Initialize the session await session.initialize() - + # Store session self.sessions[name] = session - + print(f"✅ Connected to MCP server: {name}") return True - + except Exception as e: print(f"❌ Failed to connect to {name}: {e}") import traceback + traceback.print_exc() return False - + async def connect_all_servers(self): """Connect to all enabled MCP servers from config""" config = self.load_config() servers = config.get("servers", {}) - + connection_tasks = [] for name, server_config in servers.items(): if server_config.get("enabled", False): print(f"🔌 Connecting to {name}...") task = self.connect_server(name, server_config) connection_tasks.append((name, task)) - + # Connect to all servers concurrently for name, task in connection_tasks: await task - + async def get_all_tools(self) -> List[Dict]: """Get all available tools from all connected MCP servers""" all_tools = [] - + for server_name, session in self.sessions.items(): try: # List tools from this server result = await session.list_tools() - tools = result.tools if hasattr(result, 'tools') else [] - + tools = result.tools if hasattr(result, "tools") else [] + # Add server name to each tool for tracking for tool in tools: tool_dict = { "name": tool.name, "description": tool.description, "input_schema": tool.inputSchema, - "_mcp_server": server_name # Track which server provides this + "_mcp_server": server_name, # Track which server provides this } all_tools.append(tool_dict) - + self.tools_cache[server_name] = tools print(f"📋 Loaded {len(tools)} tools from {server_name}") - + except Exception as e: print(f"⚠️ Error getting tools from {server_name}: {e}") - + return all_tools - + async def call_tool(self, tool_name: str, arguments: Dict) -> Any: """Execute a tool call on the appropriate MCP server""" # Find which server has this tool for server_name, session in self.sessions.items(): tools = self.tools_cache.get(server_name, []) - + if any(t.name == tool_name for t in tools): try: result = await session.call_tool(tool_name, arguments) return result except Exception as e: - return { - "error": str(e), - "tool": tool_name, - "server": server_name - } - - return { - "error": f"Tool '{tool_name}' not found in any connected server" - } - + return {"error": str(e), "tool": tool_name, "server": server_name} + + return {"error": f"Tool '{tool_name}' not found in any connected server"} + async def disconnect_all(self): """Disconnect from all MCP servers""" try: @@ -243,7 +238,7 @@ async def disconnect_all(self): await self.exit_stack.aclose() except Exception as e: print(f"⚠️ Error disconnecting from servers: {e}") - + self.sessions.clear() self.tools_cache.clear() @@ -254,27 +249,28 @@ async def disconnect_all(self): # ========== HELPER FUNCTIONS ========== + def create_text_chat(text: str) -> ChatMessage: """Create a ChatMessage with TextContent""" return ChatMessage( timestamp=datetime.now(timezone.utc), msg_id=uuid4(), - content=[TextContent(text=text, type="text")] + content=[TextContent(text=text, type="text")], ) def mcp_tools_to_claude_format(mcp_tools: List[Dict]) -> List[Dict]: """Convert MCP tool format to Claude's expected format""" claude_tools = [] - + for tool in mcp_tools: claude_tool = { "name": tool["name"], "description": tool["description"], - "input_schema": tool["input_schema"] + "input_schema": tool["input_schema"], } claude_tools.append(claude_tool) - + return claude_tools @@ -291,46 +287,47 @@ def format_tool_result(result: Any) -> str: # ========== AGENT HANDLERS ========== + @agent.on_event("startup") async def startup(ctx: Context): """Initialize agent and connect to MCP servers""" ctx.logger.info("🔌 Starting Claude MCP Agent...") ctx.logger.info(f"📍 Agent address: {agent.address}") - + if not MCP_AVAILABLE: ctx.logger.error("❌ MCP SDK not installed. Run: pip install mcp") return - + if anthropic_api_key: ctx.logger.info("✅ Claude API configured") else: ctx.logger.error("❌ Anthropic API key not set") return - + # Connect to MCP servers ctx.logger.info("🔌 Connecting to MCP servers...") try: await mcp_manager.connect_all_servers() - + # Get available tools mcp_tools = await mcp_manager.get_all_tools() - + if mcp_tools: ctx.logger.info(f"✅ Loaded {len(mcp_tools)} MCP tools") - + # Store tools in context for later use ctx.storage.set("mcp_tools", mcp_tools) - + # Show tool summary for tool in mcp_tools: server = tool.get("_mcp_server", "unknown") ctx.logger.info(f" 🔧 {tool['name']} ({server})") else: ctx.logger.warning("⚠️ No MCP tools available. Check server configuration.") - + except Exception as e: ctx.logger.error(f"❌ Error connecting to MCP servers: {e}") - + # Initialize stats ctx.storage.set("total_messages", 0) ctx.storage.set("total_tool_calls", 0) @@ -346,7 +343,7 @@ async def shutdown(ctx: Context): @chat_proto.on_message(ChatMessage) async def handle_chat_message(ctx: Context, sender: str, msg: ChatMessage): """Handle incoming chat messages with MCP tool support""" - + try: # Extract text from message user_text = "" @@ -354,136 +351,144 @@ async def handle_chat_message(ctx: Context, sender: str, msg: ChatMessage): if isinstance(item, TextContent): user_text = item.text break - + if not user_text: ctx.logger.warning("No text content in message") return - + # Log incoming message ctx.logger.info(f"📨 Message from {sender}: {user_text[:50]}...") - + # Send acknowledgement - await ctx.send(sender, ChatAcknowledgement( - timestamp=datetime.now(timezone.utc), - acknowledged_msg_id=msg.msg_id - )) - + await ctx.send( + sender, + ChatAcknowledgement( + timestamp=datetime.now(timezone.utc), acknowledged_msg_id=msg.msg_id + ), + ) + # Get MCP tools mcp_tools = ctx.storage.get("mcp_tools") or [] - + if not mcp_tools: - await ctx.send(sender, create_text_chat( - "⚠️ No MCP servers connected. Please check the configuration and restart the agent." - )) + await ctx.send( + sender, + create_text_chat( + "⚠️ No MCP servers connected. Please check the configuration and restart the agent." + ), + ) return - + # Convert to Claude format claude_tools = mcp_tools_to_claude_format(mcp_tools) - + # Build messages for Claude - messages = [{ - "role": "user", - "content": user_text - }] - + messages = [{"role": "user", "content": user_text}] + # Loop to handle multiple tool calls max_iterations = 5 iteration = 0 - + while iteration < max_iterations: iteration += 1 - + # Call Claude with MCP tools - ctx.logger.info(f"🤔 Calling Claude with {len(claude_tools)} MCP tools (iteration {iteration})...") - + ctx.logger.info( + f"🤔 Calling Claude with {len(claude_tools)} MCP tools (iteration {iteration})..." + ) + response = client.messages.create( model=MODEL_NAME, max_tokens=MAX_TOKENS, temperature=TEMPERATURE, system=SYSTEM_PROMPT, tools=claude_tools, - messages=messages + messages=messages, ) - + # Check if Claude wants to use tools if response.stop_reason == "tool_use": # Extract tool uses - tool_uses = [block for block in response.content if block.type == "tool_use"] - + tool_uses = [ + block for block in response.content if block.type == "tool_use" + ] + ctx.logger.info(f"🔧 Claude wants to use {len(tool_uses)} MCP tool(s)") - + # Add assistant's response to messages - messages.append({ - "role": "assistant", - "content": response.content - }) - + messages.append({"role": "assistant", "content": response.content}) + # Execute each tool via MCP tool_results = [] for tool_use in tool_uses: tool_name = tool_use.name tool_input = tool_use.input - + ctx.logger.info(f"⚙️ Executing MCP tool: {tool_name}") - ctx.logger.info(f" Input: {json.dumps(tool_input, indent=2)[:100]}...") - + ctx.logger.info( + f" Input: {json.dumps(tool_input, indent=2)[:100]}..." + ) + # Call the MCP tool result = await mcp_manager.call_tool(tool_name, tool_input) - + # Format result result_str = format_tool_result(result) ctx.logger.info(f"✅ Tool result: {result_str[:100]}...") - + # Track stats total_calls = ctx.storage.get("total_tool_calls") or 0 ctx.storage.set("total_tool_calls", total_calls + 1) - + # Add tool result - tool_results.append({ - "type": "tool_result", - "tool_use_id": tool_use.id, - "content": result_str - }) - + tool_results.append( + { + "type": "tool_result", + "tool_use_id": tool_use.id, + "content": result_str, + } + ) + # Add tool results to messages - messages.append({ - "role": "user", - "content": tool_results - }) - + messages.append({"role": "user", "content": tool_results}) + # Continue loop for Claude's final response continue - + else: # Claude provided final answer response_text = "" for block in response.content: if hasattr(block, "text"): response_text += block.text - + ctx.logger.info(f"✅ Final response: {response_text[:50]}...") - + # Track stats total = ctx.storage.get("total_messages") or 0 ctx.storage.set("total_messages", total + 1) - + # Send response await ctx.send(sender, create_text_chat(response_text)) ctx.logger.info(f"💬 Response sent to {sender}") - + break - + if iteration >= max_iterations: ctx.logger.warning("⚠️ Max iterations reached") - await ctx.send(sender, create_text_chat( - "I've reached the maximum number of tool calls. Please try a simpler request." - )) - + await ctx.send( + sender, + create_text_chat( + "I've reached the maximum number of tool calls. Please try a simpler request." + ), + ) + except Exception as e: ctx.logger.error(f"❌ Error processing message: {e}") import traceback + ctx.logger.error(traceback.format_exc()) - + error_msg = f"""❌ **Error Processing Request** {str(e)[:200]} @@ -494,7 +499,7 @@ async def handle_chat_message(ctx: Context, sender: str, msg: ChatMessage): - API rate limits Please try again or check the logs.""" - + await ctx.send(sender, create_text_chat(error_msg)) @@ -511,23 +516,23 @@ async def handle_acknowledgement(ctx: Context, sender: str, msg: ChatAcknowledge if __name__ == "__main__": print("🔌 Starting Claude MCP Agent...") print(f"📍 Agent address: {agent.address}") - + if not MCP_AVAILABLE: print("\n❌ ERROR: MCP SDK not installed") print(" Install with: pip install mcp") exit(1) - + if not anthropic_api_key: print("\n❌ ERROR: ANTHROPIC_API_KEY not set") print(" Please add it to your .env file") exit(1) - + print("\n🎯 This agent connects to MCP servers and uses their tools") print(" Configure servers in: mcp_servers.json") print(" Enable/disable servers by setting 'enabled: true/false'") - + print("\n✅ Agent starting...") print(" MCP servers will be connected on startup") print(" Press Ctrl+C to stop.\n") - + agent.run() diff --git a/anthropic-quickstart/05-multi-agent-system/router_agent.py b/anthropic-quickstart/05-multi-agent-system/router_agent.py index 6001cc5d..af294eed 100644 --- a/anthropic-quickstart/05-multi-agent-system/router_agent.py +++ b/anthropic-quickstart/05-multi-agent-system/router_agent.py @@ -16,7 +16,7 @@ import os from datetime import datetime, timezone from uuid import uuid4 -from typing import Dict, Optional +from typing import Dict from dotenv import load_dotenv from anthropic import Anthropic @@ -26,38 +26,39 @@ ChatAcknowledgement, TextContent, ResourceContent, - chat_protocol_spec + chat_protocol_spec, ) # Load environment variables load_dotenv() # Configure Anthropic Claude -anthropic_api_key = os.getenv('ANTHROPIC_API_KEY') +anthropic_api_key = os.getenv("ANTHROPIC_API_KEY") if not anthropic_api_key: raise ValueError("ANTHROPIC_API_KEY not found in environment variables") client = Anthropic(api_key=anthropic_api_key) # Model configuration -MODEL_NAME = 'claude-3-5-sonnet-20241022' +MODEL_NAME = "claude-3-5-sonnet-20241022" MAX_TOKENS = 1024 TEMPERATURE = 0.7 # Create router agent router = Agent( - name="router", - seed="router-agent-seed-phrase-12345", - port=8005, - mailbox=True + name="router", seed="router-agent-seed-phrase-12345", port=8005, mailbox=True ) # Initialize chat protocol chat_proto = Protocol(spec=chat_protocol_spec) # Agent addresses (UPDATE THESE when you run the specialized agents!) -VISION_AGENT_ADDRESS = "agent1qv7cf6qk25dej5vztevnd3mw5m06xhdxq58ql68lc22drt5wggq9wqxawpp" # Vision agent -MCP_AGENT_ADDRESS = "agent1q0ed0f5czkrn7rdkndcjpkh2rf045nt4mmpc5mfnjlq2gtagq4tyv4cplwd" # MCP agent +VISION_AGENT_ADDRESS = ( + "agent1qv7cf6qk25dej5vztevnd3mw5m06xhdxq58ql68lc22drt5wggq9wqxawpp" # Vision agent +) +MCP_AGENT_ADDRESS = ( + "agent1q0ed0f5czkrn7rdkndcjpkh2rf045nt4mmpc5mfnjlq2gtagq4tyv4cplwd" # MCP agent +) # Track pending requests pending_requests: Dict[str, Dict] = {} @@ -66,6 +67,7 @@ # Custom message model for agent responses class AgentResponse(Model): """Response from a specialized agent""" + request_id: str result: str agent_type: str @@ -76,17 +78,17 @@ def create_text_chat(text: str) -> ChatMessage: return ChatMessage( timestamp=datetime.now(timezone.utc), msg_id=uuid4(), - content=[TextContent(text=text, type="text")] + content=[TextContent(text=text, type="text")], ) async def analyze_request(query: str, has_image: bool) -> str: """Use Claude to determine which agent should handle the request""" - + # If there's an image, definitely use vision agent if has_image: return "vision" - + # Use Claude to classify text requests classification_prompt = f"""You are a router for a multi-agent system. Analyze this user request and determine which agent should handle it. @@ -104,21 +106,18 @@ async def analyze_request(query: str, has_image: bool) -> str: model=MODEL_NAME, max_tokens=10, temperature=0, - messages=[{ - "role": "user", - "content": classification_prompt - }] + messages=[{"role": "user", "content": classification_prompt}], ) - + decision = response.content[0].text.strip().lower() - + # Validate decision if decision not in ["vision", "mcp"]: # Default to mcp for unknown return "mcp" - + return decision - + except Exception as e: print(f"Error classifying request: {e}") # Default to mcp on error @@ -132,7 +131,7 @@ async def startup(ctx: Context): ctx.logger.info(f"📍 Router address: {router.address}") ctx.logger.info(f"👁️ Vision Agent: {VISION_AGENT_ADDRESS}") ctx.logger.info(f"🔧 MCP Agent: {MCP_AGENT_ADDRESS}") - + # Initialize storage ctx.storage.set("total_requests", 0) ctx.storage.set("vision_requests", 0) @@ -142,78 +141,84 @@ async def startup(ctx: Context): @chat_proto.on_message(ChatMessage) async def handle_message(ctx: Context, sender: str, msg: ChatMessage): """Handle incoming messages - either from users or from specialized agents""" - + # Check if this is a response from a specialized agent if sender in [VISION_AGENT_ADDRESS, MCP_AGENT_ADDRESS]: # This is a RESPONSE from a specialized agent - ctx.logger.info(f"📨 Response received from specialized agent ({sender[:12]}...)") - + ctx.logger.info( + f"📨 Response received from specialized agent ({sender[:12]}...)" + ) + # Extract the response text response_text = "" for item in msg.content: if isinstance(item, TextContent): response_text = item.text break - + if not response_text: ctx.logger.warning("No text in agent response") return - + # Find the original request if pending_requests: # Get the most recent request request_id = list(pending_requests.keys())[-1] request_info = pending_requests[request_id] - + original_sender = request_info["original_sender"] - - ctx.logger.info(f"← Sending response back to user ({original_sender[:12]}...)") - + + ctx.logger.info( + f"← Sending response back to user ({original_sender[:12]}...)" + ) + # Send response back to original user await ctx.send(original_sender, create_text_chat(response_text)) - + # Clean up del pending_requests[request_id] - + ctx.logger.info("✅ Request completed!") else: ctx.logger.warning("No pending request found for this response") - + return - + # Otherwise, this is a REQUEST from a user try: # Extract text and check for images user_text = "" has_image = False - + for item in msg.content: if isinstance(item, TextContent): user_text = item.text elif isinstance(item, ResourceContent): has_image = True - + if not user_text and not has_image: ctx.logger.warning("No content in message") return - + ctx.logger.info(f"📨 Request from user ({sender[:12]}...): {user_text[:50]}...") - + # Send acknowledgement to user - await ctx.send(sender, ChatAcknowledgement( - timestamp=datetime.now(timezone.utc), - acknowledged_msg_id=msg.msg_id - )) - + await ctx.send( + sender, + ChatAcknowledgement( + timestamp=datetime.now(timezone.utc), acknowledged_msg_id=msg.msg_id + ), + ) + # Decide which agent to route to target_agent = await analyze_request(user_text, has_image) - + ctx.logger.info(f"🔀 Routing to: {target_agent} agent") - + # Update stats total = ctx.storage.get("total_requests") or 0 ctx.storage.set("total_requests", total + 1) - + if target_agent == "vision": count = ctx.storage.get("vision_requests") or 0 ctx.storage.set("vision_requests", count + 1) @@ -222,27 +227,30 @@ async def handle_message(ctx: Context, sender: str, msg: ChatMessage): count = ctx.storage.get("mcp_requests") or 0 ctx.storage.set("mcp_requests", count + 1) target_address = MCP_AGENT_ADDRESS - + # Create request ID to track this request request_id = str(uuid4()) - + # Store request info pending_requests[request_id] = { "original_sender": sender, "original_message": msg, "target_agent": target_agent, - "timestamp": datetime.now(timezone.utc) + "timestamp": datetime.now(timezone.utc), } - + # Forward message to specialized agent - ctx.logger.info(f"→ Forwarding to {target_agent} agent ({target_address[:12]}...)") + ctx.logger.info( + f"→ Forwarding to {target_agent} agent ({target_address[:12]}...)" + ) await ctx.send(target_address, msg) - + except Exception as e: ctx.logger.error(f"❌ Error routing request: {e}") import traceback + ctx.logger.error(traceback.format_exc()) - + error_msg = f"Sorry, I encountered an error routing your request: {str(e)}" await ctx.send(sender, create_text_chat(error_msg)) @@ -273,5 +281,5 @@ async def handle_acknowledgement(ctx: Context, sender: str, msg: ChatAcknowledge print("✅ Router is running on port 8005") print(" Send requests via ASI One to this router!") print(" Press Ctrl+C to stop.\n") - + router.run() diff --git a/asi-cloud-agent/agent.py b/asi-cloud-agent/agent.py index ee508acf..640bbbe4 100644 --- a/asi-cloud-agent/agent.py +++ b/asi-cloud-agent/agent.py @@ -24,9 +24,7 @@ client = OpenAI( api_key=ASI_CLOUD_API_KEY, - base_url=os.getenv( - "ASICLOUD_BASE_URL", "https://inference.asicloud.cudos.org/v1" - ), + base_url=os.getenv("ASICLOUD_BASE_URL", "https://inference.asicloud.cudos.org/v1"), ) MODEL_NAME = "asi1-mini" @@ -43,7 +41,7 @@ Respect user preferences and mention limitations or safety considerations when relevant. """ -agent = Agent(name="asi_agent",port=8000,mailbox=True) +agent = Agent(name="asi_agent", port=8000, mailbox=True) chat_proto = Protocol(spec=chat_protocol_spec) @@ -131,4 +129,3 @@ async def handle_acknowledgement(ctx: Context, sender: str, msg: ChatAcknowledge if __name__ == "__main__": agent.run() - diff --git a/asi1-llm-example/ASI1_Langchain/asi1_langchain.py b/asi1-llm-example/ASI1_Langchain/asi1_langchain.py index 3a09a9fd..eb3345f1 100644 --- a/asi1-llm-example/ASI1_Langchain/asi1_langchain.py +++ b/asi1-llm-example/ASI1_Langchain/asi1_langchain.py @@ -101,4 +101,4 @@ def custom_search_handler(data): input_data = {"search_query": "What is agentverse?"} output = custom_search_handler(input_data) print("\nFinal Output:") - print(output) \ No newline at end of file + print(output) diff --git a/av-script-example/agent.py b/av-script-example/agent.py index f4f96566..bd634309 100644 --- a/av-script-example/agent.py +++ b/av-script-example/agent.py @@ -32,12 +32,7 @@ AGENT_PORT = int(os.getenv("AGENT_PORT", "8006")) HOSTING_ENDPOINT = os.getenv("HOSTING_ENDPOINT") -agent = Agent( - name=UAGENT_NAME, - seed=SEED_PHRASE, - port=AGENT_PORT, - mailbox=True -) +agent = Agent(name=UAGENT_NAME, seed=SEED_PHRASE, port=AGENT_PORT, mailbox=True) chat_proto = Protocol(spec=chat_protocol_spec) @@ -90,18 +85,18 @@ def call_asi_one_api(user_message: str) -> str: url = "https://api.asi1.ai/v1/chat/completions" headers = { "Content-Type": "application/json", - "Authorization": f"Bearer {ASI_ONE_API_KEY}" + "Authorization": f"Bearer {ASI_ONE_API_KEY}", } data = { "model": "asi1-mini", - "messages": [{"role": "user", "content": user_message}] + "messages": [{"role": "user", "content": user_message}], } - + try: response = requests.post(url, headers=headers, json=data, timeout=30) response.raise_for_status() result = response.json() - + if "choices" in result and len(result["choices"]) > 0: return result["choices"][0]["message"]["content"] return "Sorry, I couldn't generate a response." @@ -115,7 +110,7 @@ def create_text_chat(text: str, end_session: bool = False) -> ChatMessage: content = [TextContent(type="text", text=text)] if end_session: content.append(EndSessionContent()) - + return ChatMessage( timestamp=datetime.utcnow(), msg_id=uuid4(), @@ -126,67 +121,76 @@ def create_text_chat(text: str, end_session: bool = False) -> ChatMessage: @chat_proto.on_message(ChatMessage) async def handle_message(ctx: Context, sender: str, msg: ChatMessage): ctx.logger.info(f"Received message from {sender}") - + await ctx.send( sender, ChatAcknowledgement( - timestamp=datetime.utcnow(), - acknowledged_msg_id=msg.msg_id - ) + timestamp=datetime.utcnow(), acknowledged_msg_id=msg.msg_id + ), ) - + for item in msg.content: if isinstance(item, StartSessionContent): ctx.logger.info(f"Session started with {sender}") - welcome_message = create_text_chat("Hello! I'm an example agent. How can I help you?") + welcome_message = create_text_chat( + "Hello! I'm an example agent. How can I help you?" + ) await ctx.send(sender, welcome_message) - + elif isinstance(item, TextContent): ctx.logger.info(f"Text message from {sender}: {item.text}") - + if ASI_ONE_API_KEY: try: ai_response = call_asi_one_api(item.text) response_message = create_text_chat(ai_response) except Exception as e: ctx.logger.error(f"Error calling ASI One API: {e}") - response_message = create_text_chat("Sorry, I encountered an error processing your request.") + response_message = create_text_chat( + "Sorry, I encountered an error processing your request." + ) else: response_message = create_text_chat( f"Hello from Agent! You said: {item.text}\n\n" "Note: ASI_ONE_API_KEY not configured. Set it to enable AI-powered responses." ) - + await ctx.send(sender, response_message) - + elif isinstance(item, EndSessionContent): ctx.logger.info(f"Session ended with {sender}") - goodbye_message = create_text_chat("Goodbye! Thanks for chatting.", end_session=True) + goodbye_message = create_text_chat( + "Goodbye! Thanks for chatting.", end_session=True + ) await ctx.send(sender, goodbye_message) - + else: ctx.logger.info(f"Received unexpected content type from {sender}") @chat_proto.on_message(ChatAcknowledgement) async def handle_acknowledgement(ctx: Context, sender: str, msg: ChatAcknowledgement): - ctx.logger.info(f"Received acknowledgement from {sender} for message {msg.acknowledged_msg_id}") + ctx.logger.info( + f"Received acknowledgement from {sender} for message {msg.acknowledged_msg_id}" + ) @agent.on_event("startup") async def startup_handler(ctx: Context): ctx.logger.info(f"🚀 Agent starting: {ctx.agent.name} at {ctx.agent.address}") - + if AGENTVERSE_KEY and SEED_PHRASE: try: endpoint_url = HOSTING_ENDPOINT - if not endpoint_url and hasattr(agent, '_endpoints') and agent._endpoints: + if not endpoint_url and hasattr(agent, "_endpoints") and agent._endpoints: endpoint_url = agent._endpoints[0].url elif not endpoint_url: endpoint_url = f"http://localhost:{AGENT_PORT}" - - ctx.logger.info(f"Registering with Agentverse using endpoint: {endpoint_url}") - + + ctx.logger.info( + f"Registering with Agentverse using endpoint: {endpoint_url}" + ) + register_chat_agent( "Example Agent", endpoint_url, @@ -196,7 +200,7 @@ async def startup_handler(ctx: Context): agent_seed_phrase=SEED_PHRASE, ), readme=README, - description="An example agent demonstrating Agentverse registration, chat protocol, and ASI One API integration." + description="An example agent demonstrating Agentverse registration, chat protocol, and ASI One API integration.", ) ctx.logger.info("✅ Registered with Agentverse") except Exception as e: @@ -208,8 +212,10 @@ async def startup_handler(ctx: Context): missing.append("AGENTVERSE_KEY") if not SEED_PHRASE: missing.append("AGENT_SEED_PHRASE") - ctx.logger.warning(f"⚠️ {', '.join(missing)} not set, skipping Agentverse registration") - + ctx.logger.warning( + f"⚠️ {', '.join(missing)} not set, skipping Agentverse registration" + ) + if ASI_ONE_API_KEY: ctx.logger.info("✅ ASI One API key configured") else: diff --git a/deploy-agent-on-av/example/agent.py b/deploy-agent-on-av/example/agent.py index ceef93a6..ece2ddb7 100644 --- a/deploy-agent-on-av/example/agent.py +++ b/deploy-agent-on-av/example/agent.py @@ -24,8 +24,8 @@ AGENTVERSE_KEY = os.getenv("ILABS_AGENTVERSE_API_KEY") client = OpenAI( - base_url='https://api.asi1.ai/v1', - api_key=os.getenv("ASI_API_KEY"), + base_url="https://api.asi1.ai/v1", + api_key=os.getenv("ASI_API_KEY"), ) agent = Agent( @@ -33,11 +33,12 @@ seed=SEED_PHRASE, port=8001, mailbox=True, - handle_messages_concurrently=True + handle_messages_concurrently=True, ) protocol = Protocol(spec=chat_protocol_spec) + @protocol.on_message(ChatMessage) async def handle_message(ctx: Context, sender: str, msg: ChatMessage): await ctx.send( @@ -54,28 +55,36 @@ async def handle_message(ctx: Context, sender: str, msg: ChatMessage): r = client.chat.completions.create( model="asi1", messages=[ - {"role": "system", "content": "You are a helpful AI assistant. Answer user queries clearly and politely."}, + { + "role": "system", + "content": "You are a helpful AI assistant. Answer user queries clearly and politely.", + }, {"role": "user", "content": text}, ], max_tokens=2048, ) response = str(r.choices[0].message.content) - except: + except: # noqa: E722 ctx.logger.exception("Error querying model") - await ctx.send(sender, ChatMessage( - timestamp=datetime.utcnow(), - msg_id=uuid4(), - content=[ - TextContent(type="text", text=response), - EndSessionContent(type="end-session"), - ] - )) + await ctx.send( + sender, + ChatMessage( + timestamp=datetime.utcnow(), + msg_id=uuid4(), + content=[ + TextContent(type="text", text=response), + EndSessionContent(type="end-session"), + ], + ), + ) + @protocol.on_message(ChatAcknowledgement) async def handle_ack(ctx: Context, sender: str, msg: ChatAcknowledgement): pass + agent.include(protocol, publish_manifest=True) README = """# ASI1 Agent @@ -96,11 +105,12 @@ async def handle_ack(ctx: Context, sender: str, msg: ChatAcknowledgement): Send messages to the agent via ASI1 or direct chat protocol to interact with the AI assistant. """ + @agent.on_event("startup") async def startup_handler(ctx: Context): """Initialize agent and register with Agentverse on startup.""" ctx.logger.info(f"🚀 Agent starting: {ctx.agent.name} at {ctx.agent.address}") - + # Register with Agentverse if AGENTVERSE_KEY and SEED_PHRASE: try: @@ -113,13 +123,16 @@ async def startup_handler(ctx: Context): agent_seed_phrase=SEED_PHRASE, ), readme=README, - description="An AI assistant powered by ASI1 that can help answer questions and process queries using advanced AI capabilities." + description="An AI assistant powered by ASI1 that can help answer questions and process queries using advanced AI capabilities.", ) ctx.logger.info("✅ Registered with Agentverse") except Exception as e: ctx.logger.error(f"Failed to register with Agentverse: {e}") else: - ctx.logger.warning("⚠️ AGENTVERSE_KEY or SEED_PHRASE not set, skipping Agentverse registration") + ctx.logger.warning( + "⚠️ AGENTVERSE_KEY or SEED_PHRASE not set, skipping Agentverse registration" + ) + if __name__ == "__main__": - agent.run() \ No newline at end of file + agent.run() diff --git a/duffel-agent/agent.py b/duffel-agent/agent.py index c66e8563..696dd047 100644 --- a/duffel-agent/agent.py +++ b/duffel-agent/agent.py @@ -22,11 +22,11 @@ pass # Protocols -from protocols.chat_proto import chat_proto -from protocols.payment_proto import payment_proto, set_agent_wallet +from protocols.chat_proto import chat_proto # noqa: E402 +from protocols.payment_proto import payment_proto, set_agent_wallet # noqa: E402 # Optional: Skyfire helper (for logging / sanity check) -from tools.skyfire import get_skyfire_service_id +from tools.skyfire import get_skyfire_service_id # noqa: E402 # Config AGENT_NAME = os.getenv("AGENT_NAME", "DuffelFlightsAgent") @@ -46,6 +46,7 @@ # Supply wallet to payment protocol for verification set_agent_wallet(agent.wallet) + @agent.on_event("startup") async def on_startup(ctx: Context): ctx.logger.info(f"{AGENT_NAME} is up. Wallet address: {agent.wallet.address()}") @@ -55,9 +56,10 @@ async def on_startup(ctx: Context): else: ctx.logger.info("No Skyfire service ID configured (SELLER_SERVICE_ID missing).") + # Include protocols and publish their manifests agent.include(chat_proto, publish_manifest=True) agent.include(payment_proto, publish_manifest=True) if __name__ == "__main__": - agent.run() \ No newline at end of file + agent.run() diff --git a/duffel-agent/protocols/chat_proto.py b/duffel-agent/protocols/chat_proto.py index f36134fe..c0734c62 100644 --- a/duffel-agent/protocols/chat_proto.py +++ b/duffel-agent/protocols/chat_proto.py @@ -14,7 +14,6 @@ from __future__ import annotations import re from typing import Dict, Any, Optional -from uuid import uuid4 from datetime import datetime, timezone from uagents import Protocol, Context @@ -48,13 +47,15 @@ "gender": "M", "email": "abhi.gangani@fetch.ai", "phone_number": "+447788998877", - } + }, } + def _get_known_passenger(sender: str) -> Optional[Dict[str, Any]]: """Get pre-filled passenger details for known agent addresses.""" return KNOWN_PASSENGERS.get(sender) + def _format_passenger_confirmation(passenger: Dict[str, Any]) -> str: """Format passenger details for user confirmation.""" lines = ["📋 Passenger Details:"] @@ -66,7 +67,9 @@ def _format_passenger_confirmation(passenger: Dict[str, Any]) -> str: lines.append(f"• Date of Birth: {passenger['born_on']}") if passenger.get("gender"): gender_map = {"M": "Male", "F": "Female", "X": "Other"} - lines.append(f"• Gender: {gender_map.get(passenger['gender'], passenger['gender'])}") + lines.append( + f"• Gender: {gender_map.get(passenger['gender'], passenger['gender'])}" + ) if passenger.get("phone_number"): lines.append(f"• Phone: {passenger['phone_number']}") if passenger.get("email"): @@ -75,29 +78,38 @@ def _format_passenger_confirmation(passenger: Dict[str, Any]) -> str: lines.append(f"• Passport: {passenger['passport_number']}") return "\n".join(lines) + def _get_session_key(sender: str, session_id: str) -> str: """Generate a unique storage key for sender + session.""" return f"{sender}::{session_id}" + def _get_session_data(ctx: Context, sender: str, session_id: str) -> Dict[str, Any]: """Retrieve or initialize session data for this sender + session.""" key = _get_session_key(sender, session_id) session_data = ctx.storage.get(key) or {} - session_data.setdefault("state", { - "origin": None, - "destination": None, - "date": None, - "passengers": None, - "greeted": False, - }) + session_data.setdefault( + "state", + { + "origin": None, + "destination": None, + "date": None, + "passengers": None, + "greeted": False, + }, + ) session_data.setdefault("history", []) return session_data -def _save_session_data(ctx: Context, sender: str, session_id: str, session_data: Dict[str, Any]) -> None: + +def _save_session_data( + ctx: Context, sender: str, session_id: str, session_data: Dict[str, Any] +) -> None: """Save session data for this sender + session.""" key = _get_session_key(sender, session_id) ctx.storage.set(key, session_data) + def _extract_text(msg: ChatMessage) -> str: parts = [] for item in msg.content or []: @@ -105,12 +117,14 @@ def _extract_text(msg: ChatMessage) -> str: parts.append(item.text) return "\n".join(parts).strip() + async def _ack(ctx: Context, sender: str, msg: ChatMessage) -> None: try: await ctx.send(sender, ChatAcknowledgement(acknowledged_msg_id=msg.msg_id)) except Exception: pass + @chat_proto.on_message(ChatMessage) async def handle_chat(ctx: Context, sender: str, msg: ChatMessage) -> None: ctx.logger.info(f"Sender address: {sender}") @@ -127,7 +141,7 @@ async def handle_chat(ctx: Context, sender: str, msg: ChatMessage) -> None: # Get session-specific data session_id = str(ctx.session) session_data = _get_session_data(ctx, sender, session_id) - + state = session_data["state"] history = session_data["history"] @@ -136,8 +150,8 @@ async def handle_chat(ctx: Context, sender: str, msg: ChatMessage) -> None: user_text=text, session_state=state, history=history, - session_id=f"{sender}::{session_id}" - ) + session_id=f"{sender}::{session_id}", + ) # result expected to be {"content": , "state": , "history": } reply_text = result.get("content", "") @@ -176,7 +190,9 @@ async def handle_chat(ctx: Context, sender: str, msg: ChatMessage) -> None: "total": f"{amt} {cur}" if amt and cur else None, } key_hist = f"booked_offers:{sender}" - history_list = ctx.storage.get(key_hist) if ctx.storage.has(key_hist) else [] + history_list = ( + ctx.storage.get(key_hist) if ctx.storage.has(key_hist) else [] + ) if not isinstance(history_list, list): history_list = [] history_list.append(entry) @@ -192,31 +208,35 @@ async def handle_chat(ctx: Context, sender: str, msg: ChatMessage) -> None: history_list = ctx.storage.get(key_hist) if ctx.storage.has(key_hist) else [] if not isinstance(history_list, list): history_list = [] - + # Format orders for display if not history_list: orders_text = "📋 You have no booked orders yet." else: lines = ["📋 Your Bookings:\n"] - for i, item in enumerate(history_list[-10:], start=1): # Show last 10 orders + for i, item in enumerate( + history_list[-10:], start=1 + ): # Show last 10 orders order_id = item.get("order_id", "—") booking_ref = item.get("booking_ref", "—") total = item.get("total", "—") timestamp = item.get("timestamp", "") method = item.get("method", "—") - + lines.append(f"{i}. Order: {order_id}") lines.append(f" PNR: {booking_ref}") lines.append(f" Total: {total}") lines.append(f" Paid via: {method}") lines.append(f" Date: {timestamp[:10] if timestamp else '—'}\n") - - lines.append("To cancel a booking, say \"cancel ord_xxxxx\"") + + lines.append('To cancel a booking, say "cancel ord_xxxxx"') orders_text = "\n".join(lines) - + # Send the orders list - await ctx.send(sender, ChatMessage(content=[TextContent(type="text", text=orders_text)])) - + await ctx.send( + sender, ChatMessage(content=[TextContent(type="text", text=orders_text)]) + ) + # Clear the flag state["list_orders_requested"] = False session_data["state"] = state @@ -252,81 +272,111 @@ async def handle_chat(ctx: Context, sender: str, msg: ChatMessage) -> None: if msg.get("role") == "user": content = msg.get("content", "") # Title - title_match = re.search(r'\b(mr|ms|mrs|miss|dr|mx)\.?\s', content, re.I) + title_match = re.search( + r"\b(mr|ms|mrs|miss|dr|mx)\.?\s", content, re.I + ) if title_match: passenger_data["title"] = title_match.group(1).lower() # Names - name_match = re.search(r'\b([A-Z][a-z]+)\s+([A-Z][a-z]+)\b', content) + name_match = re.search( + r"\b([A-Z][a-z]+)\s+([A-Z][a-z]+)\b", content + ) if name_match: passenger_data["given_name"] = name_match.group(1) passenger_data["family_name"] = name_match.group(2) # DOB - dob_match = re.search(r'\b(19|20)\d{2}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])\b', content) + dob_match = re.search( + r"\b(19|20)\d{2}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])\b", + content, + ) if dob_match: passenger_data["born_on"] = dob_match.group(0) # Gender - gender_match = re.search(r'\b(male|female|m|f|x)\b', content, re.I) + gender_match = re.search( + r"\b(male|female|m|f|x)\b", content, re.I + ) if gender_match: g = gender_match.group(1).upper()[0] - passenger_data["gender"] = g if g in ['M', 'F', 'X'] else 'M' + passenger_data["gender"] = ( + g if g in ["M", "F", "X"] else "M" + ) # Email - email_match = re.search(r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b', content) + email_match = re.search( + r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b", + content, + ) if email_match: passenger_data["email"] = email_match.group(0) # Phone (must start with + and have 10+ digits, or be clearly a phone number) # Avoid matching dates like 1997-01-31 - phone_match = re.search(r'\+\d{10,}|\+\d[\d\s-]{9,}', content) + phone_match = re.search(r"\+\d{10,}|\+\d[\d\s-]{9,}", content) if phone_match: - phone_raw = phone_match.group(0).replace(' ', '').replace('-', '') + phone_raw = ( + phone_match.group(0).replace(" ", "").replace("-", "") + ) # Only store if it doesn't look like a date (not in format like 19970131) - if not (len(phone_raw) == 8 and phone_raw.startswith(('19', '20'))): + if not ( + len(phone_raw) == 8 + and phone_raw.startswith(("19", "20")) + ): passenger_data["phone_number"] = phone_raw - + # Store passenger and offer data using the same keys as payment_proto expects if passenger_data: ctx.storage.set(f"passenger_1:{sender}:{session_id}", passenger_data) ctx.logger.info(f"Stored passenger data for payment: {passenger_data}") - + # Store selected offer ID if available (check both new_state and merged state) - offer_id = new_state.get("selected_offer_id") or state.get("selected_offer_id") + offer_id = new_state.get("selected_offer_id") or state.get( + "selected_offer_id" + ) if offer_id: ctx.storage.set(f"selected_offer_id:{sender}:{session_id}", offer_id) ctx.logger.info(f"Stored offer ID: {offer_id}") else: - ctx.logger.warning(f"No offer_id found in state when requesting payment!") - + ctx.logger.warning( + "No offer_id found in state when requesting payment!" + ) + # Store offer_passengers (passenger IDs from the offer) - offer_passengers = new_state.get("offer_passengers") or state.get("offer_passengers") + offer_passengers = new_state.get("offer_passengers") or state.get( + "offer_passengers" + ) if offer_passengers: - ctx.storage.set(f"offer_passengers:{sender}:{session_id}", offer_passengers) + ctx.storage.set( + f"offer_passengers:{sender}:{session_id}", offer_passengers + ) ctx.logger.info(f"Stored {len(offer_passengers)} offer passenger IDs") except Exception as e: ctx.logger.error(f"Failed to store passenger/offer data: {e}") - + # Import payment protocol function try: from protocols.payment_proto import request_payment_from_user - description = new_state.get("payment_description", "Flight booking — pay to proceed") + + description = new_state.get( + "payment_description", "Flight booking — pay to proceed" + ) await request_payment_from_user(ctx, sender, description=description) # Clear the flag state["payment_requested"] = False except Exception as e: ctx.logger.error(f"Failed to send payment request: {e}") - + # Check if cancellation was confirmed and send email if new_state.get("cancellation_confirmed"): try: from protocols.payment_proto import _send_cancellation_email - + order_id = new_state.get("cancelled_order_id") refund_amount = new_state.get("cancelled_refund_amount") refund_currency = new_state.get("cancelled_refund_currency") - + # Get passenger email and booking ref from storage email_to = None pax_name = None booking_ref = None - + # Try to get passenger info from session storage passenger_key = f"passenger_1:{sender}:{session_id}" if ctx.storage.has(passenger_key): @@ -334,7 +384,7 @@ async def handle_chat(ctx: Context, sender: str, msg: ChatMessage) -> None: if isinstance(pax, dict): email_to = pax.get("email") pax_name = f"{pax.get('given_name', '')} {pax.get('family_name', '')}".strip() - + # Try to get booking ref from order history history_key = f"booked_offers:{sender}" if ctx.storage.has(history_key): @@ -347,22 +397,24 @@ async def handle_chat(ctx: Context, sender: str, msg: ChatMessage) -> None: # Try to get email from history if not in passenger storage pass break - + # Send cancellation email if email_to and order_id: refund_text = None if refund_amount and refund_currency: refund_text = f"{refund_amount} {refund_currency}" - + _send_cancellation_email( to_email=email_to, passenger_name=pax_name or "Traveler", order_id=order_id, booking_ref=booking_ref, - refund_amount=refund_text + refund_amount=refund_text, ) - ctx.logger.info(f"Sent cancellation email to {email_to} for order {order_id}") - + ctx.logger.info( + f"Sent cancellation email to {email_to} for order {order_id}" + ) + # Clear the flag state["cancellation_confirmed"] = False except Exception as e: @@ -370,9 +422,12 @@ async def handle_chat(ctx: Context, sender: str, msg: ChatMessage) -> None: # Send reply (skip if empty - e.g., after payment request) if reply_text and reply_text.strip(): - await ctx.send(sender, ChatMessage(content=[TextContent(type="text", text=reply_text)])) + await ctx.send( + sender, ChatMessage(content=[TextContent(type="text", text=reply_text)]) + ) + @chat_proto.on_message(ChatAcknowledgement) async def handle_ack(ctx: Context, sender: str, msg: ChatAcknowledgement) -> None: # optional logging - ctx.logger.info(f"Ack received from {sender} for {msg.acknowledged_msg_id}") \ No newline at end of file + ctx.logger.info(f"Ack received from {sender} for {msg.acknowledged_msg_id}") diff --git a/duffel-agent/protocols/payment_proto.py b/duffel-agent/protocols/payment_proto.py index 79f17d32..540291d8 100644 --- a/duffel-agent/protocols/payment_proto.py +++ b/duffel-agent/protocols/payment_proto.py @@ -17,7 +17,6 @@ RequestPayment, RejectPayment, CommitPayment, - CancelPayment, CompletePayment, payment_protocol_spec, ) @@ -38,34 +37,48 @@ from tools.fet_payment import verify_fet_payment # type: ignore except Exception as e: print(f"[payment_proto] Failed to import verify_fet_payment: {e}") + async def verify_fet_payment(*args, **kwargs): return True + try: from tools.skyfire import verify_and_charge, get_skyfire_service_id # type: ignore except Exception as e: print(f"[payment_proto] Failed to import skyfire: {e}") + async def verify_and_charge(*args, **kwargs): return True + def get_skyfire_service_id(): return None + # --- helpers for cross-protocol storage keys --- def _k(prefix: str, sender: str, session: str) -> str: return f"{prefix}:{sender}:{session}" + def _ka(prefix: str, sender: str) -> str: return f"{prefix}:{sender}" + # --- optional email sender --- -import httpx -import smtplib, ssl -from email.message import EmailMessage +import httpx # noqa: E402 +import smtplib # noqa: E402 +import ssl # noqa: E402 +from email.message import EmailMessage # noqa: E402 + def _smtp_enabled() -> bool: - return bool(os.getenv("SMTP_HOST") and os.getenv("SMTP_USER") and os.getenv("SMTP_PASS")) + return bool( + os.getenv("SMTP_HOST") and os.getenv("SMTP_USER") and os.getenv("SMTP_PASS") + ) + -def _send_html_email(to_email: str, subject: str, html_body: str, text_body: str) -> None: +def _send_html_email( + to_email: str, subject: str, html_body: str, text_body: str +) -> None: try: host = os.getenv("SMTP_HOST", "smtp.gmail.com") port = int(os.getenv("SMTP_PORT", "587")) @@ -80,7 +93,7 @@ def _send_html_email(to_email: str, subject: str, html_body: str, text_body: str msg["To"] = to_email msg["Subject"] = subject msg.set_content(text_body) - msg.add_alternative(html_body, subtype='html') + msg.add_alternative(html_body, subtype="html") with smtplib.SMTP(host, port) as smtp: smtp.ehlo() smtp.starttls(context=ssl.create_default_context()) @@ -89,9 +102,19 @@ def _send_html_email(to_email: str, subject: str, html_body: str, text_body: str except Exception: return -def _send_booking_email(to_email: str, passenger_name: str, order_id: str, booking_ref: Optional[str], total_text: Optional[str], itinerary: Optional[str], payment_method: Optional[str] = None, flight_route: Optional[str] = None) -> None: + +def _send_booking_email( + to_email: str, + passenger_name: str, + order_id: str, + booking_ref: Optional[str], + total_text: Optional[str], + itinerary: Optional[str], + payment_method: Optional[str] = None, + flight_route: Optional[str] = None, +) -> None: subj = f"✈️ Your Flight Booking Confirmed — {booking_ref or order_id}" - + # Plain text version lines = [ f"Hi {passenger_name or 'Traveler'},", @@ -104,10 +127,12 @@ def _send_booking_email(to_email: str, passenger_name: str, order_id: str, booki ] if flight_route: lines.append(f"Flight Route: {flight_route}") - lines.extend([ - f"Order ID: {order_id}", - f"Total Paid: {total_text or '—'}", - ]) + lines.extend( + [ + f"Order ID: {order_id}", + f"Total Paid: {total_text or '—'}", + ] + ) if payment_method: lines.append(f"Paid Via: {payment_method}") if itinerary: @@ -122,10 +147,10 @@ def _send_booking_email(to_email: str, passenger_name: str, order_id: str, booki "Need help? Contact our support team anytime.", "", "Safe travels! ✈️", - "— The Flight Booking Team" + "— The Flight Booking Team", ] text = "\n".join(lines) - + # Beautiful HTML version with gradient and modern design html = f""" @@ -152,7 +177,7 @@ def _send_booking_email(to_email: str, passenger_name: str, order_id: str, booki

- Hi {passenger_name or 'Traveler'}, + Hi {passenger_name or "Traveler"},

Great news! Your flight booking has been confirmed and you're all set for your journey. 🎉 @@ -164,38 +189,56 @@ def _send_booking_email(to_email: str, passenger_name: str, order_id: str, booki - + - {f''' + { + f''' - ''' if flight_route else ''} + ''' + if flight_route + else "" + } - + - + - {f''' + { + f''' - ''' if payment_method else ''} + ''' + if payment_method + else "" + }
Booking Reference (PNR){booking_ref or '—'}{ + booking_ref or "—" + }
Flight Route {flight_route}
Order ID{order_id}{ + order_id + }
Total Paid{total_text or '—'}{ + total_text or "—" + }
Paid Via {payment_method}
- {f''' + { + f'''

✈️ Flight Details

{itinerary}
- ''' if itinerary else ''} + ''' + if itinerary + else "" + }
@@ -231,7 +274,14 @@ def _send_booking_email(to_email: str, passenger_name: str, order_id: str, booki if _smtp_enabled(): _send_html_email(to_email, subj, html, text) -def _send_cancellation_email(to_email: str, passenger_name: str, order_id: str, booking_ref: Optional[str], refund_amount: Optional[str]) -> None: + +def _send_cancellation_email( + to_email: str, + passenger_name: str, + order_id: str, + booking_ref: Optional[str], + refund_amount: Optional[str], +) -> None: """Send cancellation confirmation email.""" try: # Normalize refund amount to USDC if possible (expects like "12.34 GBP") @@ -250,17 +300,17 @@ def _send_cancellation_email(to_email: str, passenger_name: str, order_id: str, refund_text = refund_amount # fallback subj = f"🔄 Booking Cancellation Confirmed — {booking_ref or order_id}" - + # Plain text version - text = f"""Hi {passenger_name or 'Traveler'}, + text = f"""Hi {passenger_name or "Traveler"}, Your booking has been successfully cancelled. Cancellation Details: ━━━━━━━━━━━━━━━━━━━━ Order ID: {order_id} -Booking Reference (PNR): {booking_ref or '—'} -{f'Refund Amount: {refund_text}' if refund_text else ''} +Booking Reference (PNR): {booking_ref or "—"} +{f"Refund Amount: {refund_text}" if refund_text else ""} ⏱️ Your refund will be processed within 5 working days. @@ -295,7 +345,7 @@ def _send_cancellation_email(to_email: str, passenger_name: str, order_id: str,

- Hi {passenger_name or 'Traveler'}, + Hi {passenger_name or "Traveler"},

Your booking has been successfully cancelled as requested. We've processed your cancellation and the details are below. @@ -307,18 +357,26 @@ def _send_cancellation_email(to_email: str, passenger_name: str, order_id: str, - + - + - {f''' + { + f''' - ''' if refund_text else ''} + ''' + if refund_text + else "" + }
Booking Reference (PNR){booking_ref or '—'}{ + booking_ref or "—" + }
Order ID{order_id}{ + order_id + }
Refund Amount {refund_text}

@@ -358,12 +416,13 @@ def _send_cancellation_email(to_email: str, passenger_name: str, order_id: str, """ - + if _smtp_enabled(): _send_html_email(to_email, subj, html, text) except Exception: pass # Silently fail email sending + # --- fx helper for display (optional) --- def _fx_to_usdc(amount, currency: str) -> str | None: """Convert amount in given currency to USDC using FreeCurrencyAPI; returns string like '12.34' or None on failure.""" @@ -374,14 +433,17 @@ def _fx_to_usdc(amount, currency: str) -> str | None: cur = (currency or "").upper() if not cur: return None - if cur in {"USD","USDC"}: + if cur in {"USD", "USDC"}: return f"{amt:.2f}" try: key = os.getenv("FREECURRENCYAPI_KEY", "") if not key: return None with httpx.Client(timeout=10.0) as client: - r = client.get("https://api.freecurrencyapi.com/v1/latest", params={"apikey": key, "currencies": cur}) + r = client.get( + "https://api.freecurrencyapi.com/v1/latest", + params={"apikey": key, "currencies": cur}, + ) if r.status_code >= 400: return None data = r.json() or {} @@ -394,11 +456,15 @@ def _fx_to_usdc(amount, currency: str) -> str | None: except Exception: return None + def _fet_usdc_price() -> float | None: """Fetch FET/USDC price from Binance; returns USDC per 1 FET.""" try: with httpx.Client(timeout=10.0) as client: - r = client.get("https://api.binance.com/api/v3/ticker/price", params={"symbol": "FETUSDC"}) + r = client.get( + "https://api.binance.com/api/v3/ticker/price", + params={"symbol": "FETUSDC"}, + ) if r.status_code >= 400: return None data = r.json() or {} @@ -407,6 +473,7 @@ def _fet_usdc_price() -> float | None: except Exception: return None + def _usdc_to_fet(amount_usdc: str | float | int) -> str | None: """Convert a USDC amount to FET using Binance price. Returns a string with 6 decimals, or None.""" try: @@ -419,11 +486,14 @@ def _usdc_to_fet(amount_usdc: str | float | int) -> str | None: except Exception: return None + # --- protocol --- payment_proto = Protocol(spec=payment_protocol_spec, role="seller") # Allow agent.py to inject its wallet (optional) _AGENT_WALLET_ADDR: Optional[str] = None + + def set_agent_wallet(wallet) -> None: """Call this from agent.py at startup.""" global _AGENT_WALLET_ADDR @@ -432,19 +502,29 @@ def set_agent_wallet(wallet) -> None: except Exception: _AGENT_WALLET_ADDR = None + def _recipient_str(ctx: Context) -> str: env_recipient = os.getenv("SELLER_RECIPIENT", "") - cand = _AGENT_WALLET_ADDR or (env_recipient if env_recipient else None) or str(ctx.agent.address) + cand = ( + _AGENT_WALLET_ADDR + or (env_recipient if env_recipient else None) + or str(ctx.agent.address) + ) return str(cand) + # ---------------------------- # PUBLIC: ask the user for a payment (idempotent per session) # ---------------------------- -async def request_payment_from_user(ctx: Context, user_address: str, description: Optional[str] = None) -> None: +async def request_payment_from_user( + ctx: Context, user_address: str, description: Optional[str] = None +) -> None: session = str(ctx.session) pr_key = _k("payment_requested", user_address, session) if ctx.storage.has(pr_key): - ctx.logger.info(f"[payment] payment request already sent session={session} to={user_address}") + ctx.logger.info( + f"[payment] payment request already sent session={session} to={user_address}" + ) return # Fixed pricing for testing; we will still compute and show original totals in a message @@ -466,8 +546,6 @@ async def request_payment_from_user(ctx: Context, user_address: str, description if isinstance(v2, str) and v2: offer_id_for_pricing = v2 - original_usdc: Optional[str] = None - original_fet: Optional[str] = None if offer_id_for_pricing: token = os.getenv("DUFFEL_TOKEN") if token: @@ -477,7 +555,10 @@ async def request_payment_from_user(ctx: Context, user_address: str, description "Accept": "application/json", } with httpx.Client(timeout=15) as client: - r = client.get(f"https://api.duffel.com/air/offers/{offer_id_for_pricing}", headers=headers) + r = client.get( + f"https://api.duffel.com/air/offers/{offer_id_for_pricing}", + headers=headers, + ) if r.status_code < 400: data = r.json() or {} dd = data.get("data") or {} @@ -486,10 +567,9 @@ async def request_payment_from_user(ctx: Context, user_address: str, description if amt and cur: amt_usdc = _fx_to_usdc(amt, cur) if amt_usdc: - original_usdc = amt_usdc fet_est = _usdc_to_fet(amt_usdc) if fet_est: - original_fet = fet_est + pass except Exception: pass @@ -501,9 +581,13 @@ async def request_payment_from_user(ctx: Context, user_address: str, description ] if skyfire_service_id: ctx.logger.info(f"[payment] Adding Skyfire USDC option: {usd_amount}") - accepted_funds.append(Funds(currency="USDC", amount=usd_amount, payment_method="skyfire")) + accepted_funds.append( + Funds(currency="USDC", amount=usd_amount, payment_method="skyfire") + ) else: - ctx.logger.warning("[payment] Skyfire service ID not found, only FET payment available") + ctx.logger.warning( + "[payment] Skyfire service ID not found, only FET payment available" + ) metadata: dict[str, str] = {} if skyfire_service_id: @@ -523,10 +607,13 @@ async def request_payment_from_user(ctx: Context, user_address: str, description await ctx.send(user_address, req) ctx.storage.set(pr_key, True) - ctx.logger.info(f"[payment] → RequestPayment to={user_address} session={session} accepted={[(f.currency, f.amount, f.payment_method) for f in accepted_funds]}") - + ctx.logger.info( + f"[payment] → RequestPayment to={user_address} session={session} accepted={[(f.currency, f.amount, f.payment_method) for f in accepted_funds]}" + ) + # Note: Chat message about payment details is handled by the LLM, not here + # ---------------------------- # REQUIRED seller handlers # ---------------------------- @@ -536,7 +623,9 @@ async def on_commit(ctx: Context, sender: str, msg: CommitPayment) -> None: try: tx_key = _k(f"commit_{msg.transaction_id}", sender, session) if ctx.storage.has(tx_key): - ctx.logger.info(f"[payment] duplicate CommitPayment ignored tx={msg.transaction_id}") + ctx.logger.info( + f"[payment] duplicate CommitPayment ignored tx={msg.transaction_id}" + ) return except Exception: pass @@ -544,7 +633,9 @@ async def on_commit(ctx: Context, sender: str, msg: CommitPayment) -> None: method = msg.funds.payment_method verified = False try: - ctx.logger.info(f"[payment] ← CommitPayment from={sender} session={session} method={method} currency={msg.funds.currency} amount={msg.funds.amount} tx={msg.transaction_id}") + ctx.logger.info( + f"[payment] ← CommitPayment from={sender} session={session} method={method} currency={msg.funds.currency} amount={msg.funds.amount} tx={msg.transaction_id}" + ) except Exception: pass @@ -570,10 +661,10 @@ async def on_commit(ctx: Context, sender: str, msg: CommitPayment) -> None: except Exception: usd_amount = "0.001" verified = await verify_and_charge( - token=msg.transaction_id, + token=msg.transaction_id, amount_usdc=usd_amount, - logger=ctx.logger, - ) + logger=ctx.logger, + ) if verified: try: @@ -581,7 +672,7 @@ async def on_commit(ctx: Context, sender: str, msg: CommitPayment) -> None: except Exception: pass await ctx.send(sender, CompletePayment(transaction_id=msg.transaction_id)) - #await ctx.send(sender, ChatMessage(content=[TextContent(type="text", text="✅ Payment received. We will proceed with your booking shortly.")])) + # await ctx.send(sender, ChatMessage(content=[TextContent(type="text", text="✅ Payment received. We will proceed with your booking shortly.")])) try: ctx.logger.info(f"[payment] ✅ verified method={method} session={session}") except Exception: @@ -622,7 +713,7 @@ async def on_commit(ctx: Context, sender: str, msg: CommitPayment) -> None: p2 = ctx.storage.get(_ka("passenger_1", sender)) if isinstance(p2, dict): passenger = p2 - + # Get offer_passengers (IDs from the offer) offer_passengers: Optional[list] = None if ctx.storage.has(_k("offer_passengers", sender, session)): @@ -633,15 +724,39 @@ async def on_commit(ctx: Context, sender: str, msg: CommitPayment) -> None: # Validate we have both offer and passenger if not offer_id: ctx.logger.error(f"[booking] No offer_id found for session={session}") - await ctx.send(sender, ChatMessage(content=[TextContent(type="text", text="Missing flight selection. Please search and select a flight first.")])) + await ctx.send( + sender, + ChatMessage( + content=[ + TextContent( + type="text", + text="Missing flight selection. Please search and select a flight first.", + ) + ] + ), + ) return - + if not (isinstance(passenger, dict) and passenger.get("born_on")): - ctx.logger.error(f"[booking] Missing passenger DOB for session={session}") - await ctx.send(sender, ChatMessage(content=[TextContent(type="text", text="Missing date of birth (YYYY-MM-DD). Please provide DOB and say 'book now'.")])) + ctx.logger.error( + f"[booking] Missing passenger DOB for session={session}" + ) + await ctx.send( + sender, + ChatMessage( + content=[ + TextContent( + type="text", + text="Missing date of birth (YYYY-MM-DD). Please provide DOB and say 'book now'.", + ) + ] + ), + ) return - ctx.logger.info(f"[booking] Proceeding with offer_id={offer_id}, passenger={passenger.get('given_name')} {passenger.get('family_name')}") + ctx.logger.info( + f"[booking] Proceeding with offer_id={offer_id}, passenger={passenger.get('given_name')} {passenger.get('family_name')}" + ) # Map passenger ID from offer_passengers (first adult) if offer_passengers and len(offer_passengers) > 0: @@ -649,7 +764,9 @@ async def on_commit(ctx: Context, sender: str, msg: CommitPayment) -> None: for op in offer_passengers: if isinstance(op, dict) and op.get("type") == "adult": passenger["id"] = op.get("id") - ctx.logger.info(f"[booking] Mapped passenger ID: {passenger['id']}") + ctx.logger.info( + f"[booking] Mapped passenger ID: {passenger['id']}" + ) break services: Optional[list] = None @@ -658,16 +775,18 @@ async def on_commit(ctx: Context, sender: str, msg: CommitPayment) -> None: if isinstance(sv, list): services = sv - # call booking tool + # call booking tool res = None try: if duffel_create_order is not None: # Unwrap LangChain tool if needed func = duffel_create_order - if hasattr(func, 'func'): + if hasattr(func, "func"): func = func.func - - ctx.logger.info(f"[booking] Calling duffel_create_order with offer_id={offer_id}, passenger={passenger}") + + ctx.logger.info( + f"[booking] Calling duffel_create_order with offer_id={offer_id}, passenger={passenger}" + ) res = func( offer_id=offer_id, passengers=[passenger], @@ -675,7 +794,9 @@ async def on_commit(ctx: Context, sender: str, msg: CommitPayment) -> None: pay_with_balance_now=True, ) ctx.logger.info(f"[booking] duffel_create_order result: {res}") - ctx.logger.info(f"[booking] Result type: {type(res)}, has error: {res.get('error') if isinstance(res, dict) else 'N/A'}") + ctx.logger.info( + f"[booking] Result type: {type(res)}, has error: {res.get('error') if isinstance(res, dict) else 'N/A'}" + ) except Exception as e: ctx.logger.error(f"[booking] duffel_create_order failed: {e}") res = None @@ -695,19 +816,25 @@ async def on_commit(ctx: Context, sender: str, msg: CommitPayment) -> None: history = ctx.storage.get(key_hist) if ctx.storage.has(key_hist) else [] if not isinstance(history, list): history = [] - history.append({ - "timestamp": datetime.now(timezone.utc).isoformat(), - "session": session, - "method": method, - "currency": str(msg.funds.currency), - "amount": str(msg.funds.amount), - "tx": str(msg.transaction_id), - "order_id": order_id, - "booking_ref": booking_reference, - "total": f"{display_amount} {display_currency}" if display_amount and display_currency else None, - }) + history.append( + { + "timestamp": datetime.now(timezone.utc).isoformat(), + "session": session, + "method": method, + "currency": str(msg.funds.currency), + "amount": str(msg.funds.amount), + "tx": str(msg.transaction_id), + "order_id": order_id, + "booking_ref": booking_reference, + "total": f"{display_amount} {display_currency}" + if display_amount and display_currency + else None, + } + ) ctx.storage.set(key_hist, history) - ctx.logger.info(f"[booking] saved entry addr={sender} session={session} order={order_id}") + ctx.logger.info( + f"[booking] saved entry addr={sender} session={session} order={order_id}" + ) ctx.storage.set(placed_key, True) lines = [ @@ -716,13 +843,35 @@ async def on_commit(ctx: Context, sender: str, msg: CommitPayment) -> None: f"Booking reference: {booking_reference}", f"Total paid: {display_amount} {display_currency}", ] - await ctx.send(sender, ChatMessage(content=[TextContent(type="text", text="\n".join([ln for ln in lines if ln and str(ln).strip() != "None"]))])) + await ctx.send( + sender, + ChatMessage( + content=[ + TextContent( + type="text", + text="\n".join( + [ + ln + for ln in lines + if ln and str(ln).strip() != "None" + ] + ), + ) + ] + ), + ) try: email_to = passenger.get("email") - pax_name = (passenger.get("given_name") or "") + (" " + (passenger.get("family_name") or "")) - total_text = f"{display_amount} {display_currency}" if display_amount and display_currency else None - + pax_name = (passenger.get("given_name") or "") + ( + " " + (passenger.get("family_name") or "") + ) + total_text = ( + f"{display_amount} {display_currency}" + if display_amount and display_currency + else None + ) + # Format payment method payment_method_display = None if method == "skyfire": @@ -731,12 +880,14 @@ async def on_commit(ctx: Context, sender: str, msg: CommitPayment) -> None: payment_method_display = "FET Direct" else: payment_method_display = method.upper() if method else None - + # Try to get flight route from stored offer details flight_route = None try: if ctx.storage.has(_k("offer_details", sender, session)): - offer_details = ctx.storage.get(_k("offer_details", sender, session)) + offer_details = ctx.storage.get( + _k("offer_details", sender, session) + ) if isinstance(offer_details, dict): # Extract route from offer details slices = offer_details.get("slices", []) @@ -744,32 +895,48 @@ async def on_commit(ctx: Context, sender: str, msg: CommitPayment) -> None: first_slice = slices[0] segments = first_slice.get("segments", []) if segments: - origin = segments[0].get("origin", {}).get("iata_code") - destination = segments[-1].get("destination", {}).get("iata_code") + origin = ( + segments[0] + .get("origin", {}) + .get("iata_code") + ) + destination = ( + segments[-1] + .get("destination", {}) + .get("iata_code") + ) dep_time = segments[0].get("departing_at", "") arr_time = segments[-1].get("arriving_at", "") if origin and destination: # Format: SFO→LAX • Jan 22, 2026 • 16:15-17:33 - #from datetime import datetime + # from datetime import datetime if dep_time: - dep_dt = datetime.fromisoformat(dep_time.replace("Z", "+00:00")) + dep_dt = datetime.fromisoformat( + dep_time.replace("Z", "+00:00") + ) date_str = dep_dt.strftime("%b %d, %Y") time_str = dep_dt.strftime("%H:%M") if arr_time: - arr_dt = datetime.fromisoformat(arr_time.replace("Z", "+00:00")) - time_str += f"-{arr_dt.strftime('%H:%M')}" + arr_dt = datetime.fromisoformat( + arr_time.replace("Z", "+00:00") + ) + time_str += ( + f"-{arr_dt.strftime('%H:%M')}" + ) flight_route = f"{origin}→{destination} • {date_str} • {time_str}" else: flight_route = f"{origin}→{destination}" except Exception as e: ctx.logger.warning(f"Could not extract flight route: {e}") - + if email_to: _send_booking_email( to_email=email_to, passenger_name=pax_name, order_id=str(order_id), - booking_ref=(str(booking_reference) if booking_reference else None), + booking_ref=( + str(booking_reference) if booking_reference else None + ), total_text=total_text, itinerary=None, payment_method=payment_method_display, @@ -782,17 +949,44 @@ async def on_commit(ctx: Context, sender: str, msg: CommitPayment) -> None: if res and res.get("error"): error_msg += f" Error: {res.get('error')}" ctx.logger.error(f"[booking] {error_msg}") - await ctx.send(sender, ChatMessage(content=[TextContent(type="text", text=error_msg + " Please contact support.")])) + await ctx.send( + sender, + ChatMessage( + content=[ + TextContent( + type="text", text=error_msg + " Please contact support." + ) + ] + ), + ) except Exception as e: ctx.logger.error(f"[booking] auto-book attempt failed: {e}") - await ctx.send(sender, ChatMessage(content=[TextContent(type="text", text=f"Booking error: {str(e)}")])) + await ctx.send( + sender, + ChatMessage( + content=[TextContent(type="text", text=f"Booking error: {str(e)}")] + ), + ) else: await ctx.send(sender, RejectPayment(reason="Payment verification failed")) try: - ctx.logger.error(f"[payment] ❌ verification failed method={method} session={session}") + ctx.logger.error( + f"[payment] ❌ verification failed method={method} session={session}" + ) except Exception: pass + @payment_proto.on_message(RejectPayment) async def on_reject_payment(ctx: Context, sender: str, msg: RejectPayment) -> None: - await ctx.send(sender, ChatMessage(content=[TextContent(type="text", text="Payment was rejected. We can try again, change method, or adjust the booking.")])) \ No newline at end of file + await ctx.send( + sender, + ChatMessage( + content=[ + TextContent( + type="text", + text="Payment was rejected. We can try again, change method, or adjust the booking.", + ) + ] + ), + ) diff --git a/duffel-agent/schemas/schemas.py b/duffel-agent/schemas/schemas.py index 2f259b79..d06080ef 100644 --- a/duffel-agent/schemas/schemas.py +++ b/duffel-agent/schemas/schemas.py @@ -1 +1 @@ -s \ No newline at end of file +s # noqa: F821 diff --git a/duffel-agent/state_store.py b/duffel-agent/state_store.py index 15295a80..20d76775 100644 --- a/duffel-agent/state_store.py +++ b/duffel-agent/state_store.py @@ -33,8 +33,7 @@ def _get_conn() -> sqlite3.Connection: def load_state(user_id: str) -> Dict[str, Any]: conn = _get_conn() cur = conn.execute( - "SELECT state_json FROM session_state WHERE user_id = ?", - (user_id,) + "SELECT state_json FROM session_state WHERE user_id = ?", (user_id,) ) row = cur.fetchone() if row: @@ -58,14 +57,11 @@ def save_state(user_id: str, state: Dict[str, Any]) -> None: state_json=excluded.state_json, updated_at=CURRENT_TIMESTAMP """, - (user_id, state_json) + (user_id, state_json), ) def clear_state(user_id: str) -> None: conn = _get_conn() with _conn_lock: - conn.execute( - "DELETE FROM session_state WHERE user_id = ?", - (user_id,) - ) \ No newline at end of file + conn.execute("DELETE FROM session_state WHERE user_id = ?", (user_id,)) diff --git a/duffel-agent/tools/duffel_tools.py b/duffel-agent/tools/duffel_tools.py index 0d78614f..09440ae1 100644 --- a/duffel-agent/tools/duffel_tools.py +++ b/duffel-agent/tools/duffel_tools.py @@ -32,9 +32,11 @@ try: from langchain_core.tools import tool as _tool except Exception: # pragma: no cover + def _tool(f): return f + # ----------------------------- # Environment / constants # ----------------------------- @@ -59,11 +61,13 @@ def _headers() -> Dict[str, str]: "Content-Type": "application/json", } + def _fmt_time(ts: str | None) -> str: """Show up to minutes: 'YYYY-MM-DD HH:MM' (Duffel returns ISO8601).""" s = (ts or "").replace("T", " ").replace("Z", "") return re.sub(r"(\d{2}:\d{2}):\d{2}$", r"\1", s) + def _get_usd_per(cur: str) -> Optional[float]: if not cur: return None @@ -149,13 +153,19 @@ def _apply_conversions(summary: Dict[str, Any]) -> Dict[str, Any]: def _smtp_enabled() -> bool: - return bool(os.getenv("SMTP_HOST") and os.getenv("SMTP_USER") and os.getenv("SMTP_PASS")) + return bool( + os.getenv("SMTP_HOST") and os.getenv("SMTP_USER") and os.getenv("SMTP_PASS") + ) -def _send_email(to_addr: str, subject: str, body: str, html: Optional[str] = None) -> bool: +def _send_email( + to_addr: str, subject: str, body: str, html: Optional[str] = None +) -> bool: if not _smtp_enabled(): try: - logging.getLogger(__name__).info("[email] SMTP not configured; set SMTP_HOST, SMTP_USER, SMTP_PASS to enable emails") + logging.getLogger(__name__).info( + "[email] SMTP not configured; set SMTP_HOST, SMTP_USER, SMTP_PASS to enable emails" + ) except Exception: pass return False @@ -269,28 +279,41 @@ def duffel_search_offers( body["max_connections"] = max_connections if preferred_airlines: body["allowed_carriers"] = preferred_airlines - + # Log the request body for debugging import logging + logger = logging.getLogger(__name__) if preferred_airlines: - logger.info(f"Sending to Duffel API - allowed_carriers: {body.get('allowed_carriers')}") + logger.info( + f"Sending to Duffel API - allowed_carriers: {body.get('allowed_carriers')}" + ) logger.info(f"Full request body: {body}") try: with httpx.Client(timeout=_TIMEOUT) as client: - r = client.post(f"{_DUFFEL_BASE}/air/offer_requests", headers=_headers(), json={"data": body}) + r = client.post( + f"{_DUFFEL_BASE}/air/offer_requests", + headers=_headers(), + json={"data": body}, + ) if r.status_code >= 400: - return {"error": f"Duffel {r.status_code}: {r.text[:300]}", "code": "HTTP_ERROR"} + return { + "error": f"Duffel {r.status_code}: {r.text[:300]}", + "code": "HTTP_ERROR", + } data = r.json() or {} raw_offers = (data.get("data") or {}).get("offers") or [] - + # Log airline filter info import logging + logger = logging.getLogger(__name__) if preferred_airlines: - logger.info(f"Filtered search for airlines: {preferred_airlines}, got {len(raw_offers)} raw results from Duffel") + logger.info( + f"Filtered search for airlines: {preferred_airlines}, got {len(raw_offers)} raw results from Duffel" + ) offers: List[Dict[str, Any]] = [] airlines_found = set() @@ -298,13 +321,13 @@ def duffel_search_offers( owner = o.get("owner") or {} airline_name = owner.get("name") or "" airline_iata = owner.get("iata_code") or "" - + # If filtering by airline, skip offers that don't match if preferred_airlines: # Check if this offer's airline IATA code matches any in the filter if airline_iata not in preferred_airlines: continue # Skip this offer - + airlines_found.add(airline_name or airline_iata) total_amount = o.get("total_amount") total_currency = o.get("total_currency") @@ -328,10 +351,12 @@ def _sort_key(x: Dict[str, Any]) -> float: return 1e18 offers.sort(key=_sort_key) - + # Log what airlines we actually got if preferred_airlines: - logger.info(f"After processing, found airlines: {airlines_found}, total offers: {len(offers)}") + logger.info( + f"After processing, found airlines: {airlines_found}, total offers: {len(offers)}" + ) # Pagination page_size = max(1, min(10, int(page_size))) @@ -348,7 +373,10 @@ def _sort_key(x: Dict[str, Any]) -> float: } except Exception as e: - return {"error": f"duffel_search_offers failed: {type(e).__name__}: {e}", "code": "UNKNOWN"} + return { + "error": f"duffel_search_offers failed: {type(e).__name__}: {e}", + "code": "UNKNOWN", + } @_tool @@ -368,7 +396,10 @@ def duffel_get_offer_with_services(offer_id: str) -> Dict[str, Any]: params={"return_available_services": "true"}, ) if r.status_code >= 400: - return {"error": f"Duffel {r.status_code}: {r.text[:300]}", "code": "HTTP_ERROR"} + return { + "error": f"Duffel {r.status_code}: {r.text[:300]}", + "code": "HTTP_ERROR", + } d = (r.json() or {}).get("data") or {} owner = d.get("owner") or {} @@ -379,7 +410,9 @@ def duffel_get_offer_with_services(offer_id: str) -> Dict[str, Any]: offer_passengers: List[Dict[str, Any]] = [] for p in offer_pax: if isinstance(p, dict) and p.get("id"): - offer_passengers.append({"id": p.get("id"), "type": (p.get("type") or "adult")}) + offer_passengers.append( + {"id": p.get("id"), "type": (p.get("type") or "adult")} + ) res: Dict[str, Any] = { "id": d.get("id"), @@ -387,16 +420,20 @@ def duffel_get_offer_with_services(offer_id: str) -> Dict[str, Any]: "total_amount": d.get("total_amount"), "total_currency": d.get("total_currency"), "itinerary": _format_itinerary(d.get("slices") or []), - "payment_required_by": (d.get("payment_requirements") or {}).get("payment_required_by"), + "payment_required_by": (d.get("payment_requirements") or {}).get( + "payment_required_by" + ), "bags": [], "offer_passengers": offer_passengers, } - for svc in (d.get("available_services") or []): + for svc in d.get("available_services") or []: if not isinstance(svc, dict): continue bag = { "id": svc.get("id"), - "name": (svc.get("name") or (svc.get("metadata") or {}).get("name") or "Bag"), + "name": ( + svc.get("name") or (svc.get("metadata") or {}).get("name") or "Bag" + ), "total_amount": svc.get("total_amount"), "total_currency": svc.get("total_currency"), } @@ -406,7 +443,10 @@ def duffel_get_offer_with_services(offer_id: str) -> Dict[str, Any]: return res except Exception as e: - return {"error": f"offer_refresh_failed: {type(e).__name__}: {e}", "code": "UNKNOWN"} + return { + "error": f"offer_refresh_failed: {type(e).__name__}: {e}", + "code": "UNKNOWN", + } def _sanitize_passengers(passengers: List[Dict[str, Any]]) -> List[Dict[str, Any]]: @@ -416,8 +456,12 @@ def _sanitize_passengers(passengers: List[Dict[str, Any]]) -> List[Dict[str, Any continue pp: Dict[str, Any] = { "type": p.get("type") or "adult", - "given_name": p.get("given_name") or p.get("first_name") or p.get("givenName"), - "family_name": p.get("family_name") or p.get("last_name") or p.get("familyName"), + "given_name": p.get("given_name") + or p.get("first_name") + or p.get("givenName"), + "family_name": p.get("family_name") + or p.get("last_name") + or p.get("familyName"), "born_on": p.get("born_on") or p.get("dob") or p.get("date_of_birth"), } if p.get("gender"): @@ -437,8 +481,14 @@ def _sanitize_passengers(passengers: List[Dict[str, Any]]) -> List[Dict[str, Any def _summarize_order_email(d: Dict[str, Any]) -> str: total = d.get("total_amount") cur = d.get("total_currency") - br = d.get("booking_reference") or (d.get("booking_references") or [{}])[0].get("booking_reference") - airline = (d.get("owner") or {}).get("name") or (d.get("owner") or {}).get("iata_code") or "" + br = d.get("booking_reference") or (d.get("booking_references") or [{}])[0].get( + "booking_reference" + ) + airline = ( + (d.get("owner") or {}).get("name") + or (d.get("owner") or {}).get("iata_code") + or "" + ) itin = _format_itinerary(d.get("slices") or []) lines = [ "Your booking is confirmed ✅", @@ -457,8 +507,14 @@ def _summarize_order_email(d: Dict[str, Any]) -> str: def _build_booking_html(d: Dict[str, Any]) -> str: total = d.get("total_amount") cur = d.get("total_currency") - br = d.get("booking_reference") or (d.get("booking_references") or [{}])[0].get("booking_reference") - airline = (d.get("owner") or {}).get("name") or (d.get("owner") or {}).get("iata_code") or "" + br = d.get("booking_reference") or (d.get("booking_references") or [{}])[0].get( + "booking_reference" + ) + airline = ( + (d.get("owner") or {}).get("name") + or (d.get("owner") or {}).get("iata_code") + or "" + ) itin = _format_itinerary(d.get("slices") or []) order_id = d.get("id") or d.get("order_id") or "—" total_text = f"{total} {cur}".strip() if (total or cur) else "—" @@ -470,20 +526,20 @@ def _build_booking_html(d: Dict[str, Any]) -> str: Booking Confirmed @@ -494,10 +550,10 @@ def _build_booking_html(d: Dict[str, Any]) -> str:

You're all set for your trip.

-
Airline
{airline or '-'}
-
PNR
{br or '-'}
+
Airline
{airline or "-"}
+
PNR
{br or "-"}
Order ID
{order_id}
-
Itinerary
{itin or '-'}
+
Itinerary
{itin or "-"}
Total
{total_text}

To cancel: ask Duffel agent on ASI1 to cancel {order_id}.

@@ -506,7 +562,7 @@ def _build_booking_html(d: Dict[str, Any]) -> str: -""" +""" # noqa: F821 @_tool @@ -543,50 +599,79 @@ def duffel_create_order( # Inline payment = instant ticketing try: with httpx.Client(timeout=15) as client: - r_get = client.get(f"{_DUFFEL_BASE}/air/offers/{offer_id}", headers=_headers()) - d = (r_get.json() or {}).get("data") or {} if r_get.status_code < 400 else {} + r_get = client.get( + f"{_DUFFEL_BASE}/air/offers/{offer_id}", headers=_headers() + ) + d = ( + (r_get.json() or {}).get("data") or {} + if r_get.status_code < 400 + else {} + ) amt = d.get("total_amount") cur = d.get("total_currency") if amt and cur: - body["payments"] = [{"type": "balance", "amount": str(amt), "currency": str(cur)}] + body["payments"] = [ + {"type": "balance", "amount": str(amt), "currency": str(cur)} + ] except Exception: pass try: with httpx.Client(timeout=_TIMEOUT) as client: - rr = client.post(f"{_DUFFEL_BASE}/air/orders", headers=_headers(), json={"data": body}) + rr = client.post( + f"{_DUFFEL_BASE}/air/orders", headers=_headers(), json={"data": body} + ) if rr.status_code >= 400: - return {"error": f"Duffel {rr.status_code}: {rr.text[:300]}", "code": "HTTP_ERROR"} + return { + "error": f"Duffel {rr.status_code}: {rr.text[:300]}", + "code": "HTTP_ERROR", + } data = (rr.json() or {}).get("data") or {} out = { "id": data.get("id"), - "booking_reference": data.get("booking_reference") or (data.get("booking_references") or [{}])[0].get("booking_reference"), + "booking_reference": data.get("booking_reference") + or (data.get("booking_references") or [{}])[0].get("booking_reference"), "total_amount": data.get("total_amount"), "total_currency": data.get("total_currency"), - "awaiting_payment": bool((data.get("payment_status") or {}).get("awaiting_payment")), + "awaiting_payment": bool( + (data.get("payment_status") or {}).get("awaiting_payment") + ), } if notify_email: if _smtp_enabled(): try: with httpx.Client(timeout=15) as client: - r_full = client.get(f"{_DUFFEL_BASE}/air/orders/{out['id']}", headers=_headers()) + r_full = client.get( + f"{_DUFFEL_BASE}/air/orders/{out['id']}", headers=_headers() + ) full = (r_full.json() or {}).get("data") or data html = _build_booking_html(full) - ok = _send_email(notify_email, "Your flight booking", _summarize_order_email(full), html) + ok = _send_email( + notify_email, + "Your flight booking", + _summarize_order_email(full), + html, + ) try: - logging.getLogger(__name__).info(f"[email] booking email {'sent' if ok else 'skipped'} to {notify_email}") + logging.getLogger(__name__).info( + f"[email] booking email {'sent' if ok else 'skipped'} to {notify_email}" + ) except Exception: pass except Exception as e: try: - logging.getLogger(__name__).warning(f"[email] failed to prepare/send booking email: {e}") + logging.getLogger(__name__).warning( + f"[email] failed to prepare/send booking email: {e}" + ) except Exception: pass else: try: - logging.getLogger(__name__).info("[email] SMTP not configured; skipping booking email") + logging.getLogger(__name__).info( + "[email] SMTP not configured; skipping booking email" + ) except Exception: pass @@ -600,12 +685,24 @@ def duffel_pay_hold_order(order_id: str, amount: str, currency: str) -> Dict[str """Pay a HOLD order using seller balance (POST /air/payments).""" if not _DUFFEL_TOKEN: return {"error": "DUFFEL_TOKEN not set", "code": "NO_TOKEN"} - payload = {"data": {"order_id": order_id, "type": "balance", "amount": str(amount), "currency": str(currency)}} + payload = { + "data": { + "order_id": order_id, + "type": "balance", + "amount": str(amount), + "currency": str(currency), + } + } try: with httpx.Client(timeout=_TIMEOUT) as client: - r = client.post(f"{_DUFFEL_BASE}/air/payments", headers=_headers(), json=payload) + r = client.post( + f"{_DUFFEL_BASE}/air/payments", headers=_headers(), json=payload + ) if r.status_code >= 400: - return {"error": f"Duffel {r.status_code}: {r.text[:300]}", "code": "HTTP_ERROR"} + return { + "error": f"Duffel {r.status_code}: {r.text[:300]}", + "code": "HTTP_ERROR", + } return {"ok": True} except Exception as e: return {"error": f"pay_hold_failed: {type(e).__name__}: {e}", "code": "UNKNOWN"} @@ -620,11 +717,15 @@ def duffel_get_order(order_id: str) -> Dict[str, Any]: with httpx.Client(timeout=_TIMEOUT) as client: r = client.get(f"{_DUFFEL_BASE}/air/orders/{order_id}", headers=_headers()) if r.status_code >= 400: - return {"error": f"Duffel {r.status_code}: {r.text[:300]}", "code": "HTTP_ERROR"} + return { + "error": f"Duffel {r.status_code}: {r.text[:300]}", + "code": "HTTP_ERROR", + } d = (r.json() or {}).get("data") or {} return { "id": d.get("id"), - "booking_reference": d.get("booking_reference") or (d.get("booking_references") or [{}])[0].get("booking_reference"), + "booking_reference": d.get("booking_reference") + or (d.get("booking_references") or [{}])[0].get("booking_reference"), "total_amount": d.get("total_amount"), "total_currency": d.get("total_currency"), "payment_status": d.get("payment_status"), @@ -632,7 +733,10 @@ def duffel_get_order(order_id: str) -> Dict[str, Any]: "available_actions": d.get("available_actions"), } except Exception as e: - return {"error": f"get_order_failed: {type(e).__name__}: {e}", "code": "UNKNOWN"} + return { + "error": f"get_order_failed: {type(e).__name__}: {e}", + "code": "UNKNOWN", + } @_tool @@ -642,9 +746,16 @@ def duffel_create_order_cancellation(order_id: str) -> Dict[str, Any]: return {"error": "DUFFEL_TOKEN not set", "code": "NO_TOKEN"} try: with httpx.Client(timeout=_TIMEOUT) as client: - r = client.post(f"{_DUFFEL_BASE}/air/order_cancellations", headers=_headers(), json={"data": {"order_id": order_id}}) + r = client.post( + f"{_DUFFEL_BASE}/air/order_cancellations", + headers=_headers(), + json={"data": {"order_id": order_id}}, + ) if r.status_code >= 400: - return {"error": f"Duffel {r.status_code}: {r.text[:300]}", "code": "HTTP_ERROR"} + return { + "error": f"Duffel {r.status_code}: {r.text[:300]}", + "code": "HTTP_ERROR", + } d = (r.json() or {}).get("data") or {} return { "id": d.get("id"), @@ -655,11 +766,16 @@ def duffel_create_order_cancellation(order_id: str) -> Dict[str, Any]: "status": d.get("status"), } except Exception as e: - return {"error": f"create_cxl_failed: {type(e).__name__}: {e}", "code": "UNKNOWN"} + return { + "error": f"create_cxl_failed: {type(e).__name__}: {e}", + "code": "UNKNOWN", + } @_tool -def duffel_confirm_order_cancellation(order_cancellation_id: str, notify_email: Optional[str] = None) -> Dict[str, Any]: +def duffel_confirm_order_cancellation( + order_cancellation_id: str, notify_email: Optional[str] = None +) -> Dict[str, Any]: """Confirm a previously created cancellation.""" if not _DUFFEL_TOKEN: return {"error": "DUFFEL_TOKEN not set", "code": "NO_TOKEN"} @@ -671,7 +787,10 @@ def duffel_confirm_order_cancellation(order_cancellation_id: str, notify_email: json={"data": {}}, ) if r.status_code >= 400: - return {"error": f"Duffel {r.status_code}: {r.text[:300]}", "code": "HTTP_ERROR"} + return { + "error": f"Duffel {r.status_code}: {r.text[:300]}", + "code": "HTTP_ERROR", + } if notify_email and _smtp_enabled(): try: @@ -685,7 +804,10 @@ def duffel_confirm_order_cancellation(order_cancellation_id: str, notify_email: return {"ok": True} except Exception as e: - return {"error": f"confirm_cxl_failed: {type(e).__name__}: {e}", "code": "UNKNOWN"} + return { + "error": f"confirm_cxl_failed: {type(e).__name__}: {e}", + "code": "UNKNOWN", + } # Export tools @@ -698,9 +820,10 @@ def list_orders() -> Dict[str, Any]: """ return { "message": "list_orders called - implementation handled by chat_proto", - "orders": [] + "orders": [], } + @_tool def get_order_cancellation_quote(order_id: str) -> Dict[str, Any]: """ @@ -710,41 +833,49 @@ def get_order_cancellation_quote(order_id: str) -> Dict[str, Any]: token = os.getenv("DUFFEL_TOKEN") if not token: return {"error": "DUFFEL_TOKEN not set", "code": "NO_TOKEN"} - + headers = { "Authorization": f"Bearer {token}", "Duffel-Version": "v2", "Content-Type": "application/json", "Accept": "application/json", } - + try: # First check if order is cancellable with httpx.Client(timeout=20) as client: - r = client.get(f"https://api.duffel.com/air/orders/{order_id}", headers=headers) - + r = client.get( + f"https://api.duffel.com/air/orders/{order_id}", headers=headers + ) + if r.status_code >= 400: - return {"error": f"Duffel {r.status_code}: {r.text[:300]}", "code": "HTTP_ERROR"} - + return { + "error": f"Duffel {r.status_code}: {r.text[:300]}", + "code": "HTTP_ERROR", + } + order_data = (r.json() or {}).get("data") or {} available_actions = order_data.get("available_actions") or [] - + if "cancel" not in available_actions: return {"error": "This order is not cancellable", "code": "NOT_CANCELLABLE"} - + # Create cancellation quote with httpx.Client(timeout=20) as client: cr = client.post( "https://api.duffel.com/air/order_cancellations", headers=headers, - json={"data": {"order_id": order_id}} + json={"data": {"order_id": order_id}}, ) - + if cr.status_code >= 400: - return {"error": f"Duffel {cr.status_code}: {cr.text[:300]}", "code": "HTTP_ERROR"} - + return { + "error": f"Duffel {cr.status_code}: {cr.text[:300]}", + "code": "HTTP_ERROR", + } + cancellation_data = (cr.json() or {}).get("data") or {} - + return { "order_cancellation_id": cancellation_data.get("id"), "order_id": order_id, @@ -753,9 +884,13 @@ def get_order_cancellation_quote(order_id: str) -> Dict[str, Any]: "refund_to": cancellation_data.get("refund_to"), "expires_at": cancellation_data.get("expires_at"), } - + except Exception as e: - return {"error": f"get_order_cancellation_quote failed: {type(e).__name__}: {e}", "code": "UNKNOWN"} + return { + "error": f"get_order_cancellation_quote failed: {type(e).__name__}: {e}", + "code": "UNKNOWN", + } + @_tool def confirm_order_cancellation(order_cancellation_id: str) -> Dict[str, Any]: @@ -765,26 +900,29 @@ def confirm_order_cancellation(order_cancellation_id: str) -> Dict[str, Any]: token = os.getenv("DUFFEL_TOKEN") if not token: return {"error": "DUFFEL_TOKEN not set", "code": "NO_TOKEN"} - + headers = { "Authorization": f"Bearer {token}", "Duffel-Version": "v2", "Content-Type": "application/json", "Accept": "application/json", } - + try: with httpx.Client(timeout=20) as client: r = client.post( f"https://api.duffel.com/air/order_cancellations/{order_cancellation_id}/actions/confirm", - headers=headers + headers=headers, ) - + if r.status_code >= 400: - return {"error": f"Duffel {r.status_code}: {r.text[:300]}", "code": "HTTP_ERROR"} - + return { + "error": f"Duffel {r.status_code}: {r.text[:300]}", + "code": "HTTP_ERROR", + } + cancellation_data = (r.json() or {}).get("data") or {} - + return { "order_cancellation_id": cancellation_data.get("id"), "order_id": cancellation_data.get("order_id"), @@ -792,9 +930,13 @@ def confirm_order_cancellation(order_cancellation_id: str) -> Dict[str, Any]: "refund_currency": cancellation_data.get("refund_currency"), "confirmed_at": cancellation_data.get("confirmed_at"), } - + except Exception as e: - return {"error": f"confirm_order_cancellation failed: {type(e).__name__}: {e}", "code": "UNKNOWN"} + return { + "error": f"confirm_order_cancellation failed: {type(e).__name__}: {e}", + "code": "UNKNOWN", + } + try: TOOLS = [ @@ -810,4 +952,4 @@ def confirm_order_cancellation(order_cancellation_id: str) -> Dict[str, Any]: confirm_order_cancellation, ] except Exception: # pragma: no cover - TOOLS = [] \ No newline at end of file + TOOLS = [] diff --git a/duffel-agent/tools/fet_payments.py b/duffel-agent/tools/fet_payments.py index 73b412b6..db9a48ad 100644 --- a/duffel-agent/tools/fet_payments.py +++ b/duffel-agent/tools/fet_payments.py @@ -30,6 +30,7 @@ FET_VERIFY_ATTEMPTS = int(os.getenv("FET_VERIFY_ATTEMPTS", "5")) FET_VERIFY_DELAY_SEC = float(os.getenv("FET_VERIFY_DELAY_SEC", "2.0")) + def _denom_scale(denom: Optional[str]) -> float: """Determine scaling factor: converts atomic units to FET units.""" env = os.getenv("FET_DENOM_SCALE") @@ -47,15 +48,20 @@ def _denom_scale(denom: Optional[str]) -> float: # default to atto (1e18) return 1e18 + def _to_float(s: Any) -> Optional[float]: try: return float(s) except Exception: return None -def _nearly_equal(a: float, b: float, rel_tol: float = 1e-6, abs_tol: float = 1e-12) -> bool: + +def _nearly_equal( + a: float, b: float, rel_tol: float = 1e-6, abs_tol: float = 1e-12 +) -> bool: return math.isclose(a, b, rel_tol=rel_tol, abs_tol=abs_tol) + def _parse_amount_denom_atom(s: str) -> tuple[Optional[float], str]: """ Parse string like "1000000ufet" or "5000afet" → (amount_atom, denom). @@ -76,6 +82,7 @@ def _parse_amount_denom_atom(s: str) -> tuple[Optional[float], str]: return None, "".join(denom) return amt_atom, "".join(denom) + async def _fetch_tx_json(tx_hash: str, logger) -> Optional[Dict[str, Any]]: """Fetch transaction JSON from Fetch ledger with retries.""" if not tx_hash: @@ -102,6 +109,7 @@ async def _fetch_tx_json(tx_hash: str, logger) -> Optional[Dict[str, Any]]: logger.error("LCD tx fetch failed: not found after retries") return None + def _check_tx_code_ok(data: Dict[str, Any], logger) -> bool: try: code = int((data.get("tx_response") or {}).get("code", 0)) @@ -112,13 +120,14 @@ def _check_tx_code_ok(data: Dict[str, Any], logger) -> bool: except Exception: return True + def _match_via_messages( data: Dict[str, Any], recipient_address: str, expected_amount_fet: float, logger, *, - sender_address: Optional[str] = None + sender_address: Optional[str] = None, ) -> bool: """Check for Cosmos MsgSend in body-messages matching recipient and amount.""" tx = data.get("tx") or {} @@ -134,7 +143,7 @@ def _match_via_messages( continue if sender_address and from_addr and sender_address != from_addr: continue - for coin in (m.get("amount") or []): + for coin in m.get("amount") or []: denom = (coin.get("denom") or "").lower() amt_atom = _to_float(coin.get("amount")) if amt_atom is None: @@ -145,26 +154,24 @@ def _match_via_messages( return True return False + def _match_via_events( - data: Dict[str, Any], - recipient_address: str, - expected_amount_fet: float, - logger + data: Dict[str, Any], recipient_address: str, expected_amount_fet: float, logger ) -> bool: """Fallback: scan logs → events 'coin_received' or 'transfer'.""" txr = data.get("tx_response") or {} logs = txr.get("logs") or [] for log in logs: - for ev in (log.get("events") or []): + for ev in log.get("events") or []: etype = (ev.get("type") or "").lower() - if etype not in ("coin_received","transfer"): + if etype not in ("coin_received", "transfer"): continue receiver = None amount_raw = None - for a in (ev.get("attributes") or []): + for a in ev.get("attributes") or []: k = (a.get("key") or "").lower() v = a.get("value") or "" - if k in ("receiver","recipient"): + if k in ("receiver", "recipient"): receiver = v elif k == "amount": amount_raw = v @@ -181,13 +188,14 @@ def _match_via_events( return True return False + async def verify_fet_payment( tx_hash: str, recipient_address: str, amount_fet: str | float, logger, *, - sender_address: Optional[str] = None + sender_address: Optional[str] = None, ) -> bool: """Main entry: ensure tx_hash sent `amount_fet` FET to `recipient_address`.""" if not tx_hash or not recipient_address: @@ -205,11 +213,13 @@ async def verify_fet_payment( if not _check_tx_code_ok(data, logger): return False - if _match_via_messages(data, recipient_address, expected_amount, logger, sender_address=sender_address): + if _match_via_messages( + data, recipient_address, expected_amount, logger, sender_address=sender_address + ): return True if _match_via_events(data, recipient_address, expected_amount, logger): return True logger.error("verify_fet_payment: no matching transfer found for recipient/amount") - return False \ No newline at end of file + return False diff --git a/duffel-agent/tools/openai_client.py b/duffel-agent/tools/openai_client.py index 5f929f1b..7e7b062c 100644 --- a/duffel-agent/tools/openai_client.py +++ b/duffel-agent/tools/openai_client.py @@ -216,22 +216,43 @@ "parameters": { "type": "object", "properties": { - "origin": {"type": "string", "description": "IATA code of origin airport (e.g., 'LAX')"}, - "destination": {"type": "string", "description": "IATA code of destination airport (e.g., 'JFK')"}, - "date": {"type": "string", "description": "Departure date in YYYY-MM-DD format"}, - "passengers": {"type": "integer", "description": "Optional: number of adult passengers (defaults to 1)"}, - "cabin_class": {"type": "string", "description": "Optional cabin class (e.g., 'economy')"}, + "origin": { + "type": "string", + "description": "IATA code of origin airport (e.g., 'LAX')", + }, + "destination": { + "type": "string", + "description": "IATA code of destination airport (e.g., 'JFK')", + }, + "date": { + "type": "string", + "description": "Departure date in YYYY-MM-DD format", + }, + "passengers": { + "type": "integer", + "description": "Optional: number of adult passengers (defaults to 1)", + }, + "cabin_class": { + "type": "string", + "description": "Optional cabin class (e.g., 'economy')", + }, "preferred_airlines": { "type": "array", "items": {"type": "string"}, - "description": "Optional: List of airline IATA codes to filter results (e.g., ['BA'] for British Airways, ['AA', 'DL'] for American and Delta)" + "description": "Optional: List of airline IATA codes to filter results (e.g., ['BA'] for British Airways, ['AA', 'DL'] for American and Delta)", + }, + "page": { + "type": "integer", + "description": "Page number for paginated results", + }, + "page_size": { + "type": "integer", + "description": "Number of results per page", }, - "page": {"type": "integer", "description": "Page number for paginated results"}, - "page_size": {"type": "integer", "description": "Number of results per page"} }, - "required": ["origin","destination","date"] - } - } + "required": ["origin", "destination", "date"], + }, + }, }, { "type": "function", @@ -241,11 +262,14 @@ "parameters": { "type": "object", "properties": { - "offer_id": {"type": "string", "description": "Duffel offer ID to refresh"} + "offer_id": { + "type": "string", + "description": "Duffel offer ID to refresh", + } }, - "required": ["offer_id"] - } - } + "required": ["offer_id"], + }, + }, }, { "type": "function", @@ -255,23 +279,32 @@ "parameters": { "type": "object", "properties": { - "offer_id": {"type": "string", "description": "Offer ID from Duffel"}, + "offer_id": { + "type": "string", + "description": "Offer ID from Duffel", + }, "passengers": { "type": "array", "items": {"type": "object"}, - "description": "List of passenger objects (given_name, family_name, born_on, gender, email, phone_number, id)" + "description": "List of passenger objects (given_name, family_name, born_on, gender, email, phone_number, id)", }, "services": { "type": "array", "items": {"type": "object"}, - "description": "Optional: list of service objects (bags etc.)" + "description": "Optional: list of service objects (bags etc.)", + }, + "pay_with_balance_now": { + "type": "boolean", + "description": "If true, ticket immediately by paying now", + }, + "notify_email": { + "type": "string", + "description": "Optional: email to send booking confirmation", }, - "pay_with_balance_now": {"type": "boolean", "description": "If true, ticket immediately by paying now"}, - "notify_email": {"type": "string", "description": "Optional: email to send booking confirmation"} }, - "required": ["offer_id","passengers"] - } - } + "required": ["offer_id", "passengers"], + }, + }, }, { "type": "function", @@ -283,11 +316,14 @@ "properties": { "order_id": {"type": "string", "description": "Order ID to pay"}, "amount": {"type": "string", "description": "Amount to pay"}, - "currency": {"type": "string", "description": "Currency code of payment"} + "currency": { + "type": "string", + "description": "Currency code of payment", + }, }, - "required": ["order_id","amount","currency"] - } - } + "required": ["order_id", "amount", "currency"], + }, + }, }, { "type": "function", @@ -297,11 +333,14 @@ "parameters": { "type": "object", "properties": { - "order_id": {"type": "string", "description": "Order ID to retrieve"} + "order_id": { + "type": "string", + "description": "Order ID to retrieve", + } }, - "required": ["order_id"] - } - } + "required": ["order_id"], + }, + }, }, { "type": "function", @@ -313,9 +352,9 @@ "properties": { "order_id": {"type": "string", "description": "Order ID to cancel"} }, - "required": ["order_id"] - } - } + "required": ["order_id"], + }, + }, }, { "type": "function", @@ -325,25 +364,26 @@ "parameters": { "type": "object", "properties": { - "order_cancellation_id": {"type": "string", "description": "Cancellation request ID"}, - "notify_email": {"type": "string", "description": "Optional: email to send confirmation"} + "order_cancellation_id": { + "type": "string", + "description": "Cancellation request ID", + }, + "notify_email": { + "type": "string", + "description": "Optional: email to send confirmation", + }, }, - "required": ["order_cancellation_id"] - } - } + "required": ["order_cancellation_id"], + }, + }, }, - { "type": "function", "function": { "name": "list_orders", "description": "List all orders for the current user/session", - "parameters": { - "type": "object", - "properties": {}, - "required": [] - } - } + "parameters": {"type": "object", "properties": {}, "required": []}, + }, }, { "type": "function", @@ -355,9 +395,9 @@ "properties": { "order_id": {"type": "string", "description": "Order ID to cancel"} }, - "required": ["order_id"] - } - } + "required": ["order_id"], + }, + }, }, { "type": "function", @@ -367,19 +407,23 @@ "parameters": { "type": "object", "properties": { - "order_cancellation_id": {"type": "string", "description": "Cancellation ID from the quote (ocr_...)"} + "order_cancellation_id": { + "type": "string", + "description": "Cancellation ID from the quote (ocr_...)", + } }, - "required": ["order_cancellation_id"] - } - } - } + "required": ["order_cancellation_id"], + }, + }, + }, ] + def run_agent_turn( user_text: str, session_state: Dict[str, Any], history: List[Dict[str, Any]], - session_id: Optional[str] = None + session_id: Optional[str] = None, ) -> Dict[str, Any]: """ Wrapper to process a single user turn: @@ -394,9 +438,10 @@ def run_agent_turn( # Prepare system prompt with dynamic context from datetime import datetime, timezone + current_date = datetime.now(timezone.utc).strftime("%Y-%m-%d") current_date_readable = datetime.now(timezone.utc).strftime("%A, %B %d, %Y") - + system_prompt = f"""CURRENT DATE: {current_date_readable} ({current_date}) When user says "tomorrow", calculate it as {current_date} + 1 day. @@ -404,7 +449,7 @@ def run_agent_turn( Always use YYYY-MM-DD format for dates in tool calls. {SYSTEM_PROMPT}""" - + if session_state.get("current_offers"): offers_info = "\n\nCURRENT OFFERS IN SESSION:\n" for idx, offer in enumerate(session_state["current_offers"], 1): @@ -419,7 +464,7 @@ def run_agent_turn( model=os.getenv("OPENAI_MODEL", "gpt-4o-mini"), messages=messages, tools=TOOLS, - tool_choice="auto" + tool_choice="auto", ) message = response.choices[0].message assistant_content: Optional[str] = message.content @@ -427,44 +472,63 @@ def run_agent_turn( if message.tool_calls: # Check if assistant generated "please wait" type messages with tool call - wait_phrases = ["please wait", "searching", "let me search", "let me find", "one moment", "just a moment", "hold on"] - has_wait_message = assistant_content and any(phrase in assistant_content.lower() for phrase in wait_phrases) - + wait_phrases = [ + "please wait", + "searching", + "let me search", + "let me find", + "one moment", + "just a moment", + "hold on", + ] + has_wait_message = assistant_content and any( + phrase in assistant_content.lower() for phrase in wait_phrases + ) + if has_wait_message: # Retry without the wait message - add a system reminder - logger.info(f"Detected 'please wait' message, retrying tool call without it") - history.append({ - "role": "system", - "content": "Do not generate 'please wait' or 'searching' messages when calling tools. Just call the tool." - }) - + logger.info( + "Detected 'please wait' message, retrying tool call without it" + ) + history.append( + { + "role": "system", + "content": "Do not generate 'please wait' or 'searching' messages when calling tools. Just call the tool.", + } + ) + # Retry the LLM call - messages_with_system = [{"role": "system", "content": system_prompt}] + history + messages_with_system = [ + {"role": "system", "content": system_prompt} + ] + history response = client.chat.completions.create( model=os.getenv("OPENAI_MODEL", "gpt-4o-mini"), messages=messages_with_system, tools=TOOLS, - tool_choice="auto" + tool_choice="auto", ) message = response.choices[0].message assistant_content = message.content - + # Add assistant message with tool_calls to history - history.append({ - "role": "assistant", - "content": assistant_content, - "tool_calls": [ - { - "id": tc.id, - "type": "function", - "function": { - "name": tc.function.name, - "arguments": tc.function.arguments + history.append( + { + "role": "assistant", + "content": assistant_content, + "tool_calls": [ + { + "id": tc.id, + "type": "function", + "function": { + "name": tc.function.name, + "arguments": tc.function.arguments, + }, } - } for tc in message.tool_calls - ] - }) - + for tc in message.tool_calls + ], + } + ) + tool_call_info = message.tool_calls[0] func_name = tool_call_info.function.name func_args = json.loads(tool_call_info.function.arguments) @@ -476,68 +540,93 @@ def run_agent_turn( if func_name == "duffel_search_offers": # Import and unwrap the LangChain tool decorator import importlib - dt = importlib.import_module('tools.duffel_tools') + + dt = importlib.import_module("tools.duffel_tools") func = dt.duffel_search_offers # Unwrap if it's a LangChain tool (has .func attribute) - if hasattr(func, 'func'): + if hasattr(func, "func"): func = func.func num_pax = int(func_args.get("passengers", 1)) result = func( - slices=[{"origin": func_args["origin"], "destination": func_args["destination"], "departure_date": func_args["date"]}], + slices=[ + { + "origin": func_args["origin"], + "destination": func_args["destination"], + "departure_date": func_args["date"], + } + ], passengers=[{"type": "adult"} for _ in range(max(1, num_pax))], - page=func_args.get("page",1), - page_size=func_args.get("page_size",5), + page=func_args.get("page", 1), + page_size=func_args.get("page_size", 5), cabin_class=func_args.get("cabin_class"), - preferred_airlines=func_args.get("preferred_airlines") + preferred_airlines=func_args.get("preferred_airlines"), ) elif func_name == "duffel_get_offer_with_services": # Import and unwrap the LangChain tool decorator import importlib - dt = importlib.import_module('tools.duffel_tools') + + dt = importlib.import_module("tools.duffel_tools") func = dt.duffel_get_offer_with_services # Unwrap if it's a LangChain tool (has .func attribute) - if hasattr(func, 'func'): + if hasattr(func, "func"): func = func.func result = func(offer_id=func_args["offer_id"]) elif func_name == "duffel_create_order": import importlib - dt = importlib.import_module('tools.duffel_tools') + + dt = importlib.import_module("tools.duffel_tools") func = dt.duffel_create_order - if hasattr(func, 'func'): + if hasattr(func, "func"): func = func.func # Ensure passengers exist; synthesize from session/history if missing passengers_arg = func_args.get("passengers") if not passengers_arg: pax: Dict[str, Any] = {} try: - if isinstance(session_state.get("autofill_passenger_data"), dict): - pax.update(session_state["autofill_passenger_data"]) + if isinstance( + session_state.get("autofill_passenger_data"), dict + ): + pax.update(session_state["autofill_passenger_data"]) if not pax: for h in reversed(history): txt = h.get("content") if isinstance(h, dict) else None if not isinstance(txt, str): continue - m = re.search(r"\b(mr|ms|mrs|miss|dr|mx)\.?\b", txt, re.I) + m = re.search( + r"\b(mr|ms|mrs|miss|dr|mx)\.?\b", txt, re.I + ) if m: pax["title"] = m.group(1).lower() m = re.search(r"\b([A-Za-z]+)\s+([A-Za-z]+)\b", txt) if m: pax["given_name"] = m.group(1).strip().title() pax["family_name"] = m.group(2).strip().title() - m = re.search(r"\b(19|20)\d{2}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])\b", txt) + m = re.search( + r"\b(19|20)\d{2}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])\b", + txt, + ) if m: pax["born_on"] = m.group(0) m = re.search(r"\b(male|female|m|f|x)\b", txt, re.I) if m: g = m.group(1).upper()[0] - pax["gender"] = g if g in ["M","F","X"] else "M" - m = re.search(r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b", txt) + pax["gender"] = g if g in ["M", "F", "X"] else "M" + m = re.search( + r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b", + txt, + ) if m: pax["email"] = m.group(0) m = re.search(r"\+\d{10,}|\+\d[\d\s-]{9,}", txt) if m: - pax["phone_number"] = m.group(0).replace(" ", "").replace("-", "") - if pax.get("given_name") and pax.get("family_name") and pax.get("born_on"): + pax["phone_number"] = ( + m.group(0).replace(" ", "").replace("-", "") + ) + if ( + pax.get("given_name") + and pax.get("family_name") + and pax.get("born_on") + ): break except Exception: pax = {} @@ -545,7 +634,11 @@ def run_agent_turn( offer_pax = session_state.get("offer_passengers") if isinstance(offer_pax, list) and not pax.get("id"): for op in offer_pax: - if isinstance(op, dict) and op.get("type") == "adult" and op.get("id"): + if ( + isinstance(op, dict) + and op.get("type") == "adult" + and op.get("id") + ): pax["id"] = op["id"] break except Exception: @@ -554,7 +647,10 @@ def run_agent_turn( passengers_arg = [pax] func_args["passengers"] = passengers_arg if not passengers_arg: - result = {"error": "MISSING_PASSENGERS", "message": "Passenger details are required to place booking."} + result = { + "error": "MISSING_PASSENGERS", + "message": "Passenger details are required to place booking.", + } else: # Auto-ticket on first attempt result = func( @@ -562,83 +658,104 @@ def run_agent_turn( passengers=passengers_arg, services=func_args.get("services"), pay_with_balance_now=True, - notify_email=func_args.get("notify_email") or (passengers_arg[0].get("email") if isinstance(passengers_arg, list) and passengers_arg else None) + notify_email=func_args.get("notify_email") + or ( + passengers_arg[0].get("email") + if isinstance(passengers_arg, list) and passengers_arg + else None + ), ) elif func_name == "duffel_pay_hold_order": import importlib - dt = importlib.import_module('tools.duffel_tools') + + dt = importlib.import_module("tools.duffel_tools") func = dt.duffel_pay_hold_order - if hasattr(func, 'func'): + if hasattr(func, "func"): func = func.func result = func( order_id=func_args["order_id"], amount=func_args["amount"], - currency=func_args["currency"] + currency=func_args["currency"], ) elif func_name == "duffel_get_order": import importlib - dt = importlib.import_module('tools.duffel_tools') + + dt = importlib.import_module("tools.duffel_tools") func = dt.duffel_get_order - if hasattr(func, 'func'): + if hasattr(func, "func"): func = func.func result = func(order_id=func_args["order_id"]) elif func_name == "duffel_create_order_cancellation": import importlib - dt = importlib.import_module('tools.duffel_tools') + + dt = importlib.import_module("tools.duffel_tools") func = dt.duffel_create_order_cancellation - if hasattr(func, 'func'): + if hasattr(func, "func"): func = func.func result = func(order_id=func_args["order_id"]) elif func_name == "duffel_confirm_order_cancellation": import importlib - dt = importlib.import_module('tools.duffel_tools') + + dt = importlib.import_module("tools.duffel_tools") func = dt.duffel_confirm_order_cancellation - if hasattr(func, 'func'): + if hasattr(func, "func"): func = func.func result = func( order_cancellation_id=func_args["order_cancellation_id"], - notify_email=func_args.get("notify_email") + notify_email=func_args.get("notify_email"), ) elif func_name == "list_orders": # This is handled by chat_proto which has access to storage # Return a signal that chat_proto should handle this result = { "status": "list_orders_requested", - "message": "Order list will be retrieved from storage" + "message": "Order list will be retrieved from storage", } session_state["list_orders_requested"] = True elif func_name == "get_order_cancellation_quote": import importlib - dt = importlib.import_module('tools.duffel_tools') + + dt = importlib.import_module("tools.duffel_tools") func = dt.get_order_cancellation_quote - if hasattr(func, 'func'): + if hasattr(func, "func"): func = func.func result = func(order_id=func_args["order_id"]) # Store the cancellation ID for confirmation if not result.get("error"): - session_state["pending_cancellation_id"] = result.get("order_cancellation_id") - session_state["pending_cancellation_order_id"] = func_args["order_id"] + session_state["pending_cancellation_id"] = result.get( + "order_cancellation_id" + ) + session_state["pending_cancellation_order_id"] = func_args[ + "order_id" + ] elif func_name == "confirm_order_cancellation": import importlib - dt = importlib.import_module('tools.duffel_tools') + + dt = importlib.import_module("tools.duffel_tools") func = dt.confirm_order_cancellation - if hasattr(func, 'func'): + if hasattr(func, "func"): func = func.func result = func(order_cancellation_id=func_args["order_cancellation_id"]) # Store cancellation details for email sending if not result.get("error"): session_state["cancellation_confirmed"] = True session_state["cancelled_order_id"] = result.get("order_id") - session_state["cancelled_refund_amount"] = result.get("refund_amount") - session_state["cancelled_refund_currency"] = result.get("refund_currency") + session_state["cancelled_refund_amount"] = result.get( + "refund_amount" + ) + session_state["cancelled_refund_currency"] = result.get( + "refund_currency" + ) elif func_name == "request_payment": # This tool doesn't execute directly - it signals the chat protocol to send RequestPayment # Store the request flag in session state so chat_proto can pick it up session_state["payment_requested"] = True - session_state["payment_description"] = func_args.get("description", "Flight booking — pay to proceed") + session_state["payment_description"] = func_args.get( + "description", "Flight booking — pay to proceed" + ) result = { "status": "payment_request_queued", - "message": "Payment request will be sent to user's wallet with FET and USDC options" + "message": "Payment request will be sent to user's wallet with FET and USDC options", } else: result = {"error": f"Unknown tool name: {func_name}"} @@ -647,17 +764,19 @@ def run_agent_turn( result = {"error": str(e)} # Append tool result - history.append({ - "role": "tool", - "name": func_name, - "content": json.dumps(result), - "tool_call_id": tool_call_info.id - }) + history.append( + { + "role": "tool", + "name": func_name, + "content": json.dumps(result), + "tool_call_id": tool_call_info.id, + } + ) # Update state session_state["last_tool"] = func_name session_state["last_tool_result"] = result - + # Track search params for pagination if func_name == "duffel_search_offers": session_state["last_search"] = { @@ -666,12 +785,12 @@ def run_agent_turn( "date": func_args.get("date"), "passengers": func_args.get("passengers"), "current_page": result.get("page", 1), - "total_pages": result.get("page_total", 1) + "total_pages": result.get("page_total", 1), } # Store offers for selection mapping (1-5 → offer_id) if not result.get("error") and result.get("offers"): session_state["current_offers"] = result["offers"] - + # Track selected offer for payment/booking if func_name == "duffel_get_offer_with_services": session_state["selected_offer_id"] = func_args.get("offer_id") @@ -680,7 +799,9 @@ def run_agent_turn( # Store offer_passengers for booking (need IDs for Duffel API) if result.get("offer_passengers"): session_state["offer_passengers"] = result["offer_passengers"] - logger.info(f"Stored {len(result['offer_passengers'])} passenger IDs from offer") + logger.info( + f"Stored {len(result['offer_passengers'])} passenger IDs from offer" + ) # For request_payment, skip follow-up message (payment request is sent directly to wallet) if func_name == "request_payment": @@ -688,13 +809,15 @@ def run_agent_turn( history.append({"role": "assistant", "content": assistant_content}) else: # Ask assistant again (follow up) - include system prompt - messages_with_system = [{"role": "system", "content": system_prompt}] + history + messages_with_system = [ + {"role": "system", "content": system_prompt} + ] + history follow = client.chat.completions.create( model=os.getenv("OPENAI_MODEL", "gpt-4o-mini"), - messages=messages_with_system + messages=messages_with_system, ) assistant_content = follow.choices[0].message.content - + history.append({"role": "assistant", "content": assistant_content}) else: # No tool call @@ -704,5 +827,5 @@ def run_agent_turn( "content": assistant_content or "", "state": session_state, "history": history, - "tool_executed": tool_executed - } \ No newline at end of file + "tool_executed": tool_executed, + } diff --git a/duffel-agent/tools/skyfire.py b/duffel-agent/tools/skyfire.py index dcee819f..3ae8c2a3 100644 --- a/duffel-agent/tools/skyfire.py +++ b/duffel-agent/tools/skyfire.py @@ -37,7 +37,9 @@ JWKS_URL = os.getenv("JWKS_URL", f"{_DEFAULT_APP_BASE}/.well-known/jwks.json") JWT_ISSUER = os.getenv("JWT_ISSUER", f"{_DEFAULT_APP_BASE}") -SKYFIRE_TOKENS_API_URL = os.getenv("SKYFIRE_TOKENS_API_URL", f"{_DEFAULT_API_BASE}/api/v1/tokens/charge") +SKYFIRE_TOKENS_API_URL = os.getenv( + "SKYFIRE_TOKENS_API_URL", f"{_DEFAULT_API_BASE}/api/v1/tokens/charge" +) SELLER_ACCOUNT_ID = os.getenv("SELLER_ACCOUNT_ID", "") # used as JWT audience JWT_AUDIENCE = os.getenv("JWT_AUDIENCE", SELLER_ACCOUNT_ID or "") @@ -50,20 +52,24 @@ # -------- Helpers to expose configured IDs -------- + def get_skyfire_service_id() -> Optional[str]: return SELLER_SERVICE_ID or None + def get_seller_account_id() -> Optional[str]: return SELLER_ACCOUNT_ID or (JWT_AUDIENCE or None) # -------- Internal JWKS helpers -------- + async def _fetch_json(session: aiohttp.ClientSession, url: str) -> dict[str, Any]: async with session.get(url, timeout=20) as resp: resp.raise_for_status() return await resp.json() + def _jwk_by_kid(jwks: dict[str, Any], kid: str): for k in jwks.get("keys", []) or []: if k.get("kid") == kid: @@ -73,6 +79,7 @@ def _jwk_by_kid(jwks: dict[str, Any], kid: str): # -------- Public verification & charging -------- + async def verify_token_claims(token: str, logger) -> bool: """ Verify the Skyfire buyer token: @@ -105,7 +112,9 @@ async def verify_token_claims(token: str, logger) -> bool: if SELLER_SERVICE_ID: ssi = claims.get("ssi") if ssi != SELLER_SERVICE_ID: - raise JWTError(f"Service mismatch: token.ssi={ssi}, expected={SELLER_SERVICE_ID}") + raise JWTError( + f"Service mismatch: token.ssi={ssi}, expected={SELLER_SERVICE_ID}" + ) logger.info("Skyfire token verified OK") return True @@ -188,4 +197,6 @@ async def verify_and_charge( if not ok: return False - return await charge_token(token, amount_usdc, logger, idempotency_key=idempotency_key) \ No newline at end of file + return await charge_token( + token, amount_usdc, logger, idempotency_key=idempotency_key + ) diff --git a/fet-example/agent.py b/fet-example/agent.py index 80ebb730..c82e7b2e 100644 --- a/fet-example/agent.py +++ b/fet-example/agent.py @@ -5,8 +5,8 @@ load_dotenv() -from chat_proto import chat_proto -from payment import payment_proto, set_agent_wallet +from chat_proto import chat_proto # noqa: E402 +from payment import payment_proto, set_agent_wallet # noqa: E402 agent = Agent( name=os.getenv("AGENT_NAME", "Fet Example Agent"), diff --git a/fet-example/chat_proto.py b/fet-example/chat_proto.py index b3ab8808..4744d3b8 100644 --- a/fet-example/chat_proto.py +++ b/fet-example/chat_proto.py @@ -11,7 +11,6 @@ request_payment_from_user, generate_response_after_payment, ) -from shared import create_text_chat chat_proto = Protocol(spec=chat_protocol_spec) @@ -26,9 +25,9 @@ async def handle_message(ctx: Context, sender: str, msg: ChatMessage): timestamp=datetime.now(timezone.utc), acknowledged_msg_id=msg.msg_id ), ) - + # Process text content for payment gating and prompt collection - + for item in msg.content: if isinstance(item, TextContent): text = item.text.strip() @@ -58,7 +57,9 @@ async def handle_message(ctx: Context, sender: str, msg: ChatMessage): # Clear any previous recorded marker for this session (new payment request) ctx.storage.remove(f"{sender}:{session_id}:request_recorded") # Attach the explanation as the description/metadata of the payment request - await request_payment_from_user(ctx, sender, description=payment_description) + await request_payment_from_user( + ctx, sender, description=payment_description + ) return diff --git a/fet-example/client.py b/fet-example/client.py index 6cfb2a84..39420329 100644 --- a/fet-example/client.py +++ b/fet-example/client.py @@ -5,7 +5,7 @@ import asyncio import base64 import os -from typing import Any, Optional +from typing import Any import requests from dotenv import load_dotenv @@ -59,19 +59,19 @@ async def call_asi_one_api( "Authorization": f"Bearer {ASI_ONE_API_KEY}", "Content-Type": "application/json", } - + response = requests.post(url, json=payload, headers=headers, timeout=60) if not response.ok: return { "error": f"{response.status_code} Error from ASI image API: {response.text}", "status": "failed", } - + response_data = response.json() - + # Follow the exact logic from the example image_url = response_data.get("image_url") or response_data.get("url") - + if not image_url: data_items = response_data.get("data", []) if data_items and isinstance(data_items, list): @@ -81,24 +81,29 @@ async def call_asi_one_api( # Decode base64 and upload to tmpfiles try: image_bytes = base64.b64decode(first_item["b64_json"]) - image_url = await asyncio.to_thread(upload_to_tmpfiles, image_bytes) + image_url = await asyncio.to_thread( + upload_to_tmpfiles, image_bytes + ) except Exception as e: return { "error": f"Failed to process base64 image: {str(e)}", "status": "failed", } - + if not image_url: return { "error": f"ASI image API returned no image URL: {response_data}", "status": "failed", } - + return { "image_url": image_url, "status": "success", } except requests.RequestException as e: - return {"error": f"Image generation request failed: {str(e)}", "status": "failed"} + return { + "error": f"Image generation request failed: {str(e)}", + "status": "failed", + } except Exception as e: return {"error": str(e), "status": "failed"} diff --git a/fet-example/payment.py b/fet-example/payment.py index 2855dfca..5b43fb11 100644 --- a/fet-example/payment.py +++ b/fet-example/payment.py @@ -3,7 +3,6 @@ """ import os -import traceback from datetime import datetime, timezone from uuid import uuid4 @@ -50,9 +49,12 @@ def verify_fet_payment_to_agent( try: from cosmpy.aerial.client import LedgerClient, NetworkConfig - use_testnet = not use_mainnet - testnet = os.getenv("FET_USE_TESTNET", "true").lower() == "true" if not use_mainnet else False - + testnet = ( + os.getenv("FET_USE_TESTNET", "true").lower() == "true" + if not use_mainnet + else False + ) + network_config = ( NetworkConfig.fetchai_stable_testnet() if testnet @@ -60,28 +62,28 @@ def verify_fet_payment_to_agent( ) ledger = LedgerClient(network_config) expected_amount_micro = int(float(expected_amount_fet) * 10**18) - + if not recipient_agent_wallet: logger.error("Recipient agent wallet is not set") return False - + expected_recipient = str(recipient_agent_wallet.address()) - + logger.info( f"Verifying payment of {expected_amount_fet} FET from {sender_fet_address} " f"to {expected_recipient} on {'testnet' if testnet else 'mainnet'}" ) - + tx_response = ledger.query_tx(transaction_id) if not tx_response.is_successful(): logger.error(f"Transaction {transaction_id} was not successful") return False - + recipient_found = False amount_found = False sender_found = False denom = "atestfet" if testnet else "afet" - + for event_type, event_attrs in tx_response.events.items(): if event_type == "transfer": if event_attrs.get("recipient") == expected_recipient: @@ -96,11 +98,11 @@ def verify_fet_payment_to_agent( amount_found = True except Exception: pass - + if recipient_found and amount_found and sender_found: logger.info(f"Payment verified: {transaction_id}") return True - + logger.error( f"Payment verification failed - recipient: {recipient_found}, " f"amount: {amount_found}, sender: {sender_found}" @@ -109,6 +111,7 @@ def verify_fet_payment_to_agent( except Exception as e: logger.error(f"FET payment verification failed: {e}") import traceback + logger.error(f"Traceback: {traceback.format_exc()}") return False @@ -117,38 +120,47 @@ async def request_payment_from_user( ctx: Context, user_address: str, description: str | None = None ): session = str(ctx.session) - + # Build accepted funds list accepted_funds: list[Funds] = [] - + # Add FET payment option fet_amount = os.getenv("FIXED_FET_AMOUNT", "0.1") - accepted_funds.append(Funds(currency="FET", amount=str(fet_amount), payment_method="fet_direct")) - + accepted_funds.append( + Funds(currency="FET", amount=str(fet_amount), payment_method="fet_direct") + ) + if not accepted_funds: - ctx.logger.warning(f"[payment] no accepted_funds; cannot send RequestPayment user={user_address} session={session}") + ctx.logger.warning( + f"[payment] no accepted_funds; cannot send RequestPayment user={user_address} session={session}" + ) await ctx.send( user_address, AvChatMessage( - content=[TextContent(type="text", text="No payment methods are currently available. Please try again in a moment.")] + content=[ + TextContent( + type="text", + text="No payment methods are currently available. Please try again in a moment.", + ) + ] ), ) return - + # Build metadata metadata: dict[str, str] = { "agent": "asi1-llm-agent", "service": "llm_processing", } - + use_testnet = os.getenv("FET_USE_TESTNET", "true").lower() == "true" fet_network = "stable-testnet" if use_testnet else "mainnet" metadata["fet_network"] = fet_network metadata["mainnet"] = "false" if use_testnet else "true" - + if _agent_wallet: metadata["provider_agent_wallet"] = str(_agent_wallet.address()) - + if description: metadata["content"] = description else: @@ -156,28 +168,36 @@ async def request_payment_from_user( "Please complete the payment to proceed. " "After payment, I will process your request using ASI1 One LLM." ) - - recipient_addr = str(_agent_wallet.address()) if _agent_wallet else str(ctx.agent.address) - + + recipient_addr = ( + str(_agent_wallet.address()) if _agent_wallet else str(ctx.agent.address) + ) + # Log payment request details - funds_log = [{"method": f.payment_method, "currency": f.currency, "amount": f.amount} for f in accepted_funds] + funds_log = [ + {"method": f.payment_method, "currency": f.currency, "amount": f.amount} + for f in accepted_funds + ] ctx.logger.info( f"[payment] outbound RequestPayment user={user_address} session={session} " f"funds={funds_log} metadata={metadata} deadline_seconds=300" ) - + payment_request = RequestPayment( accepted_funds=accepted_funds, recipient=recipient_addr, deadline_seconds=300, reference=session, - description=description or "ASI1 One LLM: after payment, I will process your request", + description=description + or "ASI1 One LLM: after payment, I will process your request", metadata=metadata, ) - + # Send RequestPayment await ctx.send(user_address, payment_request) - ctx.logger.info(f"[payment] RequestPayment sent to {user_address} with recipient {recipient_addr}") + ctx.logger.info( + f"[payment] RequestPayment sent to {user_address} with recipient {recipient_addr}" + ) def _allow_retry(ctx: Context, sender: str, session_id: str) -> bool: @@ -202,9 +222,9 @@ async def handle_commit_payment(ctx: Context, sender: str, msg: CommitPayment): try: buyer_fet_wallet = None if isinstance(msg.metadata, dict): - buyer_fet_wallet = msg.metadata.get("buyer_fet_wallet") or msg.metadata.get( - "buyer_fet_address" - ) + buyer_fet_wallet = msg.metadata.get( + "buyer_fet_wallet" + ) or msg.metadata.get("buyer_fet_address") if not buyer_fet_wallet: ctx.logger.error("Missing buyer_fet_wallet in CommitPayment.metadata") else: @@ -293,8 +313,10 @@ async def process_api_result(ctx: Context, sender: str, result: dict): if image_url: try: # Send image as markdown in text content (matches ASI1 example) - image_markdown = f"Image generated successfully.\n\n![Generated image]({image_url})\n\n" - + image_markdown = ( + f"Image generated successfully.\n\n![Generated image]({image_url})\n\n" + ) + await ctx.send( sender, AvChatMessage( @@ -320,14 +342,19 @@ async def process_api_result(ctx: Context, sender: str, result: dict): else: await ctx.send( sender, - create_text_chat("Could not send image. Please try again or start a new session."), + create_text_chat( + "Could not send image. Please try again or start a new session." + ), ) return # Fallback: handle text response (for backward compatibility) response_text = result.get("response_text") if not response_text: - await ctx.send(sender, create_text_chat("Response generated but could not retrieve image or text")) + await ctx.send( + sender, + create_text_chat("Response generated but could not retrieve image or text"), + ) if _allow_retry(ctx, sender, session_id): await ctx.send( sender, @@ -364,5 +391,7 @@ async def process_api_result(ctx: Context, sender: str, result: dict): else: await ctx.send( sender, - create_text_chat("Could not send response. Please try again or start a new session."), + create_text_chat( + "Could not send response. Please try again or start a new session." + ), ) diff --git a/fet-example/shared.py b/fet-example/shared.py index adcb1594..f90cbf3e 100644 --- a/fet-example/shared.py +++ b/fet-example/shared.py @@ -16,10 +16,10 @@ def create_text_chat(text: str, end_session: bool = False) -> ChatMessage: content: list[AgentContent] = [TextContent(type="text", text=text)] if end_session: from uagents_core.contrib.protocols.chat import EndSessionContent + content.append(EndSessionContent(type="end-session")) return ChatMessage( timestamp=datetime.now(timezone.utc), msg_id=uuid4(), content=content, ) - diff --git a/fetch-hackathon-quickstarter/agents/alice/alice_fetchai_wrapped_agent.py b/fetch-hackathon-quickstarter/agents/alice/alice_fetchai_wrapped_agent.py index dca3c9f2..81daac1d 100644 --- a/fetch-hackathon-quickstarter/agents/alice/alice_fetchai_wrapped_agent.py +++ b/fetch-hackathon-quickstarter/agents/alice/alice_fetchai_wrapped_agent.py @@ -26,7 +26,9 @@ def super_cool_alice_workflow(state: SharedAgentState) -> SharedAgentState: @alice.on_message(SharedAgentState) async def handle_message(ctx: Context, sender: str, state: SharedAgentState): - ctx.logger.info(f"Received state from orchestrator: session={state.chat_session_id}, query={state.query!r}") + ctx.logger.info( + f"Received state from orchestrator: session={state.chat_session_id}, query={state.query!r}" + ) state = super_cool_alice_workflow(state) await ctx.send(sender, state) diff --git a/fetch-hackathon-quickstarter/agents/bob/bob_fetchai_wrapped_agent.py b/fetch-hackathon-quickstarter/agents/bob/bob_fetchai_wrapped_agent.py index 9ea279fd..da1870e8 100644 --- a/fetch-hackathon-quickstarter/agents/bob/bob_fetchai_wrapped_agent.py +++ b/fetch-hackathon-quickstarter/agents/bob/bob_fetchai_wrapped_agent.py @@ -26,7 +26,9 @@ def super_cool_bob_workflow(state: SharedAgentState) -> SharedAgentState: @bob.on_message(SharedAgentState) async def handle_message(ctx: Context, sender: str, state: SharedAgentState): - ctx.logger.info(f"Received state from orchestrator: session={state.chat_session_id}, query={state.query!r}") + ctx.logger.info( + f"Received state from orchestrator: session={state.chat_session_id}, query={state.query!r}" + ) state = super_cool_bob_workflow(state) await ctx.send(sender, state) diff --git a/fetch-hackathon-quickstarter/agents/orchestrator/chat_protocol.py b/fetch-hackathon-quickstarter/agents/orchestrator/chat_protocol.py index 9118a041..493e2a0a 100644 --- a/fetch-hackathon-quickstarter/agents/orchestrator/chat_protocol.py +++ b/fetch-hackathon-quickstarter/agents/orchestrator/chat_protocol.py @@ -23,9 +23,7 @@ async def handle_message(ctx: Context, sender: str, msg: ChatMessage): ChatAcknowledgement(timestamp=datetime.now(), acknowledged_msg_id=msg.msg_id), ) - text = " ".join( - item.text for item in msg.content if isinstance(item, TextContent) - ) + text = " ".join(item.text for item in msg.content if isinstance(item, TextContent)) ctx.logger.info(f"Received: {text}") chat_session_id = str(ctx.session) diff --git a/fetch-hackathon-quickstarter/agents/orchestrator/orchestrator_fetchai_wrapped_agent.py b/fetch-hackathon-quickstarter/agents/orchestrator/orchestrator_fetchai_wrapped_agent.py index 368d397d..91264a50 100644 --- a/fetch-hackathon-quickstarter/agents/orchestrator/orchestrator_fetchai_wrapped_agent.py +++ b/fetch-hackathon-quickstarter/agents/orchestrator/orchestrator_fetchai_wrapped_agent.py @@ -3,9 +3,16 @@ from agents.models.config import ORCHESTRATOR_SEED from agents.models.models import SharedAgentState -from agents.orchestrator.chat_protocol import chat_proto, generate_orchestrator_response_from_state +from agents.orchestrator.chat_protocol import ( + chat_proto, + generate_orchestrator_response_from_state, +) from uagents import Agent, Context, Model -from uagents_core.contrib.protocols.chat import ChatMessage, EndSessionContent, TextContent +from uagents_core.contrib.protocols.chat import ( + ChatMessage, + EndSessionContent, + TextContent, +) orchestrator = Agent( name="orchestrator", @@ -71,7 +78,9 @@ async def handle_agent_response(ctx: Context, sender: str, state: SharedAgentSta The orchestrator is the sole bridge between the internal agent flow and ASI:One — so once a helper agent finishes, we relay the result directly back to the original user. """ - ctx.logger.info(f"Received state back from agent: session={state.chat_session_id}, result={state.result!r}") + ctx.logger.info( + f"Received state back from agent: session={state.chat_session_id}, result={state.result!r}" + ) response = generate_orchestrator_response_from_state(state) await ctx.send( state.user_sender_address, diff --git a/flight-tracker-openai-workflow-agent/uagent_bridge.py b/flight-tracker-openai-workflow-agent/uagent_bridge.py index 74edee28..b518666a 100644 --- a/flight-tracker-openai-workflow-agent/uagent_bridge.py +++ b/flight-tracker-openai-workflow-agent/uagent_bridge.py @@ -20,7 +20,9 @@ def _load_workflow_module(): here = os.path.dirname(__file__) workflow_path = os.path.join(here, "workflow.py") - spec = importlib.util.spec_from_file_location("flight_tracker_workflow", workflow_path) + spec = importlib.util.spec_from_file_location( + "flight_tracker_workflow", workflow_path + ) if spec is None or spec.loader is None: raise RuntimeError("Failed to load workflow module spec") mod = importlib.util.module_from_spec(spec) @@ -41,7 +43,9 @@ def text_msg(text: str, *, end_session: bool = False) -> ChatMessage: content = [TextContent(type="text", text=text)] if end_session: content.append(EndSessionContent(type="end-session")) - return ChatMessage(timestamp=datetime.now(timezone.utc), msg_id=uuid4(), content=content) + return ChatMessage( + timestamp=datetime.now(timezone.utc), msg_id=uuid4(), content=content + ) @chat_proto.on_message(ChatMessage) @@ -62,10 +66,16 @@ async def on_chat(ctx: Context, sender: str, msg: ChatMessage): ChatMessage( timestamp=datetime.now(timezone.utc), msg_id=uuid4(), - content=[MetadataContent(type="metadata", metadata={"attachments": "false"})], + content=[ + MetadataContent( + type="metadata", metadata={"attachments": "false"} + ) + ], ), ) - await ctx.send(sender, text_msg("Hi! Send a flight number like 'AI102 today'.")) + await ctx.send( + sender, text_msg("Hi! Send a flight number like 'AI102 today'.") + ) return if isinstance(item, TextContent): @@ -96,5 +106,3 @@ async def on_ack(ctx: Context, sender: str, msg: ChatAcknowledgement): if __name__ == "__main__": agent.run() - - diff --git a/flight-tracker-openai-workflow-agent/workflow.py b/flight-tracker-openai-workflow-agent/workflow.py index 394c9ac6..cfab9216 100644 --- a/flight-tracker-openai-workflow-agent/workflow.py +++ b/flight-tracker-openai-workflow-agent/workflow.py @@ -1,21 +1,28 @@ -from agents import WebSearchTool, Agent, ModelSettings, TResponseInputItem, Runner, RunConfig +from agents import ( + WebSearchTool, + Agent, + ModelSettings, + TResponseInputItem, + Runner, + RunConfig, +) from openai.types.shared.reasoning import Reasoning from pydantic import BaseModel # Tool definitions web_search_preview = WebSearchTool( - user_location={ - "type": "approximate", - "country": None, - "region": None, - "city": None, - "timezone": None - }, - search_context_size="high" + user_location={ + "type": "approximate", + "country": None, + "region": None, + "city": None, + "timezone": None, + }, + search_context_size="high", ) my_agent = Agent( - name="My agent", - instructions="""You are FlightStatusAgent. Your job is to fetch today’s live flight status for the flight number mentioned by the user, using Web Search to find up-to-date data (Flightradar24, FlightAware, Flight.info, etc.). + name="My agent", + instructions="""You are FlightStatusAgent. Your job is to fetch today’s live flight status for the flight number mentioned by the user, using Web Search to find up-to-date data (Flightradar24, FlightAware, Flight.info, etc.). Output format (Markdown only, no citations or notes): ** ** - @@ -39,54 +46,41 @@ Always include Flightradar24 link with the {flight_num} placeholder at the end. Keep output concise, factual, and visually clean. Don’t include internal reasoning, sources list beyond the visible Markdown links, or citations.""", - model="gpt-5", - tools=[ - web_search_preview - ], - model_settings=ModelSettings( - store=True, - reasoning=Reasoning( - effort="low", - summary="auto" - ) - ) + model="gpt-5", + tools=[web_search_preview], + model_settings=ModelSettings( + store=True, reasoning=Reasoning(effort="low", summary="auto") + ), ) class WorkflowInput(BaseModel): - input_as_text: str + input_as_text: str # Main code entrypoint async def run_workflow(workflow_input: WorkflowInput): - workflow = workflow_input.model_dump() - conversation_history: list[TResponseInputItem] = [ - { - "role": "user", - "content": [ + workflow = workflow_input.model_dump() + conversation_history: list[TResponseInputItem] = [ { - "type": "input_text", - "text": workflow["input_as_text"] + "role": "user", + "content": [{"type": "input_text", "text": workflow["input_as_text"]}], } - ] - } - ] - my_agent_result_temp = await Runner.run( - my_agent, - input=[ - *conversation_history - ], - run_config=RunConfig(trace_metadata={ - "__trace_source__": "agent-builder", - "workflow_id": "wf_68ee256af5f48190a7cb98d6309861eb0e505757706e1cf6" - }) - ) - - conversation_history.extend([item.to_input_item() for item in my_agent_result_temp.new_items]) - - my_agent_result = { - "output_text": my_agent_result_temp.final_output_as(str) - } - return my_agent_result + ] + my_agent_result_temp = await Runner.run( + my_agent, + input=[*conversation_history], + run_config=RunConfig( + trace_metadata={ + "__trace_source__": "agent-builder", + "workflow_id": "wf_68ee256af5f48190a7cb98d6309861eb0e505757706e1cf6", + } + ), + ) + conversation_history.extend( + [item.to_input_item() for item in my_agent_result_temp.new_items] + ) + my_agent_result = {"output_text": my_agent_result_temp.final_output_as(str)} + return my_agent_result diff --git a/frontend-integration/frontend_app.py b/frontend-integration/frontend_app.py index 90e2d4a3..f0de1c71 100644 --- a/frontend-integration/frontend_app.py +++ b/frontend-integration/frontend_app.py @@ -1,132 +1,139 @@ from flask import Flask, render_template, request, jsonify import requests -import json import os app = Flask(__name__) # Agent endpoints -AGENTS = { - "search": "http://127.0.0.1:8001", - "info": "http://127.0.0.1:8002" -} +AGENTS = {"search": "http://127.0.0.1:8001", "info": "http://127.0.0.1:8002"} -@app.route('/') + +@app.route("/") def index(): - return render_template('index.html') + return render_template("index.html") + -@app.route('/search_products', methods=['POST']) +@app.route("/search_products", methods=["POST"]) def search_products(): """Search products via search agent""" try: - query = request.form.get('query', '').strip() + query = request.form.get("query", "").strip() if not query: return jsonify({"error": "Please provide a search query"}) - + # Call search agent with POST request payload = {"query": query} response = requests.post(f"{AGENTS['search']}/search", json=payload, timeout=10) response.raise_for_status() - + # Agent returns JSON directly result = response.json() - + # Format the results for display formatted_results = [] - if result.get('products'): - for product in result['products'][:10]: # Limit to 10 results + if result.get("products"): + for product in result["products"][:10]: # Limit to 10 results formatted_product = { - 'name': product.get('product_name', 'N/A'), - 'brands': product.get('brands', 'N/A'), - 'barcode': product.get('code', 'N/A'), - 'categories': product.get('categories', 'N/A'), - 'image_url': product.get('image_url', '') + "name": product.get("product_name", "N/A"), + "brands": product.get("brands", "N/A"), + "barcode": product.get("code", "N/A"), + "categories": product.get("categories", "N/A"), + "image_url": product.get("image_url", ""), } formatted_results.append(formatted_product) - - return jsonify({ - "success": True, - "count": result.get('count', 0), - "query": query, - "products": formatted_results - }) - + + return jsonify( + { + "success": True, + "count": result.get("count", 0), + "query": query, + "products": formatted_results, + } + ) + except requests.RequestException as e: return jsonify({"error": f"Failed to connect to search agent: {str(e)}"}) except Exception as e: return jsonify({"error": f"Search failed: {str(e)}"}) -@app.route('/get_product_info', methods=['POST']) + +@app.route("/get_product_info", methods=["POST"]) def get_product_info(): """Get product info via info agent""" try: - barcode = request.form.get('barcode', '').strip() + barcode = request.form.get("barcode", "").strip() if not barcode: return jsonify({"error": "Please provide a barcode"}) - + # Call info agent with POST request payload = {"barcode": barcode} response = requests.post(f"{AGENTS['info']}/product", json=payload, timeout=10) response.raise_for_status() - + # Agent returns JSON directly result = response.json() - + # Format the result for display - if result.get('error'): - return jsonify({"error": result['error']}) - + if result.get("error"): + return jsonify({"error": result["error"]}) + formatted_result = { - 'barcode': result.get('barcode', 'N/A'), - 'name': result.get('product_name', 'N/A'), - 'brands': result.get('brands', 'N/A'), - 'categories': result.get('categories', 'N/A'), - 'ingredients': result.get('ingredients_text', 'N/A'), - 'allergens': result.get('allergens', 'N/A'), - 'nutrition_grade': result.get('nutrition_grades', 'N/A'), - 'eco_score': result.get('ecoscore_grade', 'N/A'), - 'countries': result.get('countries', 'N/A'), - 'packaging': result.get('packaging', 'N/A'), - 'quantity': result.get('quantity', 'N/A'), - 'stores': result.get('stores', 'N/A'), - 'image_url': result.get('image_url', ''), - 'nutrition': { - 'energy_100g': result.get('energy_100g', 'N/A'), - 'fat_100g': result.get('fat_100g', 'N/A'), - 'sugars_100g': result.get('sugars_100g', 'N/A'), - 'salt_100g': result.get('salt_100g', 'N/A') - } + "barcode": result.get("barcode", "N/A"), + "name": result.get("product_name", "N/A"), + "brands": result.get("brands", "N/A"), + "categories": result.get("categories", "N/A"), + "ingredients": result.get("ingredients_text", "N/A"), + "allergens": result.get("allergens", "N/A"), + "nutrition_grade": result.get("nutrition_grades", "N/A"), + "eco_score": result.get("ecoscore_grade", "N/A"), + "countries": result.get("countries", "N/A"), + "packaging": result.get("packaging", "N/A"), + "quantity": result.get("quantity", "N/A"), + "stores": result.get("stores", "N/A"), + "image_url": result.get("image_url", ""), + "nutrition": { + "energy_100g": result.get("energy_100g", "N/A"), + "fat_100g": result.get("fat_100g", "N/A"), + "sugars_100g": result.get("sugars_100g", "N/A"), + "salt_100g": result.get("salt_100g", "N/A"), + }, } - + return jsonify({"success": True, "product": formatted_result}) - + except requests.RequestException as e: return jsonify({"error": f"Failed to connect to info agent: {str(e)}"}) except Exception as e: return jsonify({"error": f"Info retrieval failed: {str(e)}"}) -@app.route('/health') + +@app.route("/health") def health_check(): """Check health of all agents""" health_status = {} - + for agent_name, agent_url in AGENTS.items(): try: response = requests.get(f"{agent_url}/health", timeout=5) if response.status_code == 200: # Health endpoint returns JSON directly health_data = response.json() - health_status[agent_name] = {"status": "healthy", "url": agent_url, "agent_info": health_data} + health_status[agent_name] = { + "status": "healthy", + "url": agent_url, + "agent_info": health_data, + } else: health_status[agent_name] = {"status": "unhealthy", "url": agent_url} - except: + except: # noqa: E722 health_status[agent_name] = {"status": "offline", "url": agent_url} - + return jsonify(health_status) -if __name__ == '__main__': + +if __name__ == "__main__": print("Starting Flask frontend...") print("Available endpoints:") print("- Main interface: http://127.0.0.1:5000") print("- Health check: http://127.0.0.1:5000/health") - app.run(host='127.0.0.1', port=5000, debug=os.environ.get('FLASK_DEBUG') == '1') \ No newline at end of file + app.run(host="127.0.0.1", port=5000, debug=os.environ.get("FLASK_DEBUG") == "1") diff --git a/frontend-integration/product_info_agent.py b/frontend-integration/product_info_agent.py index 6881cc1e..1ac53f6d 100644 --- a/frontend-integration/product_info_agent.py +++ b/frontend-integration/product_info_agent.py @@ -1,13 +1,14 @@ from uagents import Agent, Context, Model from uagents.setup import fund_agent_if_low import requests -import json -from typing import Dict, Any, Optional +from typing import Optional + # Define request/response models class ProductRequest(Model): barcode: str + class ProductInfoResponse(Model): barcode: str product_name: str @@ -28,26 +29,30 @@ class ProductInfoResponse(Model): salt_100g: str error: Optional[str] = None + class HealthResponse(Model): status: str agent: str + # Create the product info agent info_agent = Agent( name="product_info_agent", port=8002, seed="product_info_secret_seed", - endpoint=["http://127.0.0.1:8002/submit"] + endpoint=["http://127.0.0.1:8002/submit"], ) # Fund agent if low on balance fund_agent_if_low(info_agent.wallet.address()) + @info_agent.on_event("startup") async def startup_event(ctx: Context): ctx.logger.info(f"Product Info Agent {info_agent.name} started!") ctx.logger.info(f"Agent address: {info_agent.address}") + @info_agent.on_rest_post("/product", ProductRequest, ProductInfoResponse) async def get_product_info(ctx: Context, req: ProductRequest) -> ProductInfoResponse: """ @@ -57,16 +62,16 @@ async def get_product_info(ctx: Context, req: ProductRequest) -> ProductInfoResp try: barcode = req.barcode ctx.logger.info(f"Getting product info for barcode: {barcode}") - + # Use direct API call to Open Food Facts url = f"https://world.openfoodfacts.org/api/v0/product/{barcode}.json" headers = { - 'User-Agent': 'uAgents-FoodInfo/1.0 (https://github.com/fetchai/uAgents)' + "User-Agent": "uAgents-FoodInfo/1.0 (https://github.com/fetchai/uAgents)" } - + ctx.logger.info(f"Making API call to: {url}") response = requests.get(url, headers=headers, timeout=10) - + if response.status_code != 200: ctx.logger.error(f"API call failed with status {response.status_code}") return ProductInfoResponse( @@ -87,13 +92,13 @@ async def get_product_info(ctx: Context, req: ProductRequest) -> ProductInfoResp fat_100g="N/A", sugars_100g="N/A", salt_100g="N/A", - error=f"API call failed: HTTP {response.status_code}" + error=f"API call failed: HTTP {response.status_code}", ) - + data = response.json() ctx.logger.info(f"API response status: {data.get('status')}") - - if data.get('status') != 1 or "product" not in data: + + if data.get("status") != 1 or "product" not in data: ctx.logger.warning("Product not found in API response") return ProductInfoResponse( barcode=barcode, @@ -113,13 +118,13 @@ async def get_product_info(ctx: Context, req: ProductRequest) -> ProductInfoResp fat_100g="N/A", sugars_100g="N/A", salt_100g="N/A", - error="Product not found" + error="Product not found", ) - + product = data["product"] product_name = product.get("product_name", "N/A") ctx.logger.info(f"Retrieved product: {product_name}") - + # Extract comprehensive product information return ProductInfoResponse( barcode=barcode, @@ -138,9 +143,9 @@ async def get_product_info(ctx: Context, req: ProductRequest) -> ProductInfoResp energy_100g=str(product.get("nutriments", {}).get("energy_100g", "N/A")), fat_100g=str(product.get("nutriments", {}).get("fat_100g", "N/A")), sugars_100g=str(product.get("nutriments", {}).get("sugars_100g", "N/A")), - salt_100g=str(product.get("nutriments", {}).get("salt_100g", "N/A")) + salt_100g=str(product.get("nutriments", {}).get("salt_100g", "N/A")), ) - + except requests.RequestException as e: ctx.logger.error(f"Network error: {str(e)}") return ProductInfoResponse( @@ -161,7 +166,7 @@ async def get_product_info(ctx: Context, req: ProductRequest) -> ProductInfoResp fat_100g="N/A", sugars_100g="N/A", salt_100g="N/A", - error=f"Network error: {str(e)}" + error=f"Network error: {str(e)}", ) except Exception as e: ctx.logger.error(f"Product info error: {str(e)}") @@ -183,13 +188,15 @@ async def get_product_info(ctx: Context, req: ProductRequest) -> ProductInfoResp fat_100g="N/A", sugars_100g="N/A", salt_100g="N/A", - error=f"Failed to get product info: {str(e)}" + error=f"Failed to get product info: {str(e)}", ) + @info_agent.on_rest_get("/health", HealthResponse) async def health_check(ctx: Context) -> HealthResponse: """Health check endpoint""" return HealthResponse(status="healthy", agent="product_info_agent") + if __name__ == "__main__": - info_agent.run() \ No newline at end of file + info_agent.run() diff --git a/frontend-integration/product_search_agent.py b/frontend-integration/product_search_agent.py index ba2ce2da..a09dd374 100644 --- a/frontend-integration/product_search_agent.py +++ b/frontend-integration/product_search_agent.py @@ -1,13 +1,14 @@ from uagents import Agent, Context, Model from uagents.setup import fund_agent_if_low import openfoodfacts -import json -from typing import Dict, Any, List, Optional +from typing import List, Optional + # Define request/response models class SearchRequest(Model): query: str + class ProductInfo(Model): code: str product_name: str @@ -15,22 +16,25 @@ class ProductInfo(Model): categories: str image_url: str + class SearchResponse(Model): query: str count: int products: List[ProductInfo] error: Optional[str] = None + class HealthResponse(Model): status: str agent: str + # Create the product search agent search_agent = Agent( name="product_search_agent", port=8001, seed="product_search_secret_seed", - endpoint=["http://127.0.0.1:8001/submit"] + endpoint=["http://127.0.0.1:8001/submit"], ) # Fund agent if low on balance @@ -39,11 +43,13 @@ class HealthResponse(Model): # Initialize Open Food Facts API api = openfoodfacts.API(user_agent="uAgents-FoodSearch/1.0") + @search_agent.on_event("startup") async def startup_event(ctx: Context): ctx.logger.info(f"Product Search Agent {search_agent.name} started!") ctx.logger.info(f"Agent address: {search_agent.address}") + @search_agent.on_rest_post("/search", SearchRequest, SearchResponse) async def search_products(ctx: Context, req: SearchRequest) -> SearchResponse: """ @@ -53,10 +59,10 @@ async def search_products(ctx: Context, req: SearchRequest) -> SearchResponse: try: query = req.query ctx.logger.info(f"Searching for products with query: {query}") - + # Search products using Open Food Facts API results = api.product.text_search(query, page_size=10) - + # Extract relevant information products = [] for product in results.get("products", [])[:10]: # Limit to 10 results @@ -65,30 +71,30 @@ async def search_products(ctx: Context, req: SearchRequest) -> SearchResponse: product_name=product.get("product_name", "N/A"), brands=product.get("brands", "N/A"), categories=product.get("categories", "N/A"), - image_url=product.get("image_url", "") + image_url=product.get("image_url", ""), ) products.append(product_info) - + ctx.logger.info(f"Found {len(products)} products") return SearchResponse( - query=query, - count=results.get("count", 0), - products=products + query=query, count=results.get("count", 0), products=products ) - + except Exception as e: ctx.logger.error(f"Search error: {str(e)}") return SearchResponse( query=req.query, count=0, products=[], - error=f"Failed to search products: {str(e)}" + error=f"Failed to search products: {str(e)}", ) + @search_agent.on_rest_get("/health", HealthResponse) async def health_check(ctx: Context) -> HealthResponse: """Health check endpoint""" return HealthResponse(status="healthy", agent="product_search_agent") + if __name__ == "__main__": - search_agent.run() \ No newline at end of file + search_agent.run() diff --git a/gemini-quickstart/01-basic-gemini-agent/basic_gemini_agent.py b/gemini-quickstart/01-basic-gemini-agent/basic_gemini_agent.py index 10b063c9..25ed691e 100644 --- a/gemini-quickstart/01-basic-gemini-agent/basic_gemini_agent.py +++ b/gemini-quickstart/01-basic-gemini-agent/basic_gemini_agent.py @@ -19,14 +19,14 @@ ChatMessage, ChatAcknowledgement, TextContent, - chat_protocol_spec + chat_protocol_spec, ) # Load environment variables load_dotenv() # Configure Gemini -gemini_api_key = os.getenv('GEMINI_API_KEY') +gemini_api_key = os.getenv("GEMINI_API_KEY") if not gemini_api_key: raise ValueError("GEMINI_API_KEY not found in environment variables") @@ -35,12 +35,12 @@ client = genai.Client(api_key=gemini_api_key) # Model configuration -MODEL_NAME = 'gemini-2.5-flash' +MODEL_NAME = "gemini-2.5-flash" GENERATION_CONFIG = { - 'temperature': 0.7, # Balance creativity and consistency - 'top_p': 0.95, - 'top_k': 40, - 'max_output_tokens': 1024, + "temperature": 0.7, # Balance creativity and consistency + "top_p": 0.95, + "top_k": 40, + "max_output_tokens": 1024, } # Create agent @@ -48,7 +48,7 @@ name="gemini_assistant", seed="", # Change this for your agent to a unique seed phrase port=8000, - mailbox=True # Required for Agentverse deployment + mailbox=True, # Required for Agentverse deployment ) # Initialize chat protocol @@ -77,9 +77,7 @@ # Helper function to create text chat messages def create_text_chat(text: str) -> ChatMessage: """Create a ChatMessage with TextContent""" - return ChatMessage( - content=[TextContent(text=text, type="text")] - ) + return ChatMessage(content=[TextContent(text=text, type="text")]) @agent.on_event("startup") @@ -87,12 +85,12 @@ async def startup(ctx: Context): """Initialize agent on startup""" ctx.logger.info("🤖 Starting Gemini Assistant...") ctx.logger.info(f"📍 Agent address: {agent.address}") - + if gemini_api_key: ctx.logger.info("✅ Gemini API configured") else: ctx.logger.error("❌ Gemini API key not set") - + # Initialize conversation storage ctx.storage.set("total_messages", 0) ctx.storage.set("conversations", {}) @@ -101,7 +99,7 @@ async def startup(ctx: Context): @chat_proto.on_message(ChatMessage) async def handle_chat_message(ctx: Context, sender: str, msg: ChatMessage): """Handle incoming chat messages""" - + try: # Extract text from message content user_text = "" @@ -109,64 +107,66 @@ async def handle_chat_message(ctx: Context, sender: str, msg: ChatMessage): if isinstance(item, TextContent): user_text = item.text break - + if not user_text: ctx.logger.warning("No text content in message") return - + # Log incoming message ctx.logger.info(f"📨 Message from {sender}: {user_text[:50]}...") - + # Send acknowledgement - await ctx.send(sender, ChatAcknowledgement( - timestamp=datetime.now(timezone.utc), - acknowledged_msg_id=msg.msg_id - )) - + await ctx.send( + sender, + ChatAcknowledgement( + timestamp=datetime.now(timezone.utc), acknowledged_msg_id=msg.msg_id + ), + ) + # Get conversation history for context conversations = ctx.storage.get("conversations") or {} history = conversations.get(sender, []) - + # Build chat contents with history context # For simplicity, we'll include recent history in the prompt conversation_context = "" if history: for h in history[-5:]: - role = "User" if h['role'] == 'user' else "Assistant" + role = "User" if h["role"] == "user" else "Assistant" conversation_context += f"{role}: {h['text']}\n" - + # Combine context with current message - full_prompt = f"{SYSTEM_PROMPT}\n\n{conversation_context}User: {user_text}\nAssistant:" - + full_prompt = ( + f"{SYSTEM_PROMPT}\n\n{conversation_context}User: {user_text}\nAssistant:" + ) + # Generate response from Gemini ctx.logger.info("🤔 Generating response with Gemini...") response = client.models.generate_content( - model=MODEL_NAME, - contents=full_prompt, - config=GENERATION_CONFIG + model=MODEL_NAME, contents=full_prompt, config=GENERATION_CONFIG ) response_text = response.text - + ctx.logger.info(f"✅ Response generated: {response_text[:50]}...") - + # Update conversation history - history.append({'role': 'user', 'text': user_text}) - history.append({'role': 'model', 'text': response_text}) + history.append({"role": "user", "text": user_text}) + history.append({"role": "model", "text": response_text}) conversations[sender] = history[-10:] # Keep last 10 messages ctx.storage.set("conversations", conversations) - + # Track stats total = ctx.storage.get("total_messages") or 0 ctx.storage.set("total_messages", total + 1) - + # Send response back to user await ctx.send(sender, create_text_chat(response_text)) - + ctx.logger.info(f"💬 Response sent to {sender}") - + except Exception as e: ctx.logger.error(f"❌ Error processing message: {e}") - + # Send error message to user error_msg = "I'm sorry, I encountered an error processing your message. Please try again." await ctx.send(sender, create_text_chat(error_msg)) @@ -185,21 +185,23 @@ async def handle_acknowledgement(ctx: Context, sender: str, msg: ChatAcknowledge if __name__ == "__main__": print("🤖 Starting Gemini Assistant...") print(f"📍 Agent address: {agent.address}") - + if gemini_api_key: print("✅ Gemini API configured") else: print("❌ ERROR: GEMINI_API_KEY not set") print(" Please add it to your .env file") exit(1) - + print("\n🎯 Agent Features:") print(" • Conversational AI with Gemini 2.5 Flash") print(" • Context-aware responses") print(" • Conversation history tracking") print(" • Ready for Agentverse deployment") - - print("\n✅ Agent is running! Connect via ASI One or send messages programmatically.") + + print( + "\n✅ Agent is running! Connect via ASI One or send messages programmatically." + ) print(" Press Ctrl+C to stop.\n") - + agent.run() diff --git a/gemini-quickstart/02-imagen-agent/imagen_agent.py b/gemini-quickstart/02-imagen-agent/imagen_agent.py index 53470743..024d391e 100644 --- a/gemini-quickstart/02-imagen-agent/imagen_agent.py +++ b/gemini-quickstart/02-imagen-agent/imagen_agent.py @@ -23,7 +23,7 @@ TextContent, ResourceContent, Resource, - chat_protocol_spec + chat_protocol_spec, ) from uagents_core.storage import ExternalStorage @@ -31,30 +31,34 @@ load_dotenv() # Configure Gemini/Imagen -gemini_api_key = os.getenv('GEMINI_API_KEY') +gemini_api_key = os.getenv("GEMINI_API_KEY") if not gemini_api_key: raise ValueError("GEMINI_API_KEY not found in environment variables") # Configure Agentverse Storage -agentverse_api_key = os.getenv('AGENTVERSE_API_KEY') +agentverse_api_key = os.getenv("AGENTVERSE_API_KEY") if not agentverse_api_key: - raise ValueError("AGENTVERSE_API_KEY not found in environment variables. Get it from https://agentverse.ai") + raise ValueError( + "AGENTVERSE_API_KEY not found in environment variables. Get it from https://agentverse.ai" + ) storage_url = os.getenv("AGENTVERSE_URL", "https://agentverse.ai") + "/v1/storage" -external_storage = ExternalStorage(api_token=agentverse_api_key, storage_url=storage_url) +external_storage = ExternalStorage( + api_token=agentverse_api_key, storage_url=storage_url +) # Initialize Gemini client client = genai.Client(api_key=gemini_api_key) # Model configuration -IMAGE_MODEL = 'gemini-2.5-flash-image' +IMAGE_MODEL = "gemini-2.5-flash-image" # Create agent agent = Agent( name="imagen_generator", seed="", # Change this for your agent to a unique seed phrase port=8001, - mailbox=True # Required for Agentverse deployment + mailbox=True, # Required for Agentverse deployment ) # Initialize chat protocol @@ -87,9 +91,10 @@ def create_text_chat(text: str) -> ChatMessage: return ChatMessage( timestamp=datetime.now(timezone.utc), msg_id=uuid4(), - content=[TextContent(text=text, type="text")] + content=[TextContent(text=text, type="text")], ) + def create_resource_chat(asset_id: str, uri: str, caption: str = None) -> ChatMessage: """Create a ChatMessage with ResourceContent (for images)""" content = [ @@ -97,23 +102,17 @@ def create_resource_chat(asset_id: str, uri: str, caption: str = None) -> ChatMe type="resource", resource_id=asset_id, resource=Resource( - uri=uri, - metadata={ - "mime_type": "image/png", - "role": "generated-image" - } - ) + uri=uri, metadata={"mime_type": "image/png", "role": "generated-image"} + ), ) ] - + # Add optional caption as text if caption: content.append(TextContent(text=caption, type="text")) - + return ChatMessage( - timestamp=datetime.now(timezone.utc), - msg_id=uuid4(), - content=content + timestamp=datetime.now(timezone.utc), msg_id=uuid4(), content=content ) @@ -122,17 +121,19 @@ async def startup(ctx: Context): """Initialize agent on startup""" ctx.logger.info("🎨 Starting Image Generator...") ctx.logger.info(f"📍 Agent address: {agent.address}") - + if gemini_api_key: ctx.logger.info("✅ Gemini API configured for image generation") else: ctx.logger.error("❌ Gemini API key not set") - + if agentverse_api_key: ctx.logger.info("✅ Agentverse storage configured") else: - ctx.logger.warning("⚠️ Agentverse API key not set - images won't display in ASI One") - + ctx.logger.warning( + "⚠️ Agentverse API key not set - images won't display in ASI One" + ) + # Initialize image storage ctx.storage.set("total_images", 0) ctx.storage.set("images", {}) @@ -141,7 +142,7 @@ async def startup(ctx: Context): @chat_proto.on_message(ChatMessage) async def handle_chat_message(ctx: Context, sender: str, msg: ChatMessage): """Handle incoming chat messages and generate images""" - + try: # Extract text from message content user_prompt = "" @@ -149,23 +150,27 @@ async def handle_chat_message(ctx: Context, sender: str, msg: ChatMessage): if isinstance(item, TextContent): user_prompt = item.text break - + if not user_prompt: ctx.logger.warning("No text content in message") return - + # Log incoming message ctx.logger.info(f"📨 Prompt from {sender}: {user_prompt[:50]}...") - + # Send acknowledgement - await ctx.send(sender, ChatAcknowledgement( - timestamp=datetime.now(timezone.utc), - acknowledged_msg_id=msg.msg_id - )) - + await ctx.send( + sender, + ChatAcknowledgement( + timestamp=datetime.now(timezone.utc), acknowledged_msg_id=msg.msg_id + ), + ) + # Check for help/info requests lower_prompt = user_prompt.lower() - if any(word in lower_prompt for word in ['help', 'how', 'what can you', 'guide']): + if any( + word in lower_prompt for word in ["help", "how", "what can you", "guide"] + ): help_msg = f"""{SYSTEM_PROMPT} **Example prompts:** @@ -176,70 +181,74 @@ async def handle_chat_message(ctx: Context, sender: str, msg: ChatMessage): • "A sketch of a coffee cup on a wooden table" Just describe what you want to see!""" - + await ctx.send(sender, create_text_chat(help_msg)) ctx.logger.info(f"💬 Help sent to {sender}") return - + # Send "generating" message - await ctx.send(sender, create_text_chat("🎨 Generating your image... This may take a moment.")) - + await ctx.send( + sender, + create_text_chat("🎨 Generating your image... This may take a moment."), + ) + # Generate image with Gemini ctx.logger.info(f"🎨 Generating image with Gemini: {user_prompt[:100]}...") - + response = client.models.generate_content( - model=IMAGE_MODEL, - contents=[user_prompt] + model=IMAGE_MODEL, contents=[user_prompt] ) - + # Extract image from response image_parts = [ part.inline_data.data for part in response.candidates[0].content.parts if part.inline_data ] - + if not image_parts: - error_msg = "❌ Sorry, I couldn't generate an image. Please try a different prompt." + error_msg = ( + "❌ Sorry, I couldn't generate an image. Please try a different prompt." + ) await ctx.send(sender, create_text_chat(error_msg)) return - + # Get image data (already in bytes) img_data = image_parts[0] - + # Upload to Agentverse External Storage ctx.logger.info("📤 Uploading image to Agentverse storage...") - + try: asset_id = external_storage.create_asset( name=f"generated_image_{int(datetime.now().timestamp())}", content=img_data, - mime_type="image/png" + mime_type="image/png", ) ctx.logger.info(f"✅ Asset created with ID: {asset_id}") - + # Set permissions so sender can view it external_storage.set_permissions(asset_id=asset_id, agent_address=sender) ctx.logger.info(f"🔓 Asset permissions set for: {sender}") - + # Create asset URI asset_uri = f"agent-storage://{storage_url}/{asset_id}" - + # Store in local storage for tracking total_images = ctx.storage.get("total_images") or 0 ctx.storage.set("total_images", total_images + 1) - + # Send image as ResourceContent caption = f"✨ Generated: {user_prompt[:100]}..." await ctx.send(sender, create_resource_chat(asset_id, asset_uri, caption)) - + ctx.logger.info(f"📸 Image sent to {sender}") - + except Exception as storage_err: ctx.logger.error(f"❌ Storage error: {storage_err}") - + # Fallback: send base64 as text - img_base64 = base64.b64encode(img_data).decode('utf-8') + img_base64 = base64.b64encode(img_data).decode("utf-8") fallback_msg = f"""✨ **Image Generated!** **Prompt:** {user_prompt} @@ -251,16 +260,21 @@ async def handle_chat_message(ctx: Context, sender: str, msg: ChatMessage): Please contact support or enable Agentverse storage.""" await ctx.send(sender, create_text_chat(fallback_msg)) ctx.logger.info(f"💬 Response sent to {sender}") - + except Exception as e: ctx.logger.error(f"❌ Error processing message: {e}") import traceback + ctx.logger.error(traceback.format_exc()) - + # Check for specific error types error_str = str(e) - - if "RESOURCE_EXHAUSTED" in error_str or "429" in error_str or "quota" in error_str.lower(): + + if ( + "RESOURCE_EXHAUSTED" in error_str + or "429" in error_str + or "quota" in error_str.lower() + ): error_msg = """⚠️ **API Quota Limit Reached** I've hit the free tier quota limits for image generation. This happens when: @@ -277,7 +291,7 @@ async def handle_chat_message(ctx: Context, sender: str, msg: ChatMessage): - Limited requests per minute Sorry for the inconvenience! Please try again later. 🙏""" - + elif "INVALID_ARGUMENT" in error_str or "billing" in error_str.lower(): error_msg = """⚠️ **Billing Required** @@ -289,7 +303,7 @@ async def handle_chat_message(ctx: Context, sender: str, msg: ChatMessage): - Use DALL-E via OpenAI (if available) - Wait for quota to reset - Upgrade to paid tier""" - + else: error_msg = f"""❌ **Image Generation Error** @@ -300,7 +314,7 @@ async def handle_chat_message(ctx: Context, sender: str, msg: ChatMessage): - Checking if your prompt follows content guidelines - Waiting a moment and trying again - Using a different style or subject""" - + await ctx.send(sender, create_text_chat(error_msg)) @@ -317,21 +331,21 @@ async def handle_acknowledgement(ctx: Context, sender: str, msg: ChatAcknowledge if __name__ == "__main__": print("🎨 Starting Gemini Image Generator Agent...") print(f"📍 Agent address: {agent.address}") - + if gemini_api_key: print("✅ Gemini API configured for image generation") else: print("❌ ERROR: GEMINI_API_KEY not set") print(" Please add it to your .env file") exit(1) - + if agentverse_api_key: print("✅ Agentverse storage configured") else: print("⚠️ WARNING: AGENTVERSE_API_KEY not set") print(" Images won't display in ASI One without it") print(" Get your key from: https://agentverse.ai") - + print("\n🎯 Agent Features:") print(" • Image generation with Gemini 2.5 Flash") print(" • High-quality, realistic images") @@ -339,8 +353,8 @@ async def handle_acknowledgement(ctx: Context, sender: str, msg: ChatAcknowledge print(" • Natural language prompts") print(" • No billing required (uses Gemini API)") print(" • Ready for Agentverse deployment") - + print("\n✅ Agent is running! Connect via ASI One to generate images.") print(" Press Ctrl+C to stop.\n") - + agent.run() diff --git a/gemini-quickstart/03-veo-video-agent/veo_agent.py b/gemini-quickstart/03-veo-video-agent/veo_agent.py index 567f98ce..6d7b9277 100644 --- a/gemini-quickstart/03-veo-video-agent/veo_agent.py +++ b/gemini-quickstart/03-veo-video-agent/veo_agent.py @@ -10,7 +10,6 @@ """ import os -import time import asyncio from datetime import datetime, timezone from uuid import uuid4 @@ -25,7 +24,7 @@ TextContent, ResourceContent, Resource, - chat_protocol_spec + chat_protocol_spec, ) from uagents_core.storage import ExternalStorage @@ -33,23 +32,27 @@ load_dotenv() # Configure Gemini/Veo -gemini_api_key = os.getenv('GEMINI_API_KEY') +gemini_api_key = os.getenv("GEMINI_API_KEY") if not gemini_api_key: raise ValueError("GEMINI_API_KEY not found in environment variables") # Configure Agentverse Storage -agentverse_api_key = os.getenv('AGENTVERSE_API_KEY') +agentverse_api_key = os.getenv("AGENTVERSE_API_KEY") if not agentverse_api_key: - raise ValueError("AGENTVERSE_API_KEY not found in environment variables. Get it from https://agentverse.ai") + raise ValueError( + "AGENTVERSE_API_KEY not found in environment variables. Get it from https://agentverse.ai" + ) storage_url = os.getenv("AGENTVERSE_URL", "https://agentverse.ai") + "/v1/storage" -external_storage = ExternalStorage(api_token=agentverse_api_key, storage_url=storage_url) +external_storage = ExternalStorage( + api_token=agentverse_api_key, storage_url=storage_url +) # Initialize Gemini client client = genai.Client(api_key=gemini_api_key) # Model configuration -VEO_MODEL = 'veo-3.1-generate-preview' +VEO_MODEL = "veo-3.1-generate-preview" DEFAULT_VIDEO_CONFIG = types.GenerateVideosConfig( number_of_videos=1, resolution="720p", # 720p or 1080p @@ -60,7 +63,7 @@ name="veo_generator", seed="", # Change this for your agent to a unique seed phrase port=8002, - mailbox=True # Required for Agentverse deployment + mailbox=True, # Required for Agentverse deployment ) # Initialize chat protocol @@ -99,9 +102,10 @@ def create_text_chat(text: str) -> ChatMessage: return ChatMessage( timestamp=datetime.now(timezone.utc), msg_id=uuid4(), - content=[TextContent(text=text, type="text")] + content=[TextContent(text=text, type="text")], ) + def create_resource_chat(asset_id: str, uri: str, caption: str = None) -> ChatMessage: """Create a ChatMessage with ResourceContent (for videos)""" content = [ @@ -109,23 +113,17 @@ def create_resource_chat(asset_id: str, uri: str, caption: str = None) -> ChatMe type="resource", resource_id=asset_id, resource=Resource( - uri=uri, - metadata={ - "mime_type": "video/mp4", - "role": "generated-video" - } - ) + uri=uri, metadata={"mime_type": "video/mp4", "role": "generated-video"} + ), ) ] - + # Add optional caption as text if caption: content.append(TextContent(text=caption, type="text")) - + return ChatMessage( - timestamp=datetime.now(timezone.utc), - msg_id=uuid4(), - content=content + timestamp=datetime.now(timezone.utc), msg_id=uuid4(), content=content ) @@ -134,17 +132,19 @@ async def startup(ctx: Context): """Initialize agent on startup""" ctx.logger.info("🎬 Starting Veo Video Generator...") ctx.logger.info(f"📍 Agent address: {agent.address}") - + if gemini_api_key: ctx.logger.info("✅ Veo API configured for video generation") else: ctx.logger.error("❌ Gemini API key not set") - + if agentverse_api_key: ctx.logger.info("✅ Agentverse storage configured") else: - ctx.logger.warning("⚠️ Agentverse API key not set - videos won't display in ASI One") - + ctx.logger.warning( + "⚠️ Agentverse API key not set - videos won't display in ASI One" + ) + # Initialize video storage ctx.storage.set("total_videos", 0) ctx.storage.set("videos", {}) @@ -153,7 +153,7 @@ async def startup(ctx: Context): @chat_proto.on_message(ChatMessage) async def handle_chat_message(ctx: Context, sender: str, msg: ChatMessage): """Handle incoming chat messages and generate videos""" - + try: # Extract text from message content user_prompt = "" @@ -161,23 +161,27 @@ async def handle_chat_message(ctx: Context, sender: str, msg: ChatMessage): if isinstance(item, TextContent): user_prompt = item.text break - + if not user_prompt: ctx.logger.warning("No text content in message") return - + # Log incoming message ctx.logger.info(f"📨 Prompt from {sender}: {user_prompt[:50]}...") - + # Send acknowledgement - await ctx.send(sender, ChatAcknowledgement( - timestamp=datetime.now(timezone.utc), - acknowledged_msg_id=msg.msg_id - )) - + await ctx.send( + sender, + ChatAcknowledgement( + timestamp=datetime.now(timezone.utc), acknowledged_msg_id=msg.msg_id + ), + ) + # Check for help/info requests lower_prompt = user_prompt.lower() - if any(word in lower_prompt for word in ['help', 'how', 'what can you', 'guide']): + if any( + word in lower_prompt for word in ["help", "how", "what can you", "guide"] + ): help_msg = f"""{SYSTEM_PROMPT} **Example prompts:** @@ -188,62 +192,70 @@ async def handle_chat_message(ctx: Context, sender: str, msg: ChatMessage): • "A robot dancing on a city street at night" Just describe the video you want to see!""" - + await ctx.send(sender, create_text_chat(help_msg)) ctx.logger.info(f"💬 Help sent to {sender}") return - + # Send "generating" message - await ctx.send(sender, create_text_chat( - "🎬 Generating your video... This takes 30-60 seconds. Please wait! ⏳" - )) - + await ctx.send( + sender, + create_text_chat( + "🎬 Generating your video... This takes 30-60 seconds. Please wait! ⏳" + ), + ) + # Generate video with Veo - ctx.logger.info(f"🎬 Starting video generation with Veo: {user_prompt[:100]}...") - + ctx.logger.info( + f"🎬 Starting video generation with Veo: {user_prompt[:100]}..." + ) + operation = client.models.generate_videos( - model=VEO_MODEL, - prompt=user_prompt, - config=DEFAULT_VIDEO_CONFIG + model=VEO_MODEL, prompt=user_prompt, config=DEFAULT_VIDEO_CONFIG ) - + ctx.logger.info(f"⏳ Operation started: {operation.name}") - + # Poll operation status poll_count = 0 max_polls = 60 # Max 10 minutes (60 * 10 seconds) - + while not operation.done and poll_count < max_polls: ctx.logger.info(f"⏳ Waiting for video generation... ({poll_count * 10}s)") - + # Send periodic updates to keep user engaged if poll_count % 3 == 0 and poll_count > 0: - await ctx.send(sender, create_text_chat( - f"⏳ Still generating... {poll_count * 10}s elapsed. Almost there!" - )) - + await ctx.send( + sender, + create_text_chat( + f"⏳ Still generating... {poll_count * 10}s elapsed. Almost there!" + ), + ) + await asyncio.sleep(10) # Wait 10 seconds operation = client.operations.get(operation) poll_count += 1 - + if not operation.done: error_msg = "❌ Video generation timed out. Please try a simpler prompt or try again later." await ctx.send(sender, create_text_chat(error_msg)) return - + # Check if generation was successful if not operation.response or not operation.response.generated_videos: - error_msg = "❌ Sorry, I couldn't generate a video. Please try a different prompt." + error_msg = ( + "❌ Sorry, I couldn't generate a video. Please try a different prompt." + ) await ctx.send(sender, create_text_chat(error_msg)) return - + generated_video = operation.response.generated_videos[0] - ctx.logger.info(f"✅ Video generated successfully!") - + ctx.logger.info("✅ Video generated successfully!") + # Get video and download bytes video = generated_video.video ctx.logger.info("📥 Downloading video...") - + # Try Files.download() with just the file parameter try: video_bytes = client.files.download(file=video) @@ -251,43 +263,53 @@ async def handle_chat_message(ctx: Context, sender: str, msg: ChatMessage): except Exception as download_err: ctx.logger.error(f"Download error: {download_err}") # Fallback: try getting URI and fetching manually - video_uri = video.uri if hasattr(video, 'uri') else f"https://generativelanguage.googleapis.com/v1beta/{video.name}" + video_uri = ( + video.uri + if hasattr(video, "uri") + else f"https://generativelanguage.googleapis.com/v1beta/{video.name}" + ) ctx.logger.info(f"Trying manual fetch from: {video_uri}") - + import requests + headers = {"Authorization": f"Bearer {gemini_api_key}"} response = requests.get(video_uri, headers=headers) video_bytes = response.content ctx.logger.info(f"✅ Fetched {len(video_bytes)} bytes manually") - + # Upload to Agentverse storage ctx.logger.info("📤 Uploading to Agentverse...") asset_id = external_storage.create_asset( name=f"video_{int(datetime.now().timestamp())}", content=video_bytes, - mime_type="video/mp4" + mime_type="video/mp4", ) - + external_storage.set_permissions(asset_id=asset_id, agent_address=sender) asset_uri = f"agent-storage://{storage_url}/{asset_id}" - + # Track and send total_videos = ctx.storage.get("total_videos") or 0 ctx.storage.set("total_videos", total_videos + 1) - + caption = f"🎬 {user_prompt[:100]}... (8s, 720p)" await ctx.send(sender, create_resource_chat(asset_id, asset_uri, caption)) - ctx.logger.info(f"🎬 Video sent!") - + ctx.logger.info("🎬 Video sent!") + except Exception as e: ctx.logger.error(f"❌ Error processing message: {e}") import traceback + ctx.logger.error(traceback.format_exc()) - + # Check for specific error types error_str = str(e) - - if "RESOURCE_EXHAUSTED" in error_str or "429" in error_str or "quota" in error_str.lower(): + + if ( + "RESOURCE_EXHAUSTED" in error_str + or "429" in error_str + or "quota" in error_str.lower() + ): error_msg = """⚠️ **API Quota Limit Reached** I've hit the quota limits for video generation. This happens when: @@ -302,7 +324,7 @@ async def handle_chat_message(ctx: Context, sender: str, msg: ChatMessage): Video generation uses more quota than images, so limits are reached faster. Sorry for the inconvenience! Please try again later. 🙏""" - + else: error_msg = f"""❌ **Video Generation Error** @@ -313,7 +335,7 @@ async def handle_chat_message(ctx: Context, sender: str, msg: ChatMessage): - More specific camera angles and actions - Checking if your prompt follows content guidelines - Waiting a moment and trying again""" - + await ctx.send(sender, create_text_chat(error_msg)) @@ -330,21 +352,21 @@ async def handle_acknowledgement(ctx: Context, sender: str, msg: ChatAcknowledge if __name__ == "__main__": print("🎬 Starting Veo 3.1 Video Generator Agent...") print(f"📍 Agent address: {agent.address}") - + if gemini_api_key: print("✅ Veo API configured for video generation") else: print("❌ ERROR: GEMINI_API_KEY not set") print(" Please add it to your .env file") exit(1) - + if agentverse_api_key: print("✅ Agentverse storage configured") else: print("⚠️ WARNING: AGENTVERSE_API_KEY not set") print(" Videos won't display in ASI One without it") print(" Get your key from: https://agentverse.ai") - + print("\n🎯 Agent Features:") print(" • Video generation with Veo 3.1") print(" • 8-second HD videos (720p/1080p)") @@ -352,9 +374,9 @@ async def handle_acknowledgement(ctx: Context, sender: str, msg: ChatAcknowledge print(" • Cinematic realism") print(" • Natural language prompts") print(" • Ready for Agentverse deployment") - + print("\n⏳ Note: Video generation takes 30-60 seconds per request") print("\n✅ Agent is running! Connect via ASI One to generate videos.") print(" Press Ctrl+C to stop.\n") - + agent.run() diff --git a/gemini-quickstart/04-lyria-music-agent/lyria_agent.py b/gemini-quickstart/04-lyria-music-agent/lyria_agent.py index dc46bf65..c38bb313 100644 --- a/gemini-quickstart/04-lyria-music-agent/lyria_agent.py +++ b/gemini-quickstart/04-lyria-music-agent/lyria_agent.py @@ -12,7 +12,6 @@ import os import asyncio import wave -import struct from datetime import datetime, timezone from uuid import uuid4 from dotenv import load_dotenv @@ -26,7 +25,7 @@ TextContent, ResourceContent, Resource, - chat_protocol_spec + chat_protocol_spec, ) from uagents_core.storage import ExternalStorage @@ -34,23 +33,25 @@ load_dotenv() # Configure Gemini/Lyria -gemini_api_key = os.getenv('GEMINI_API_KEY') +gemini_api_key = os.getenv("GEMINI_API_KEY") if not gemini_api_key: raise ValueError("GEMINI_API_KEY not found in environment variables") # Configure Agentverse Storage -agentverse_api_key = os.getenv('AGENTVERSE_API_KEY') +agentverse_api_key = os.getenv("AGENTVERSE_API_KEY") if not agentverse_api_key: raise ValueError("AGENTVERSE_API_KEY not found in environment variables") storage_url = os.getenv("AGENTVERSE_URL", "https://agentverse.ai") + "/v1/storage" -external_storage = ExternalStorage(api_token=agentverse_api_key, storage_url=storage_url) +external_storage = ExternalStorage( + api_token=agentverse_api_key, storage_url=storage_url +) # Initialize Gemini client with alpha API version for Lyria -client = genai.Client(http_options={'api_version': 'v1alpha'}, api_key=gemini_api_key) +client = genai.Client(http_options={"api_version": "v1alpha"}, api_key=gemini_api_key) # Model configuration -LYRIA_MODEL = 'models/lyria-realtime-exp' +LYRIA_MODEL = "models/lyria-realtime-exp" SAMPLE_RATE = 48000 # 48kHz CHANNELS = 2 # Stereo DURATION_SECONDS = 30 # Generate 30 seconds of music @@ -60,7 +61,7 @@ name="lyria_generator", seed="", # Change this for your agent to a unique seed phrase port=8003, - mailbox=True + mailbox=True, ) # Initialize chat protocol @@ -99,9 +100,10 @@ def create_text_chat(text: str) -> ChatMessage: return ChatMessage( timestamp=datetime.now(timezone.utc), msg_id=uuid4(), - content=[TextContent(text=text, type="text")] + content=[TextContent(text=text, type="text")], ) + def create_resource_chat(asset_id: str, uri: str, caption: str = None) -> ChatMessage: """Create a ChatMessage with ResourceContent (for audio)""" content = [ @@ -109,27 +111,22 @@ def create_resource_chat(asset_id: str, uri: str, caption: str = None) -> ChatMe type="resource", resource_id=asset_id, resource=Resource( - uri=uri, - metadata={ - "mime_type": "audio/wav", - "role": "generated-music" - } - ) + uri=uri, metadata={"mime_type": "audio/wav", "role": "generated-music"} + ), ) ] - + if caption: content.append(TextContent(text=caption, type="text")) - + return ChatMessage( - timestamp=datetime.now(timezone.utc), - msg_id=uuid4(), - content=content + timestamp=datetime.now(timezone.utc), msg_id=uuid4(), content=content ) + def save_pcm_as_wav(pcm_data: bytes, filename: str): """Convert raw PCM data to WAV file""" - with wave.open(filename, 'wb') as wav_file: + with wave.open(filename, "wb") as wav_file: wav_file.setnchannels(CHANNELS) wav_file.setsampwidth(2) # 16-bit wav_file.setframerate(SAMPLE_RATE) @@ -141,24 +138,24 @@ async def startup(ctx: Context): """Initialize agent on startup""" ctx.logger.info("🎵 Starting Lyria Music Generator...") ctx.logger.info(f"📍 Agent address: {agent.address}") - + if gemini_api_key: ctx.logger.info("✅ Lyria API configured for music generation") else: ctx.logger.error("❌ Gemini API key not set") - + if agentverse_api_key: ctx.logger.info("✅ Agentverse storage configured") else: ctx.logger.warning("⚠️ Agentverse API key not set") - + ctx.storage.set("total_tracks", 0) @chat_proto.on_message(ChatMessage) async def handle_chat_message(ctx: Context, sender: str, msg: ChatMessage): """Handle incoming chat messages and generate music""" - + try: # Extract text from message user_prompt = "" @@ -166,21 +163,23 @@ async def handle_chat_message(ctx: Context, sender: str, msg: ChatMessage): if isinstance(item, TextContent): user_prompt = item.text break - + if not user_prompt: ctx.logger.warning("No text content in message") return - + ctx.logger.info(f"📨 Prompt from {sender}: {user_prompt[:50]}...") - + # Send acknowledgement - await ctx.send(sender, ChatAcknowledgement( - timestamp=datetime.now(timezone.utc), - acknowledged_msg_id=msg.msg_id - )) - + await ctx.send( + sender, + ChatAcknowledgement( + timestamp=datetime.now(timezone.utc), acknowledged_msg_id=msg.msg_id + ), + ) + # Check for help - if any(word in user_prompt.lower() for word in ['help', 'how', 'what can you']): + if any(word in user_prompt.lower() for word in ["help", "how", "what can you"]): help_msg = f"""{SYSTEM_PROMPT} **Example prompts:** @@ -191,38 +190,44 @@ async def handle_chat_message(ctx: Context, sender: str, msg: ChatMessage): • "Ambient meditation music" Describe the music you want to hear!""" - + await ctx.send(sender, create_text_chat(help_msg)) return - + # Send generating message - await ctx.send(sender, create_text_chat( - "🎵 Generating your music... This takes ~30 seconds. Please wait! ⏳" - )) - + await ctx.send( + sender, + create_text_chat( + "🎵 Generating your music... This takes ~30 seconds. Please wait! ⏳" + ), + ) + ctx.logger.info(f"🎵 Starting music generation: {user_prompt[:100]}...") - + # Generate music using Lyria RealTime audio_chunks = [] is_collecting = True - + async def receive_audio(session): """Collect audio chunks""" try: async for message in session.receive(): if not is_collecting: break - if hasattr(message, 'server_content') and message.server_content.audio_chunks: + if ( + hasattr(message, "server_content") + and message.server_content.audio_chunks + ): for chunk in message.server_content.audio_chunks: audio_chunks.append(chunk.data) except asyncio.CancelledError: pass # Task cancelled, exit gracefully - + # Connect and generate async with client.aio.live.music.connect(model=LYRIA_MODEL) as session: # Start audio receiver task receiver_task = asyncio.create_task(receive_audio(session)) - + try: # Send prompts and config await session.set_weighted_prompts( @@ -232,26 +237,26 @@ async def receive_audio(session): config=types.LiveMusicGenerationConfig( bpm=120, temperature=1.0, - music_generation_mode=types.MusicGenerationMode.QUALITY + music_generation_mode=types.MusicGenerationMode.QUALITY, ) ) - + # Start streaming await session.play() ctx.logger.info("🎵 Streaming music...") - + # Collect for 30 seconds await asyncio.sleep(DURATION_SECONDS) - + # Stop streaming is_collecting = False await session.stop() - + # Wait a bit for final chunks await asyncio.sleep(1) - + ctx.logger.info(f"✅ Collected {len(audio_chunks)} audio chunks") - + finally: # Cancel receiver task receiver_task.cancel() @@ -259,62 +264,64 @@ async def receive_audio(session): await receiver_task except asyncio.CancelledError: pass - + if not audio_chunks: error_msg = "❌ No audio generated. Please try a different prompt." await ctx.send(sender, create_text_chat(error_msg)) return - + # Combine audio chunks - pcm_data = b''.join(audio_chunks) + pcm_data = b"".join(audio_chunks) ctx.logger.info(f"📦 Combined audio: {len(pcm_data)} bytes") - + # Convert to WAV import tempfile + with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as tmp: temp_path = tmp.name - + save_pcm_as_wav(pcm_data, temp_path) - + # Read WAV bytes - with open(temp_path, 'rb') as f: + with open(temp_path, "rb") as f: wav_data = f.read() - + os.remove(temp_path) ctx.logger.info(f"✅ WAV file created: {len(wav_data)} bytes") - + # Upload to Agentverse storage ctx.logger.info("📤 Uploading to Agentverse...") - + asset_id = external_storage.create_asset( name=f"music_{int(datetime.now().timestamp())}", content=wav_data, - mime_type="audio/wav" + mime_type="audio/wav", ) - + external_storage.set_permissions(asset_id=asset_id, agent_address=sender) asset_uri = f"agent-storage://{storage_url}/{asset_id}" - + # Track and send total_tracks = ctx.storage.get("total_tracks") or 0 ctx.storage.set("total_tracks", total_tracks + 1) - + caption = f"🎵 {user_prompt[:100]}... (30s)" await ctx.send(sender, create_resource_chat(asset_id, asset_uri, caption)) ctx.logger.info(f"🎵 Music sent to {sender}!") - + except Exception as e: ctx.logger.error(f"❌ Error: {e}") import traceback + ctx.logger.error(traceback.format_exc()) - + error_str = str(e) - + if "RESOURCE_EXHAUSTED" in error_str or "429" in error_str: error_msg = "⚠️ API quota reached. Please try again later." else: error_msg = f"❌ Music generation error: {str(e)[:200]}\n\nTry a different prompt or wait a moment." - + await ctx.send(sender, create_text_chat(error_msg)) @@ -331,27 +338,27 @@ async def handle_acknowledgement(ctx: Context, sender: str, msg: ChatAcknowledge if __name__ == "__main__": print("🎵 Starting Lyria Music Generator Agent...") print(f"📍 Agent address: {agent.address}") - + if gemini_api_key: print("✅ Lyria API configured") else: print("❌ ERROR: GEMINI_API_KEY not set") exit(1) - + if agentverse_api_key: print("✅ Agentverse storage configured") else: print("⚠️ WARNING: AGENTVERSE_API_KEY not set") - + print("\n🎯 Agent Features:") print(" • Music generation with Lyria RealTime") print(" • 30-second instrumental tracks") print(" • Real-time streaming synthesis") print(" • High-quality 48kHz stereo") print(" • Natural language prompts") - + print("\n⏳ Note: Music generation takes ~30 seconds") print("\n✅ Agent is running! Connect via ASI One to generate music.") print(" Press Ctrl+C to stop.\n") - + agent.run() diff --git a/gemini-quickstart/05-tts-speech-agent/tts_agent.py b/gemini-quickstart/05-tts-speech-agent/tts_agent.py index b581b68b..54de65b6 100644 --- a/gemini-quickstart/05-tts-speech-agent/tts_agent.py +++ b/gemini-quickstart/05-tts-speech-agent/tts_agent.py @@ -26,7 +26,7 @@ TextContent, ResourceContent, Resource, - chat_protocol_spec + chat_protocol_spec, ) from uagents_core.storage import ExternalStorage @@ -34,36 +34,44 @@ load_dotenv() # Configure Gemini -gemini_api_key = os.getenv('GEMINI_API_KEY') +gemini_api_key = os.getenv("GEMINI_API_KEY") if not gemini_api_key: raise ValueError("GEMINI_API_KEY not found in environment variables") # Configure Agentverse Storage -agentverse_api_key = os.getenv('AGENTVERSE_API_KEY') +agentverse_api_key = os.getenv("AGENTVERSE_API_KEY") if not agentverse_api_key: raise ValueError("AGENTVERSE_API_KEY not found in environment variables") storage_url = os.getenv("AGENTVERSE_URL", "https://agentverse.ai") + "/v1/storage" -external_storage = ExternalStorage(api_token=agentverse_api_key, storage_url=storage_url) +external_storage = ExternalStorage( + api_token=agentverse_api_key, storage_url=storage_url +) # Initialize Gemini client client = genai.Client(api_key=gemini_api_key) # Model configuration -TTS_MODEL = 'gemini-2.5-pro-preview-tts' +TTS_MODEL = "gemini-2.5-pro-preview-tts" # Available voices AVAILABLE_VOICES = [ - "Zephyr", "Puck", "Charon", "Kore", - "Fenrir", "Aoede", "Orbit", "Nimbus" + "Zephyr", + "Puck", + "Charon", + "Kore", + "Fenrir", + "Aoede", + "Orbit", + "Nimbus", ] # Create agent agent = Agent( name="tts_generator", - seed="", # Change this for your agent to a unique seed phrase + seed="", # Change this for your agent to a unique seed phrase port=8004, - mailbox=True + mailbox=True, ) # Initialize chat protocol @@ -96,9 +104,10 @@ def create_text_chat(text: str) -> ChatMessage: return ChatMessage( timestamp=datetime.now(timezone.utc), msg_id=uuid4(), - content=[TextContent(text=text, type="text")] + content=[TextContent(text=text, type="text")], ) + def create_resource_chat(asset_id: str, uri: str, caption: str = None) -> ChatMessage: """Create a ChatMessage with ResourceContent (for audio)""" content = [ @@ -106,24 +115,19 @@ def create_resource_chat(asset_id: str, uri: str, caption: str = None) -> ChatMe type="resource", resource_id=asset_id, resource=Resource( - uri=uri, - metadata={ - "mime_type": "audio/wav", - "role": "generated-speech" - } - ) + uri=uri, metadata={"mime_type": "audio/wav", "role": "generated-speech"} + ), ) ] - + if caption: content.append(TextContent(text=caption, type="text")) - + return ChatMessage( - timestamp=datetime.now(timezone.utc), - msg_id=uuid4(), - content=content + timestamp=datetime.now(timezone.utc), msg_id=uuid4(), content=content ) + def parse_audio_mime_type(mime_type: str) -> dict: """Parse audio MIME type for bits per sample and rate""" bits_per_sample = 16 @@ -145,6 +149,7 @@ def parse_audio_mime_type(mime_type: str) -> dict: return {"bits_per_sample": bits_per_sample, "rate": rate} + def convert_to_wav(audio_data: bytes, mime_type: str) -> bytes: """Convert raw audio to WAV format""" parameters = parse_audio_mime_type(mime_type) @@ -171,18 +176,20 @@ def convert_to_wav(audio_data: bytes, mime_type: str) -> bytes: block_align, bits_per_sample, b"data", - data_size + data_size, ) return header + audio_data + def detect_speakers(text: str) -> bool: """Detect if text has multi-speaker format""" - return bool(re.search(r'Speaker \d+:', text)) + return bool(re.search(r"Speaker \d+:", text)) + def parse_speakers(text: str) -> list: """Parse speakers from text""" speakers = set() - for match in re.finditer(r'(Speaker \d+):', text): + for match in re.finditer(r"(Speaker \d+):", text): speakers.add(match.group(1)) return sorted(list(speakers)) @@ -192,24 +199,24 @@ async def startup(ctx: Context): """Initialize agent on startup""" ctx.logger.info("🎤 Starting TTS Speech Generator...") ctx.logger.info(f"📍 Agent address: {agent.address}") - + if gemini_api_key: ctx.logger.info("✅ Gemini TTS configured") else: ctx.logger.error("❌ Gemini API key not set") - + if agentverse_api_key: ctx.logger.info("✅ Agentverse storage configured") else: ctx.logger.warning("⚠️ Agentverse API key not set") - + ctx.storage.set("total_speeches", 0) @chat_proto.on_message(ChatMessage) async def handle_chat_message(ctx: Context, sender: str, msg: ChatMessage): """Handle incoming chat messages and generate speech""" - + try: # Extract text user_prompt = "" @@ -217,59 +224,61 @@ async def handle_chat_message(ctx: Context, sender: str, msg: ChatMessage): if isinstance(item, TextContent): user_prompt = item.text break - + if not user_prompt: ctx.logger.warning("No text content in message") return - + ctx.logger.info(f"📨 Prompt from {sender}: {user_prompt[:50]}...") - + # Send acknowledgement - await ctx.send(sender, ChatAcknowledgement( - timestamp=datetime.now(timezone.utc), - acknowledged_msg_id=msg.msg_id - )) - + await ctx.send( + sender, + ChatAcknowledgement( + timestamp=datetime.now(timezone.utc), acknowledged_msg_id=msg.msg_id + ), + ) + # Check for help requests (not dialogue containing these words) lower_prompt = user_prompt.lower().strip() is_help_request = ( - lower_prompt.startswith('help') or - lower_prompt.startswith('what can you') or - lower_prompt.startswith('how do') or - lower_prompt.startswith('show voices') or - lower_prompt == 'voices' or - 'list voices' in lower_prompt + lower_prompt.startswith("help") + or lower_prompt.startswith("what can you") + or lower_prompt.startswith("how do") + or lower_prompt.startswith("show voices") + or lower_prompt == "voices" + or "list voices" in lower_prompt ) - + if is_help_request: help_msg = f"""{SYSTEM_PROMPT} **Available Voices:** -{', '.join(AVAILABLE_VOICES)} +{", ".join(AVAILABLE_VOICES)} **Examples:** • "Read this in a warm, welcoming tone: Hello world!" • Multi-speaker dialogue with Speaker 1: and Speaker 2: • Any text you want converted to speech!""" - + await ctx.send(sender, create_text_chat(help_msg)) return - + # Send generating message - await ctx.send(sender, create_text_chat( - "🎤 Generating speech... Please wait! ⏳" - )) - - ctx.logger.info(f"🎤 Starting speech generation...") - + await ctx.send( + sender, create_text_chat("🎤 Generating speech... Please wait! ⏳") + ) + + ctx.logger.info("🎤 Starting speech generation...") + # Detect if multi-speaker is_multi_speaker = detect_speakers(user_prompt) - + # Build config if is_multi_speaker: speakers = parse_speakers(user_prompt) ctx.logger.info(f"Multi-speaker detected: {speakers}") - + # Assign voices to speakers speaker_configs = [] for i, speaker in enumerate(speakers): @@ -284,7 +293,7 @@ async def handle_chat_message(ctx: Context, sender: str, msg: ChatMessage): ), ) ) - + speech_config = types.SpeechConfig( multi_speaker_voice_config=types.MultiSpeakerVoiceConfig( speaker_voice_configs=speaker_configs @@ -294,12 +303,10 @@ async def handle_chat_message(ctx: Context, sender: str, msg: ChatMessage): # Single speaker - use default voice speech_config = types.SpeechConfig( voice_config=types.VoiceConfig( - prebuilt_voice_config=types.PrebuiltVoiceConfig( - voice_name="Zephyr" - ) + prebuilt_voice_config=types.PrebuiltVoiceConfig(voice_name="Zephyr") ) ) - + # Generate speech contents = [ types.Content( @@ -307,16 +314,16 @@ async def handle_chat_message(ctx: Context, sender: str, msg: ChatMessage): parts=[types.Part.from_text(text=user_prompt)], ), ] - + generate_config = types.GenerateContentConfig( temperature=1, response_modalities=["audio"], speech_config=speech_config, ) - + # Collect audio chunks audio_chunks = [] - + for chunk in client.models.generate_content_stream( model=TTS_MODEL, contents=contents, @@ -329,59 +336,66 @@ async def handle_chat_message(ctx: Context, sender: str, msg: ChatMessage): ): part = chunk.candidates[0].content.parts[0] if part.inline_data and part.inline_data.data: - audio_chunks.append({ - 'data': part.inline_data.data, - 'mime_type': part.inline_data.mime_type - }) - + audio_chunks.append( + { + "data": part.inline_data.data, + "mime_type": part.inline_data.mime_type, + } + ) + if not audio_chunks: error_msg = "❌ No audio generated. Please try again." await ctx.send(sender, create_text_chat(error_msg)) return - + ctx.logger.info(f"✅ Collected {len(audio_chunks)} audio chunks") - + # Combine and convert to WAV - combined_audio = b'' + combined_audio = b"" for chunk in audio_chunks: - mime_type = chunk['mime_type'] - data = chunk['data'] - + mime_type = chunk["mime_type"] + data = chunk["data"] + # Check if needs conversion file_ext = mimetypes.guess_extension(mime_type) - if file_ext is None or file_ext != '.wav': + if file_ext is None or file_ext != ".wav": data = convert_to_wav(data, mime_type) - + combined_audio += data - + ctx.logger.info(f"📦 Combined audio: {len(combined_audio)} bytes") - + # Upload to Agentverse storage ctx.logger.info("📤 Uploading to Agentverse...") - + asset_id = external_storage.create_asset( name=f"speech_{int(datetime.now().timestamp())}", content=combined_audio, - mime_type="audio/wav" + mime_type="audio/wav", ) - + external_storage.set_permissions(asset_id=asset_id, agent_address=sender) asset_uri = f"agent-storage://{storage_url}/{asset_id}" - + # Track and send total_speeches = ctx.storage.get("total_speeches") or 0 ctx.storage.set("total_speeches", total_speeches + 1) - - speaker_info = f"{len(parse_speakers(user_prompt))} speakers" if is_multi_speaker else "Single voice" + + speaker_info = ( + f"{len(parse_speakers(user_prompt))} speakers" + if is_multi_speaker + else "Single voice" + ) caption = f"🎤 {user_prompt[:80]}... ({speaker_info})" await ctx.send(sender, create_resource_chat(asset_id, asset_uri, caption)) ctx.logger.info(f"🎤 Speech sent to {sender}!") - + except Exception as e: ctx.logger.error(f"❌ Error: {e}") import traceback + ctx.logger.error(traceback.format_exc()) - + error_msg = f"❌ Speech generation error: {str(e)[:200]}\n\nPlease try again with simpler text." await ctx.send(sender, create_text_chat(error_msg)) @@ -399,26 +413,26 @@ async def handle_acknowledgement(ctx: Context, sender: str, msg: ChatAcknowledge if __name__ == "__main__": print("🎤 Starting Gemini TTS Speech Generator Agent...") print(f"📍 Agent address: {agent.address}") - + if gemini_api_key: print("✅ Gemini TTS configured") else: print("❌ ERROR: GEMINI_API_KEY not set") exit(1) - + if agentverse_api_key: print("✅ Agentverse storage configured") else: print("⚠️ WARNING: AGENTVERSE_API_KEY not set") - + print("\n🎯 Agent Features:") print(" • Text-to-speech with Gemini 2.5 Pro") print(" • Multi-speaker dialogue support") print(" • 8 different voice presets") print(" • Natural, expressive speech") print(" • High-quality audio output") - + print("\n✅ Agent is running! Connect via ASI One to generate speech.") print(" Press Ctrl+C to stop.\n") - + agent.run() diff --git a/gemini-quickstart/06-web-research-agent/research_agent.py b/gemini-quickstart/06-web-research-agent/research_agent.py index 5d236338..47d248f5 100644 --- a/gemini-quickstart/06-web-research-agent/research_agent.py +++ b/gemini-quickstart/06-web-research-agent/research_agent.py @@ -22,14 +22,14 @@ ChatMessage, ChatAcknowledgement, TextContent, - chat_protocol_spec + chat_protocol_spec, ) # Load environment variables load_dotenv() # Configure Gemini -gemini_api_key = os.getenv('GEMINI_API_KEY') +gemini_api_key = os.getenv("GEMINI_API_KEY") if not gemini_api_key: raise ValueError("GEMINI_API_KEY not found in environment variables") @@ -37,14 +37,14 @@ client = genai.Client(api_key=gemini_api_key) # Model configuration -MODEL = 'gemini-2.5-flash' +MODEL = "gemini-2.5-flash" # Create agent agent = Agent( name="web_researcher", - seed="", # Change this for your agent to a unique seed phrase + seed="", # Change this for your agent to a unique seed phrase port=8005, - mailbox=True + mailbox=True, ) # Initialize chat protocol @@ -77,34 +77,35 @@ def create_text_chat(text: str) -> ChatMessage: return ChatMessage( timestamp=datetime.now(timezone.utc), msg_id=uuid4(), - content=[TextContent(text=text, type="text")] + content=[TextContent(text=text, type="text")], ) + def parse_url_and_question(text: str) -> tuple: """Extract URL and question from user input""" # Pattern 1: "URL: Question: " - pattern1 = r'URL:\s*(https?://\S+)\s+Question:\s*(.+)' + pattern1 = r"URL:\s*(https?://\S+)\s+Question:\s*(.+)" match = re.search(pattern1, text, re.IGNORECASE) if match: return match.group(1), match.group(2) - + # Pattern 2: " - " - pattern2 = r'(https?://\S+)\s*-\s*(.+)' + pattern2 = r"(https?://\S+)\s*-\s*(.+)" match = re.search(pattern2, text) if match: return match.group(1), match.group(2) - + # Pattern 3: Just URL (default question: "Summarize this") - pattern3 = r'(https?://\S+)' + pattern3 = r"(https?://\S+)" match = re.search(pattern3, text) if match: url = match.group(1) # Remove URL from text to get the question - question = text.replace(url, '').strip() + question = text.replace(url, "").strip() if not question: question = "Summarize the main points from this page" return url, question - + return None, None @@ -113,19 +114,19 @@ async def startup(ctx: Context): """Initialize agent on startup""" ctx.logger.info("🔍 Starting Web Research Agent...") ctx.logger.info(f"📍 Agent address: {agent.address}") - + if gemini_api_key: ctx.logger.info("✅ Gemini API configured") else: ctx.logger.error("❌ Gemini API key not set") - + ctx.storage.set("total_researches", 0) @chat_proto.on_message(ChatMessage) async def handle_chat_message(ctx: Context, sender: str, msg: ChatMessage): """Handle incoming chat messages and perform web research""" - + try: # Extract text user_prompt = "" @@ -133,22 +134,24 @@ async def handle_chat_message(ctx: Context, sender: str, msg: ChatMessage): if isinstance(item, TextContent): user_prompt = item.text break - + if not user_prompt: ctx.logger.warning("No text content in message") return - + ctx.logger.info(f"📨 Prompt from {sender}: {user_prompt[:50]}...") - + # Send acknowledgement - await ctx.send(sender, ChatAcknowledgement( - timestamp=datetime.now(timezone.utc), - acknowledged_msg_id=msg.msg_id - )) - + await ctx.send( + sender, + ChatAcknowledgement( + timestamp=datetime.now(timezone.utc), acknowledged_msg_id=msg.msg_id + ), + ) + # Check for help lower_prompt = user_prompt.lower().strip() - if lower_prompt.startswith('help') or lower_prompt.startswith('how do'): + if lower_prompt.startswith("help") or lower_prompt.startswith("how do"): help_msg = f"""{SYSTEM_PROMPT} **Quick Examples:** @@ -157,13 +160,13 @@ async def handle_chat_message(ctx: Context, sender: str, msg: ChatMessage): • `https://news.site.com/article - What is this about?` Just send me a URL and your question!""" - + await ctx.send(sender, create_text_chat(help_msg)) return - + # Parse URL and question url, question = parse_url_and_question(user_prompt) - + if not url: error_msg = """❌ Could not find a valid URL in your message. @@ -175,21 +178,21 @@ async def handle_chat_message(ctx: Context, sender: str, msg: ChatMessage): **Example:** `https://ai.google.dev - What are the latest updates?`""" - + await ctx.send(sender, create_text_chat(error_msg)) return - + # Send researching message - await ctx.send(sender, create_text_chat( - f"🔍 Researching {url}... Please wait! ⏳" - )) - + await ctx.send( + sender, create_text_chat(f"🔍 Researching {url}... Please wait! ⏳") + ) + ctx.logger.info(f"🔍 Fetching: {url}") ctx.logger.info(f"❓ Question: {question}") - + # Build prompt with URL full_prompt = f"{question}\n\nURL for research: {url}" - + # Configure with URL context tool contents = [ types.Content( @@ -197,21 +200,21 @@ async def handle_chat_message(ctx: Context, sender: str, msg: ChatMessage): parts=[types.Part.from_text(text=full_prompt)], ), ] - + tools = [ types.Tool(url_context=types.UrlContext()), ] - + generate_config = types.GenerateContentConfig( thinking_config=types.ThinkingConfig( thinking_budget=-1, # Use extended thinking ), tools=tools, ) - + # Generate response response_text = "" - + for chunk in client.models.generate_content_stream( model=MODEL, contents=contents, @@ -219,29 +222,30 @@ async def handle_chat_message(ctx: Context, sender: str, msg: ChatMessage): ): if chunk.text: response_text += chunk.text - + if not response_text: error_msg = "❌ Could not fetch or analyze the URL. The page might be inaccessible or blocked." await ctx.send(sender, create_text_chat(error_msg)) return - + ctx.logger.info(f"✅ Research complete: {len(response_text)} chars") - + # Send response final_response = f"**Research Results for:** {url}\n\n{response_text}" await ctx.send(sender, create_text_chat(final_response)) - + # Track total = ctx.storage.get("total_researches") or 0 ctx.storage.set("total_researches", total + 1) - + ctx.logger.info(f"🔍 Response sent to {sender}!") - + except Exception as e: ctx.logger.error(f"❌ Error: {e}") import traceback + ctx.logger.error(traceback.format_exc()) - + error_msg = f"❌ Research error: {str(e)[:200]}\n\nThe URL might be inaccessible or there was an issue processing it." await ctx.send(sender, create_text_chat(error_msg)) @@ -259,21 +263,21 @@ async def handle_acknowledgement(ctx: Context, sender: str, msg: ChatAcknowledge if __name__ == "__main__": print("🔍 Starting Web Research Agent...") print(f"📍 Agent address: {agent.address}") - + if gemini_api_key: print("✅ Gemini API configured") else: print("❌ ERROR: GEMINI_API_KEY not set") exit(1) - + print("\n🎯 Agent Features:") print(" • Fetch and analyze web content") print(" • Answer questions about URLs") print(" • Extract key information") print(" • Summarize articles and pages") print(" • Deep thinking mode enabled") - + print("\n✅ Agent is running! Connect via ASI One for web research.") print(" Press Ctrl+C to stop.\n") - + agent.run() diff --git a/gemini-quickstart/07-short-film-agent/unified-movie-agent/config.py b/gemini-quickstart/07-short-film-agent/unified-movie-agent/config.py index 9ed0c1ca..29c42671 100644 --- a/gemini-quickstart/07-short-film-agent/unified-movie-agent/config.py +++ b/gemini-quickstart/07-short-film-agent/unified-movie-agent/config.py @@ -38,8 +38,8 @@ # key 4 → opening + closing videos API_KEYS = [ - os.getenv("7GEMINI_API_KEY"), # key 0 → scenes 0,1 + safety + creative - os.getenv("9GEMINI_API_KEY"), # key 1 → scenes 2,3 + os.getenv("7GEMINI_API_KEY"), # key 0 → scenes 0,1 + safety + creative + os.getenv("9GEMINI_API_KEY"), # key 1 → scenes 2,3 os.getenv("10GEMINI_API_KEY"), # key 2 → scenes 4,5 os.getenv("11GEMINI_API_KEY"), # key 3 → scenes 6,7 os.getenv("12GEMINI_API_KEY"), # key 4 → opening + closing @@ -64,8 +64,8 @@ def key_index_for_scene(scene_index: int) -> int: # Convenience aliases SAFETY_KEY = API_KEYS[0] CREATIVE_KEY = API_KEYS[0] -OPENING_KEY = API_KEYS[4] # dedicated key for opening -CLOSING_KEY = API_KEYS[4] # dedicated key for closing +OPENING_KEY = API_KEYS[4] # dedicated key for opening +CLOSING_KEY = API_KEYS[4] # dedicated key for closing # ── GCS ───────────────────────────────────────────────────────── GCS_BUCKET_NAME = os.getenv("GCS_BUCKET_NAME") diff --git a/gemini-quickstart/07-short-film-agent/unified-movie-agent/main.py b/gemini-quickstart/07-short-film-agent/unified-movie-agent/main.py index 271e3225..9988c8a1 100644 --- a/gemini-quickstart/07-short-film-agent/unified-movie-agent/main.py +++ b/gemini-quickstart/07-short-film-agent/unified-movie-agent/main.py @@ -20,16 +20,16 @@ load_dotenv() -from uagents import Agent, Context, Protocol -from uagents_core.contrib.protocols.chat import ( +from uagents import Agent, Context, Protocol # noqa: E402 +from uagents_core.contrib.protocols.chat import ( # noqa: E402 ChatMessage, ChatAcknowledgement, TextContent, chat_protocol_spec, ) -from config import AGENT_NAME, AGENT_SEED, AGENT_PORT, SCENE_COUNT -from pipeline.orchestrator import produce_film +from config import AGENT_NAME, AGENT_SEED, AGENT_PORT, SCENE_COUNT # noqa: E402 +from pipeline.orchestrator import produce_film # noqa: E402 logging.basicConfig( level=logging.INFO, @@ -64,6 +64,7 @@ def _extract(raw: str): # ── Chat helpers ──────────────────────────────────────────────── + def _chat_msg(text: str) -> ChatMessage: return ChatMessage( timestamp=datetime.now(timezone.utc), @@ -74,8 +75,8 @@ def _chat_msg(text: str) -> ChatMessage: # ── Request queue ─────────────────────────────────────────────── -_active: Optional[str] = None # request-id of in-flight film -_queue: List[Dict[str, Any]] = [] # waiting requests +_active: Optional[str] = None # request-id of in-flight film +_queue: List[Dict[str, Any]] = [] # waiting requests async def _process_queue(ctx: Context) -> None: @@ -86,7 +87,9 @@ async def _process_queue(ctx: Context) -> None: return nxt = _queue.pop(0) for i, q in enumerate(_queue): - await ctx.send(q["user"], _chat_msg(f"⏳ Queue update: you are now #{i+1} in line.")) + await ctx.send( + q["user"], _chat_msg(f"⏳ Queue update: you are now #{i + 1} in line.") + ) await _run_film(ctx, nxt["user"], nxt["prompt"], nxt["refs"]) @@ -136,7 +139,7 @@ async def notify(msg: str) -> None: lines.append("") if result.final_url: lines.append(f"📽️ **[Watch Full Movie]({result.final_url})**\n") - lines.append(f"**Final Movie:**\n") + lines.append("**Final Movie:**\n") lines.append(f"![]({result.final_url})\n") lines.append("Thank you for creating with the Unified Movie Agent ✨") await notify("\n".join(lines)) @@ -147,6 +150,7 @@ async def notify(msg: str) -> None: # ── Chat handler ──────────────────────────────────────────────── + @chat_proto.on_message(ChatMessage) async def handle_user_message(ctx: Context, sender: str, msg: ChatMessage) -> None: # Extract text @@ -173,10 +177,13 @@ async def handle_user_message(ctx: Context, sender: str, msg: ChatMessage) -> No if _active is not None: _queue.append({"user": sender, "prompt": prompt, "refs": refs}) pos = len(_queue) - await ctx.send(sender, _chat_msg( - f"⏳ **You're #{pos} in the queue.**\n" - f"Another film is being produced. I'll start yours automatically!" - )) + await ctx.send( + sender, + _chat_msg( + f"⏳ **You're #{pos} in the queue.**\n" + f"Another film is being produced. I'll start yours automatically!" + ), + ) return # No active film — run immediately (as background task so agent stays responsive) @@ -190,6 +197,7 @@ async def handle_ack(ctx: Context, sender: str, msg: ChatAcknowledgement) -> Non # ── Lifecycle ─────────────────────────────────────────────────── + @agent.on_event("startup") async def startup(ctx: Context) -> None: log.info("🎬 Unified Movie Agent starting…") diff --git a/gemini-quickstart/07-short-film-agent/unified-movie-agent/main_stripe.py b/gemini-quickstart/07-short-film-agent/unified-movie-agent/main_stripe.py index f4139727..0a0dafd7 100644 --- a/gemini-quickstart/07-short-film-agent/unified-movie-agent/main_stripe.py +++ b/gemini-quickstart/07-short-film-agent/unified-movie-agent/main_stripe.py @@ -28,14 +28,14 @@ load_dotenv() -from uagents import Agent, Context, Protocol -from uagents_core.contrib.protocols.chat import ( +from uagents import Agent, Context, Protocol # noqa: E402 +from uagents_core.contrib.protocols.chat import ( # noqa: E402 ChatMessage, ChatAcknowledgement, TextContent, chat_protocol_spec, ) -from uagents_core.contrib.protocols.payment import ( +from uagents_core.contrib.protocols.payment import ( # noqa: E402 CommitPayment, CompletePayment, Funds, @@ -43,10 +43,10 @@ RequestPayment, ) -from config import SCENE_COUNT -from pipeline.orchestrator import produce_film -from payment_proto import build_payment_proto -from stripe_payments import ( +from config import SCENE_COUNT # noqa: E402 +from pipeline.orchestrator import produce_film # noqa: E402 +from payment_proto import build_payment_proto # noqa: E402 +from stripe_payments import ( # noqa: E402 STRIPE_AMOUNT_CENTS, STRIPE_CURRENCY, create_embedded_checkout_session, @@ -103,6 +103,7 @@ def _chat_msg(text: str) -> ChatMessage: # ── State helpers (per-sender, stored in ctx.storage) ───────────── + def _state_key(sender: str) -> str: return f"film_payment_state:{sender}" @@ -130,8 +131,8 @@ def _clear_state(ctx: Context, sender: str) -> None: # ── Request queue (one film at a time) ───────────────────────────── -_active: Optional[str] = None # request-id of in-flight film -_queue: List[Dict[str, Any]] = [] # waiting requests +_active: Optional[str] = None # request-id of in-flight film +_queue: List[Dict[str, Any]] = [] # waiting requests async def _process_queue(ctx: Context) -> None: @@ -142,7 +143,9 @@ async def _process_queue(ctx: Context) -> None: return nxt = _queue.pop(0) for i, q in enumerate(_queue): - await ctx.send(q["user"], _chat_msg(f"⏳ Queue update: you are now #{i+1} in line.")) + await ctx.send( + q["user"], _chat_msg(f"⏳ Queue update: you are now #{i + 1} in line.") + ) await _run_film(ctx, nxt["user"], nxt["prompt"], nxt["refs"]) @@ -191,7 +194,7 @@ async def notify(msg: str) -> None: lines.append("") if result.final_url: lines.append(f"📽️ **[Watch Full Movie]({result.final_url})**\n") - lines.append(f"**Final Movie:**\n") + lines.append("**Final Movie:**\n") lines.append(f"![]({result.final_url})\n") lines.append("Thank you for creating with the Unified Movie Agent ✨") await notify("\n".join(lines)) @@ -202,6 +205,7 @@ async def notify(msg: str) -> None: # ── Chat handler (payment-gated) ────────────────────────────────── + @chat_proto.on_message(ChatMessage) async def handle_user_message(ctx: Context, sender: str, msg: ChatMessage) -> None: # Extract text @@ -242,10 +246,13 @@ async def handle_user_message(ctx: Context, sender: str, msg: ChatMessage) -> No ) except Exception as e: log.error("Stripe checkout creation failed: %s", e, exc_info=True) - await ctx.send(sender, _chat_msg( - f"❌ **Payment setup failed.** Could not create Stripe checkout session.\n" - f"Error: {e}\n\nPlease try again." - )) + await ctx.send( + sender, + _chat_msg( + f"❌ **Payment setup failed.** Could not create Stripe checkout session.\n" + f"Error: {e}\n\nPlease try again." + ), + ) return log.info( @@ -268,7 +275,9 @@ async def handle_user_message(ctx: Context, sender: str, msg: ChatMessage) -> No # Send RequestPayment FIRST (triggers Stripe checkout UI in Agentverse) amount_str = f"{STRIPE_AMOUNT_CENTS / 100:.2f}" req = RequestPayment( - accepted_funds=[Funds(currency="USD", amount=amount_str, payment_method="stripe")], + accepted_funds=[ + Funds(currency="USD", amount=amount_str, payment_method="stripe") + ], recipient=str(ctx.agent.address), deadline_seconds=300, reference=str(ctx.session), @@ -277,9 +286,12 @@ async def handle_user_message(ctx: Context, sender: str, msg: ChatMessage) -> No ) await ctx.send(sender, req) # Chat message AFTER RequestPayment (UI needs RequestPayment first to render checkout) - await ctx.send(sender, _chat_msg( - "Once payment completes, I'll start generating your film automatically." - )) + await ctx.send( + sender, + _chat_msg( + "Once payment completes, I'll start generating your film automatically." + ), + ) log.info("💳 RequestPayment sent for %s — awaiting payment", sender[:16]) @@ -291,16 +303,22 @@ async def handle_ack(ctx: Context, sender: str, msg: ChatAcknowledgement) -> Non # ── Payment handlers ────────────────────────────────────────────── + async def on_commit(ctx: Context, sender: str, msg: CommitPayment) -> None: """Called when the UI sends CommitPayment after user completes Stripe checkout.""" try: log.info("💰 CommitPayment received from %s", sender[:16]) log.info(" transaction_id=%s", msg.transaction_id) log.info(" funds=%s", msg.funds) - log.info(" payment_method=%s", getattr(msg.funds, "payment_method", None) if msg.funds else None) + log.info( + " payment_method=%s", + getattr(msg.funds, "payment_method", None) if msg.funds else None, + ) # Defensive: check funds exists before accessing payment_method - payment_method = getattr(msg.funds, "payment_method", None) if msg.funds else None + ( + getattr(msg.funds, "payment_method", None) if msg.funds else None + ) if not msg.transaction_id: log.warning("No transaction_id in CommitPayment") await ctx.send(sender, RejectPayment(reason="Missing transaction ID.")) @@ -311,9 +329,12 @@ async def on_commit(ctx: Context, sender: str, msg: CommitPayment) -> None: paid = await asyncio.to_thread(verify_checkout_session_paid, msg.transaction_id) log.info("Stripe verification result: paid=%s", paid) if not paid: - await ctx.send(sender, RejectPayment( - reason="Stripe payment not completed yet. Please finish checkout." - )) + await ctx.send( + sender, + RejectPayment( + reason="Stripe payment not completed yet. Please finish checkout." + ), + ) return # Payment verified — send CompletePayment @@ -322,31 +343,45 @@ async def on_commit(ctx: Context, sender: str, msg: CommitPayment) -> None: # Load the stored prompt + refs state = _load_state(ctx, sender) - log.info("Loaded state for %s: has_prompt=%s, has_refs=%s", sender[:16], bool(state.get("prompt")), bool(state.get("refs"))) + log.info( + "Loaded state for %s: has_prompt=%s, has_refs=%s", + sender[:16], + bool(state.get("prompt")), + bool(state.get("refs")), + ) prompt = state.get("prompt", "") refs = state.get("refs", []) _clear_state(ctx, sender) if not prompt: - await ctx.send(sender, _chat_msg( - "✅ Payment received, but I couldn't find your film prompt. " - "Please send your prompt again (no additional payment needed — " - "contact support if charged)." - )) + await ctx.send( + sender, + _chat_msg( + "✅ Payment received, but I couldn't find your film prompt. " + "Please send your prompt again (no additional payment needed — " + "contact support if charged)." + ), + ) return - await ctx.send(sender, _chat_msg( - "✅ **Payment confirmed!** Starting your film production now...\n" - )) + await ctx.send( + sender, + _chat_msg( + "✅ **Payment confirmed!** Starting your film production now...\n" + ), + ) # Queue or run immediately if _active is not None: _queue.append({"user": sender, "prompt": prompt, "refs": refs}) pos = len(_queue) - await ctx.send(sender, _chat_msg( - f"⏳ **You're #{pos} in the queue.**\n" - f"Another film is being produced. Yours will start automatically!" - )) + await ctx.send( + sender, + _chat_msg( + f"⏳ **You're #{pos} in the queue.**\n" + f"Another film is being produced. Yours will start automatically!" + ), + ) else: asyncio.create_task(_run_film(ctx, sender, prompt, refs)) @@ -362,10 +397,13 @@ async def on_reject(ctx: Context, sender: str, msg: RejectPayment) -> None: """Called when payment is rejected or cancelled.""" log.info("❌ RejectPayment from %s: %s", sender[:16], msg.reason) _clear_state(ctx, sender) - await ctx.send(sender, _chat_msg( - f"❌ **Payment was cancelled or rejected.** {msg.reason or ''}\n\n" - f"Send your prompt again anytime to start a new checkout." - )) + await ctx.send( + sender, + _chat_msg( + f"❌ **Payment was cancelled or rejected.** {msg.reason or ''}\n\n" + f"Send your prompt again anytime to start a new checkout." + ), + ) # ── Wire up protocols ───────────────────────────────────────────── @@ -378,6 +416,7 @@ async def on_reject(ctx: Context, sender: str, msg: RejectPayment) -> None: # ── Lifecycle ───────────────────────────────────────────────────── + @agent.on_event("startup") async def startup(ctx: Context) -> None: log.info("🎬 Unified Movie Agent (Stripe-gated) starting…") @@ -388,6 +427,8 @@ async def startup(ctx: Context) -> None: if __name__ == "__main__": print("🎬 Unified Movie Agent (Stripe-gated)") print(f"📍 Address: {agent.address}") - print(f"🎞️ {SCENE_COUNT} scenes | 💳 Stripe payment: ${STRIPE_AMOUNT_CENTS/100:.2f} {STRIPE_CURRENCY.upper()}") + print( + f"🎞️ {SCENE_COUNT} scenes | 💳 Stripe payment: ${STRIPE_AMOUNT_CENTS / 100:.2f} {STRIPE_CURRENCY.upper()}" + ) print() agent.run() diff --git a/gemini-quickstart/07-short-film-agent/unified-movie-agent/models.py b/gemini-quickstart/07-short-film-agent/unified-movie-agent/models.py index 3e0f1495..303fbeab 100644 --- a/gemini-quickstart/07-short-film-agent/unified-movie-agent/models.py +++ b/gemini-quickstart/07-short-film-agent/unified-movie-agent/models.py @@ -4,34 +4,37 @@ """ from __future__ import annotations -from dataclasses import dataclass, field +from dataclasses import dataclass from typing import List, Optional @dataclass class SceneBrief: """Per-scene creative brief produced by the Creative Director.""" - scene_index: int # 1-based (1..8) + + scene_index: int # 1-based (1..8) scene_title: str - visual_prompt: str # for Veo - voiceover_prompt: str # for TTS (≤16 words) - music_prompt: str # for Lyria + visual_prompt: str # for Veo + voiceover_prompt: str # for TTS (≤16 words) + music_prompt: str # for Lyria duration_seconds: int = 8 @dataclass class StoryPlan: """Full story plan returned by the Creative Director.""" + title: str logline: str scenes: List[SceneBrief] - status: str = "ok" # "ok" | "fallback" | "error" + status: str = "ok" # "ok" | "fallback" | "error" error: Optional[str] = None @dataclass class SceneResult: """Aggregated result for a single scene after all pipeline steps.""" + scene_index: int scene_title: str = "" video_url: Optional[str] = None @@ -44,6 +47,7 @@ class SceneResult: @dataclass class FilmResult: """Final output of the full pipeline.""" + title: str logline: str scenes: List[SceneResult] diff --git a/gemini-quickstart/07-short-film-agent/unified-movie-agent/pipeline/assembly.py b/gemini-quickstart/07-short-film-agent/unified-movie-agent/pipeline/assembly.py index 9794dba7..cd93492d 100644 --- a/gemini-quickstart/07-short-film-agent/unified-movie-agent/pipeline/assembly.py +++ b/gemini-quickstart/07-short-film-agent/unified-movie-agent/pipeline/assembly.py @@ -23,6 +23,7 @@ # ── Helpers ────────────────────────────────────────────────────── + async def _download(url: str, path: Path) -> Path: async with httpx.AsyncClient(timeout=300.0) as http: resp = await http.get(url) @@ -35,9 +36,12 @@ def _duration(media_path: Path) -> float: try: result = subprocess.run( ["ffmpeg", "-i", str(media_path), "-f", "null", "-"], - capture_output=True, text=True, + capture_output=True, + text=True, + ) + m = re.search( + r"Duration: (\d{2}):(\d{2}):(\d{2}(?:\.\d+)?)", result.stderr or "" ) - m = re.search(r"Duration: (\d{2}):(\d{2}):(\d{2}(?:\.\d+)?)", result.stderr or "") if not m: return 0.0 return int(m.group(1)) * 3600 + int(m.group(2)) * 60 + float(m.group(3)) @@ -53,10 +57,21 @@ def _adjust_duration(audio: Path, target: float, out: Path) -> Path: return audio if current < target: cmd = [ - "ffmpeg", "-i", str(audio), - "-f", "lavfi", "-i", "anullsrc=channel_layout=stereo:sample_rate=44100", - "-filter_complex", "[0:a][1:a]concat=n=2:v=0:a=1[out]", - "-map", "[out]", "-t", str(target), "-y", str(out), + "ffmpeg", + "-i", + str(audio), + "-f", + "lavfi", + "-i", + "anullsrc=channel_layout=stereo:sample_rate=44100", + "-filter_complex", + "[0:a][1:a]concat=n=2:v=0:a=1[out]", + "-map", + "[out]", + "-t", + str(target), + "-y", + str(out), ] else: cmd = ["ffmpeg", "-i", str(audio), "-t", str(target), "-y", str(out)] @@ -67,13 +82,28 @@ def _adjust_duration(audio: Path, target: float, out: Path) -> Path: def _combine(video: Path, voice: Path, music: Path, out: Path) -> Path: cmd = [ "ffmpeg", - "-i", str(video), "-i", str(voice), "-i", str(music), + "-i", + str(video), + "-i", + str(voice), + "-i", + str(music), "-filter_complex", "[1:a]volume=1.0[voice];[2:a]volume=0.5[music];" "[voice][music]amix=inputs=2:duration=first[audio]", - "-map", "0:v", "-map", "[audio]", - "-c:v", "copy", "-c:a", "aac", "-b:a", "192k", - "-shortest", "-y", str(out), + "-map", + "0:v", + "-map", + "[audio]", + "-c:v", + "copy", + "-c:a", + "aac", + "-b:a", + "192k", + "-shortest", + "-y", + str(out), ] result = subprocess.run(cmd, capture_output=True, text=True) if result.returncode != 0: @@ -83,6 +113,7 @@ def _combine(video: Path, voice: Path, music: Path, out: Path) -> Path: # ── Public API ────────────────────────────────────────────────── + async def assemble_scene( scene_index: int, video_url: str, diff --git a/gemini-quickstart/07-short-film-agent/unified-movie-agent/pipeline/chargen.py b/gemini-quickstart/07-short-film-agent/unified-movie-agent/pipeline/chargen.py index 022d1103..9df69983 100644 --- a/gemini-quickstart/07-short-film-agent/unified-movie-agent/pipeline/chargen.py +++ b/gemini-quickstart/07-short-film-agent/unified-movie-agent/pipeline/chargen.py @@ -33,6 +33,7 @@ # ── Step 1: Extract characters from the story plan ─────────────── + async def _extract_characters( story_title: str, story_logline: str, @@ -40,7 +41,7 @@ async def _extract_characters( ) -> List[dict]: """Use Gemini to identify main characters and build image-gen prompts.""" scenes_text = "\n".join( - f"- Scene {i+1}: {vp}" for i, vp in enumerate(scene_visual_prompts) + f"- Scene {i + 1}: {vp}" for i, vp in enumerate(scene_visual_prompts) ) prompt = f"""Analyze this story plan and identify up to {MAX_CHARACTER_REFS} main characters. @@ -94,6 +95,7 @@ async def _extract_characters( # ── Step 2: Generate one reference image ───────────────────────── + async def _generate_character_image(name: str, image_prompt: str, index: int) -> str: """Generate one character reference image and upload to GCS.""" log.info("Generating reference image for: %s", name) @@ -110,9 +112,11 @@ async def _generate_character_image(name: str, image_prompt: str, index: int) -> ) for part in response.parts: - if hasattr(part, 'inline_data') and part.inline_data is not None: + if hasattr(part, "inline_data") and part.inline_data is not None: image_data = part.inline_data.data - mime_type = getattr(part.inline_data, 'mime_type', 'image/png') or 'image/png' + mime_type = ( + getattr(part.inline_data, "mime_type", "image/png") or "image/png" + ) # Handle base64-encoded data (str or bytes) if isinstance(image_data, str): @@ -120,7 +124,14 @@ async def _generate_character_image(name: str, image_prompt: str, index: int) -> elif isinstance(image_data, bytes): # Gemini sometimes returns base64 as bytes try: - if image_data[:20].decode('ascii', errors='ignore').replace('=', '').replace('+', '').replace('/', '').isalnum(): + if ( + image_data[:20] + .decode("ascii", errors="ignore") + .replace("=", "") + .replace("+", "") + .replace("/", "") + .isalnum() + ): img_bytes = base64.b64decode(image_data) else: img_bytes = image_data @@ -130,11 +141,13 @@ async def _generate_character_image(name: str, image_prompt: str, index: int) -> log.warning("Unexpected image data type: %s", type(image_data)) continue - ext = 'png' if 'png' in mime_type else 'jpg' + ext = "png" if "png" in mime_type else "jpg" ts = int(datetime.now().timestamp()) filename = f"charref_{ts}_{index}.{ext}" url = upload_to_storage(img_bytes, filename, mime_type) - log.info("Character ref '%s' uploaded: %s (%d bytes)", name, url, len(img_bytes)) + log.info( + "Character ref '%s' uploaded: %s (%d bytes)", name, url, len(img_bytes) + ) return url raise RuntimeError(f"Image generation returned no image for '{name}'") @@ -142,6 +155,7 @@ async def _generate_character_image(name: str, image_prompt: str, index: int) -> # ── Public API ─────────────────────────────────────────────────── + async def generate_character_refs( story_title: str, story_logline: str, @@ -155,7 +169,9 @@ async def generate_character_refs( log.info("Extracting characters from story plan…") try: characters = await _extract_characters( - story_title, story_logline, scene_visual_prompts, + story_title, + story_logline, + scene_visual_prompts, ) except Exception as e: log.warning("Character extraction failed: %s — skipping ref generation", e) diff --git a/gemini-quickstart/07-short-film-agent/unified-movie-agent/pipeline/creative.py b/gemini-quickstart/07-short-film-agent/unified-movie-agent/pipeline/creative.py index 7db84e05..cb31591d 100644 --- a/gemini-quickstart/07-short-film-agent/unified-movie-agent/pipeline/creative.py +++ b/gemini-quickstart/07-short-film-agent/unified-movie-agent/pipeline/creative.py @@ -6,7 +6,7 @@ import json import re import logging -from typing import List, Dict, Any, Optional +from typing import List, Dict, Any from google import genai @@ -20,6 +20,7 @@ # ── Text helpers ──────────────────────────────────────────────── + def _clean(text: str) -> str: if text is None: return "" @@ -54,14 +55,18 @@ def _fallback_scene(index: int, story: str) -> Dict[str, Any]: m = "Steady, hopeful underscore with subtle momentum" t = "Side by side, we keep pushing the world forward." return { - "scene_index": index, "scene_title": f"Scene {index}", - "visual_prompt": v, "music_prompt": m, - "voiceover_prompt": t, "duration_seconds": 8, + "scene_index": index, + "scene_title": f"Scene {index}", + "visual_prompt": v, + "music_prompt": m, + "voiceover_prompt": t, + "duration_seconds": 8, } # ── Gemini prompt ─────────────────────────────────────────────── + def _build_prompt(user_prompt: str, refs: List[str]) -> str: refs_str = json.dumps(refs or [], ensure_ascii=False) return f""" @@ -166,7 +171,7 @@ def _parse_json(raw: str) -> Dict[str, Any]: start, end = raw.find("{"), raw.rfind("}") if start != -1 and end > start: try: - return json.loads(raw[start:end + 1]) + return json.loads(raw[start : end + 1]) except json.JSONDecodeError: pass raise @@ -182,21 +187,29 @@ def _normalize(data: Dict[str, Any], story: str) -> List[SceneBrief]: for i, s in enumerate(raw_scenes[:SCENE_COUNT]): idx = max(1, min(SCENE_COUNT, int(s.get("scene_index", i + 1)))) - vp = _clean(str(s.get("visual_prompt", s.get("video_prompt", "")))) or \ - "Visualize a key moment from the story." - mp = _clean(str(s.get("music_prompt", ""))) or \ - "Cinematic, hopeful underscore with gentle forward motion" - vt = _truncate(str(s.get("voiceover_prompt", s.get("voiceover_text", "")))) or \ - "Side by side, we turn this idea into something real." - - briefs.append(SceneBrief( - scene_index=idx, - scene_title=_clean(str(s.get("scene_title", f"Scene {idx}"))), - visual_prompt=vp, - music_prompt=mp, - voiceover_prompt=vt, - duration_seconds=int(s.get("duration_seconds", 8)), - )) + vp = ( + _clean(str(s.get("visual_prompt", s.get("video_prompt", "")))) + or "Visualize a key moment from the story." + ) + mp = ( + _clean(str(s.get("music_prompt", ""))) + or "Cinematic, hopeful underscore with gentle forward motion" + ) + vt = ( + _truncate(str(s.get("voiceover_prompt", s.get("voiceover_text", "")))) + or "Side by side, we turn this idea into something real." + ) + + briefs.append( + SceneBrief( + scene_index=idx, + scene_title=_clean(str(s.get("scene_title", f"Scene {idx}"))), + visual_prompt=vp, + music_prompt=mp, + voiceover_prompt=vt, + duration_seconds=int(s.get("duration_seconds", 8)), + ) + ) while len(briefs) < SCENE_COUNT: fb = _fallback_scene(len(briefs) + 1, story) @@ -211,6 +224,7 @@ def _normalize(data: Dict[str, Any], story: str) -> List[SceneBrief]: # ── Public API ────────────────────────────────────────────────── + async def plan_story(user_prompt: str, ref_urls: List[str]) -> StoryPlan: """ Call Gemini Flash and return a StoryPlan with 8 SceneBriefs. @@ -234,11 +248,15 @@ async def plan_story(user_prompt: str, ref_urls: List[str]) -> StoryPlan: except Exception as e: log.error("Creative Director error: %s — using fallbacks", e) - fallback_scenes = [SceneBrief(**_fallback_scene(i, user_prompt)) - for i in range(1, SCENE_COUNT + 1)] + fallback_scenes = [ + SceneBrief(**_fallback_scene(i, user_prompt)) + for i in range(1, SCENE_COUNT + 1) + ] return StoryPlan( title="AI Story (Fallback)", - logline=user_prompt[:100] if user_prompt else "A story about humans and AI.", + logline=user_prompt[:100] + if user_prompt + else "A story about humans and AI.", scenes=fallback_scenes, status="fallback", error=str(e), diff --git a/gemini-quickstart/07-short-film-agent/unified-movie-agent/pipeline/music.py b/gemini-quickstart/07-short-film-agent/unified-movie-agent/pipeline/music.py index b702f74a..9ecfe005 100644 --- a/gemini-quickstart/07-short-film-agent/unified-movie-agent/pipeline/music.py +++ b/gemini-quickstart/07-short-film-agent/unified-movie-agent/pipeline/music.py @@ -16,8 +16,12 @@ from google.genai import types from config import ( - LYRIA_MODEL, LYRIA_SAMPLE_RATE, LYRIA_CHANNELS, - SCENE_DURATION_SECONDS, key_for_scene, API_KEYS, + LYRIA_MODEL, + LYRIA_SAMPLE_RATE, + LYRIA_CHANNELS, + SCENE_DURATION_SECONDS, + key_for_scene, + API_KEYS, ) from utils.gcs import upload_to_storage @@ -56,6 +60,7 @@ def _save_wav(pcm: bytes, path: str) -> None: # ── Public API ────────────────────────────────────────────────── + async def generate_music( scene_index: int, prompt: str, @@ -69,7 +74,9 @@ async def generate_music( api_key = key_for_scene(scene_index) client = _clients[api_key] - log.info("Music scene %d — prompt='%s', dur=%ds", scene_index, prompt[:60], duration) + log.info( + "Music scene %d — prompt='%s', dur=%ds", scene_index, prompt[:60], duration + ) audio_chunks: list[bytes] = [] is_collecting = True diff --git a/gemini-quickstart/07-short-film-agent/unified-movie-agent/pipeline/orchestrator.py b/gemini-quickstart/07-short-film-agent/unified-movie-agent/pipeline/orchestrator.py index d8e5d141..5740b6f8 100644 --- a/gemini-quickstart/07-short-film-agent/unified-movie-agent/pipeline/orchestrator.py +++ b/gemini-quickstart/07-short-film-agent/unified-movie-agent/pipeline/orchestrator.py @@ -38,6 +38,7 @@ async def _noop(_msg: str) -> None: # ── Per-scene pipeline ────────────────────────────────────────── + async def _produce_scene( brief: SceneBrief, ref_urls: List[str], @@ -49,12 +50,16 @@ async def _produce_scene( Uses retry wrapper for each step. """ idx = brief.scene_index # 1-based - idx0 = idx - 1 # 0-based (for key assignment) + idx0 = idx - 1 # 0-based (for key assignment) result = SceneResult(scene_index=idx, scene_title=brief.scene_title) try: # --- Generate video / voice / music in parallel --- - video_url, (voice_url, voice_dur), (music_url, music_dur) = await asyncio.gather( + ( + video_url, + (voice_url, voice_dur), + (music_url, music_dur), + ) = await asyncio.gather( with_retry( video.generate_scene_video, scene_index=idx0, @@ -110,6 +115,7 @@ async def _produce_scene( # ── Full pipeline ─────────────────────────────────────────────── + async def produce_film( user_prompt: str, ref_urls: Optional[List[str]] = None, @@ -128,7 +134,9 @@ async def produce_film( if not is_safe: log.warning("Prompt rejected: %s", reason) return FilmResult( - title="", logline="", scenes=[], + title="", + logline="", + scenes=[], error=f"Content policy violation: {reason}", ) log.info("Safety check passed") @@ -140,11 +148,7 @@ async def produce_film( ) plan: StoryPlan = await creative.plan_story(user_prompt, ref_urls) - summary = ( - f"📖 **{plan.title}**\n" - f"🧵 {plan.logline}\n\n" - f"**Scene Briefs:**\n\n" - ) + summary = f"📖 **{plan.title}**\n🧵 {plan.logline}\n\n**Scene Briefs:**\n\n" for sb in plan.scenes: summary += ( f"**Scene {sb.scene_index}: {sb.scene_title}**\n" @@ -162,7 +166,9 @@ async def produce_film( ) scene_visual_prompts = [s.visual_prompt for s in plan.scenes] auto_refs = await chargen.generate_character_refs( - plan.title, plan.logline, scene_visual_prompts, + plan.title, + plan.logline, + scene_visual_prompts, ) if auto_refs: ref_urls = auto_refs @@ -178,10 +184,7 @@ async def produce_film( ) # Scene tasks - scene_coros = [ - _produce_scene(brief, ref_urls, notify) - for brief in plan.scenes - ] + scene_coros = [_produce_scene(brief, ref_urls, notify) for brief in plan.scenes] # Opening / closing tasks opening_coro = with_retry( video.generate_opening, @@ -206,9 +209,15 @@ async def produce_film( return_exceptions=True, ) - scene_results: List[SceneResult] = results[0] if not isinstance(results[0], Exception) else [] - opening_url: Optional[str] = results[1] if not isinstance(results[1], Exception) else None - closing_url: Optional[str] = results[2] if not isinstance(results[2], Exception) else None + scene_results: List[SceneResult] = ( + results[0] if not isinstance(results[0], Exception) else [] + ) + opening_url: Optional[str] = ( + results[1] if not isinstance(results[1], Exception) else None + ) + closing_url: Optional[str] = ( + results[2] if not isinstance(results[2], Exception) else None + ) if isinstance(results[0], Exception): log.error("Scene batch failed: %s", results[0]) diff --git a/gemini-quickstart/07-short-film-agent/unified-movie-agent/pipeline/safety.py b/gemini-quickstart/07-short-film-agent/unified-movie-agent/pipeline/safety.py index 210a4661..a1e9dacb 100644 --- a/gemini-quickstart/07-short-film-agent/unified-movie-agent/pipeline/safety.py +++ b/gemini-quickstart/07-short-film-agent/unified-movie-agent/pipeline/safety.py @@ -54,7 +54,11 @@ async def check_prompt_safety(prompt: str) -> tuple[bool, str]: result = response.text.strip() if result.upper().startswith("UNSAFE"): - reason = result.split(":", 1)[1].strip() if ":" in result else "Content policy violation" + reason = ( + result.split(":", 1)[1].strip() + if ":" in result + else "Content policy violation" + ) return False, reason elif result.upper().startswith("SAFE"): return True, "" diff --git a/gemini-quickstart/07-short-film-agent/unified-movie-agent/pipeline/stitcher.py b/gemini-quickstart/07-short-film-agent/unified-movie-agent/pipeline/stitcher.py index 7ca3d24d..90266196 100644 --- a/gemini-quickstart/07-short-film-agent/unified-movie-agent/pipeline/stitcher.py +++ b/gemini-quickstart/07-short-film-agent/unified-movie-agent/pipeline/stitcher.py @@ -34,13 +34,34 @@ def _concat_ffmpeg(paths: List[Path], out: Path) -> None: cmd = ["ffmpeg", "-loglevel", "warning"] for p in paths: cmd.extend(["-i", str(p)]) - cmd.extend([ - "-filter_complex", filt, - "-map", "[outv]", "-map", "[outa]", - "-c:v", "libx264", "-preset", "medium", "-crf", "23", - "-c:a", "aac", "-b:a", "192k", "-ar", "48000", "-ac", "2", - "-movflags", "+faststart", "-y", str(out), - ]) + cmd.extend( + [ + "-filter_complex", + filt, + "-map", + "[outv]", + "-map", + "[outa]", + "-c:v", + "libx264", + "-preset", + "medium", + "-crf", + "23", + "-c:a", + "aac", + "-b:a", + "192k", + "-ar", + "48000", + "-ac", + "2", + "-movflags", + "+faststart", + "-y", + str(out), + ] + ) result = subprocess.run(cmd, capture_output=True, text=True) if result.returncode != 0: @@ -49,6 +70,7 @@ def _concat_ffmpeg(paths: List[Path], out: Path) -> None: # ── Public API ────────────────────────────────────────────────── + async def stitch_story( scene_urls: List[str], opening_url: str | None = None, diff --git a/gemini-quickstart/07-short-film-agent/unified-movie-agent/pipeline/tts.py b/gemini-quickstart/07-short-film-agent/unified-movie-agent/pipeline/tts.py index a08df67c..7aa2735c 100644 --- a/gemini-quickstart/07-short-film-agent/unified-movie-agent/pipeline/tts.py +++ b/gemini-quickstart/07-short-film-agent/unified-movie-agent/pipeline/tts.py @@ -23,6 +23,7 @@ # ── Helpers ────────────────────────────────────────────────────── + def _is_base64(data: bytes) -> bool: if len(data) < 100: return False @@ -64,9 +65,19 @@ def _to_wav(pcm: bytes, mime_type: str) -> bytes: block_align = ch * (bits // 8) header = struct.pack( "<4sI4s4sIHHIIHH4sI", - b"RIFF", 36 + len(pcm), b"WAVE", b"fmt ", 16, - 1, ch, rate, byte_rate, block_align, bits, - b"data", len(pcm), + b"RIFF", + 36 + len(pcm), + b"WAVE", + b"fmt ", + 16, + 1, + ch, + rate, + byte_rate, + block_align, + bits, + b"data", + len(pcm), ) return header + pcm @@ -78,6 +89,7 @@ def _estimate_duration(wav_bytes: bytes, rate: int = 24000, bits: int = 16) -> f # ── Public API ────────────────────────────────────────────────── + def _generate_voice_sync( client: genai.Client, scene_index: int, @@ -120,10 +132,12 @@ def _generate_voice_sync( ): part = chunk.candidates[0].content.parts[0] if part.inline_data and part.inline_data.data: - chunks.append({ - "data": part.inline_data.data, - "mime_type": part.inline_data.mime_type, - }) + chunks.append( + { + "data": part.inline_data.data, + "mime_type": part.inline_data.mime_type, + } + ) if not chunks: raise RuntimeError("No audio generated from TTS") diff --git a/gemini-quickstart/07-short-film-agent/unified-movie-agent/pipeline/video.py b/gemini-quickstart/07-short-film-agent/unified-movie-agent/pipeline/video.py index c438e467..cb76e0da 100644 --- a/gemini-quickstart/07-short-film-agent/unified-movie-agent/pipeline/video.py +++ b/gemini-quickstart/07-short-film-agent/unified-movie-agent/pipeline/video.py @@ -13,9 +13,14 @@ from google.genai import types from config import ( - VEO_MODEL, VEO_RESOLUTION, VEO_ASPECT_RATIO, - SCENE_DURATION_SECONDS, key_for_scene, - OPENING_KEY, CLOSING_KEY, API_KEYS, + VEO_MODEL, + VEO_RESOLUTION, + VEO_ASPECT_RATIO, + SCENE_DURATION_SECONDS, + key_for_scene, + OPENING_KEY, + CLOSING_KEY, + API_KEYS, ) from utils.gcs import upload_to_storage @@ -35,6 +40,7 @@ def _get_client(api_key: str) -> genai.Client: # ── Helpers ────────────────────────────────────────────────────── + async def _download_image(url: str) -> tuple[bytes, str]: async with httpx.AsyncClient(timeout=120.0) as http: resp = await http.get(url) @@ -60,10 +66,12 @@ async def _build_refs(urls: Optional[List[str]]) -> Optional[list]: for url in urls[:3]: try: img_bytes, mime = await _download_image(url) - refs.append(types.VideoGenerationReferenceImage( - image=types.Image(image_bytes=img_bytes, mime_type=mime), - reference_type="asset", - )) + refs.append( + types.VideoGenerationReferenceImage( + image=types.Image(image_bytes=img_bytes, mime_type=mime), + reference_type="asset", + ) + ) except Exception as e: log.warning("Failed to download ref image %s: %s", url, e) return refs or None @@ -124,9 +132,12 @@ async def _generate_video( try: video_bytes = client.files.download(file=video) except Exception: - uri = getattr(video, "uri", None) or \ - f"https://generativelanguage.googleapis.com/v1beta/{video.name}" + uri = ( + getattr(video, "uri", None) + or f"https://generativelanguage.googleapis.com/v1beta/{video.name}" + ) import requests + resp = requests.get(uri, headers={"Authorization": f"Bearer {api_key}"}) resp.raise_for_status() video_bytes = resp.content @@ -148,6 +159,7 @@ async def _generate_video( # ── Public API ────────────────────────────────────────────────── + async def generate_scene_video( scene_index: int, prompt: str, diff --git a/gemini-quickstart/07-short-film-agent/unified-movie-agent/utils/gcs.py b/gemini-quickstart/07-short-film-agent/unified-movie-agent/utils/gcs.py index d711a6da..adda574c 100644 --- a/gemini-quickstart/07-short-film-agent/unified-movie-agent/utils/gcs.py +++ b/gemini-quickstart/07-short-film-agent/unified-movie-agent/utils/gcs.py @@ -3,7 +3,6 @@ Returns public URLs. Supports base64 credentials (Agentverse) and ADC (local). """ -import os import base64 import json from typing import Optional @@ -23,7 +22,9 @@ credentials = service_account.Credentials.from_service_account_info( json.loads(creds_json) ) - _client = storage.Client(credentials=credentials, project=GOOGLE_CLOUD_PROJECT) + _client = storage.Client( + credentials=credentials, project=GOOGLE_CLOUD_PROJECT + ) else: _client = storage.Client(project=GOOGLE_CLOUD_PROJECT) @@ -33,11 +34,14 @@ print(f"❌ GCS init failed: {e}") -def upload_to_storage(file_data: bytes, filename: str, - content_type: str = "video/mp4") -> str: +def upload_to_storage( + file_data: bytes, filename: str, content_type: str = "video/mp4" +) -> str: """Upload bytes to GCS, return public URL.""" if not _client or not _bucket: - raise ValueError("GCS not configured. Set GCS_BUCKET_NAME and GOOGLE_CLOUD_PROJECT.") + raise ValueError( + "GCS not configured. Set GCS_BUCKET_NAME and GOOGLE_CLOUD_PROJECT." + ) blob = _bucket.blob(f"videos/{filename}") blob.upload_from_string(file_data, content_type=content_type) diff --git a/gemini-quickstart/07-short-film-agent/unified-movie-agent/utils/retry.py b/gemini-quickstart/07-short-film-agent/unified-movie-agent/utils/retry.py index a741c380..05e91111 100644 --- a/gemini-quickstart/07-short-film-agent/unified-movie-agent/utils/retry.py +++ b/gemini-quickstart/07-short-film-agent/unified-movie-agent/utils/retry.py @@ -35,13 +35,18 @@ async def with_retry( wait = backoff * (attempt + 1) log.warning( "%s failed (attempt %d/%d): %s — retrying in %.0fs", - label or fn.__name__, attempt + 1, max_retries + 1, - str(e)[:120], wait, + label or fn.__name__, + attempt + 1, + max_retries + 1, + str(e)[:120], + wait, ) await asyncio.sleep(wait) else: log.error( "%s failed after %d attempts: %s", - label or fn.__name__, max_retries + 1, str(e)[:200], + label or fn.__name__, + max_retries + 1, + str(e)[:200], ) raise last_error # type: ignore[misc] diff --git a/google-genai-parallel-processing/brand-management-agent/agent.py b/google-genai-parallel-processing/brand-management-agent/agent.py index e0a65eea..b7a40914 100644 --- a/google-genai-parallel-processing/brand-management-agent/agent.py +++ b/google-genai-parallel-processing/brand-management-agent/agent.py @@ -14,9 +14,7 @@ - Chat protocol handling """ -import asyncio import uuid -import time import requests import httpx from typing import AsyncIterable @@ -33,8 +31,6 @@ from uagents_core.contrib.protocols.chat import ( ChatAcknowledgement, ChatMessage, - EndSessionContent, - StartSessionContent, TextContent, Resource, ResourceContent, @@ -54,11 +50,15 @@ # Initialize external services openai.api_key = OPENAI_API_KEY -external_storage = ExternalStorage(api_token=AGENTVERSE_API_KEY, storage_url=STORAGE_URL) +external_storage = ExternalStorage( + api_token=AGENTVERSE_API_KEY, storage_url=STORAGE_URL +) + # === Base Processor Class === class ASI1MiniProcessor(processor.Processor): - """ Base processor for ASI:One Mini text generation.""" + """Base processor for ASI:One Mini text generation.""" + def __init__(self, prompt: str, task: str, metadata_type: str): self.prompt = prompt self.task = task @@ -74,47 +74,65 @@ async def call(self, content: AsyncIterable[ProcessorPart]): ASI1_BASE_URL, headers={ "Authorization": f"Bearer {ASI1_API_KEY}", - "Content-Type": "application/json" + "Content-Type": "application/json", }, json={ "model": "asi1-mini", - "messages": [{"role": "user", "content": f"{self.task} for: {self.prompt}"}], + "messages": [ + { + "role": "user", + "content": f"{self.task} for: {self.prompt}", + } + ], "temperature": 0.7, "max_tokens": 1024, - "stream": False + "stream": False, }, - timeout=30.0 + timeout=30.0, ) response.raise_for_status() result = response.json() response_text = result["choices"][0]["message"]["content"].strip() - + # Yield the result with metadata for stream processing yield ProcessorPart(response_text, metadata={"type": self.metadata_type}) except Exception as e: - yield ProcessorPart(f"Error during {self.task}: {str(e)}", metadata={"type": self.metadata_type, "error": True}) + yield ProcessorPart( + f"Error during {self.task}: {str(e)}", + metadata={"type": self.metadata_type, "error": True}, + ) + # === Specialized Processors === class BrandNameProcessor(ASI1MiniProcessor): """Processor for generating brand names""" + def __init__(self, prompt: str): super().__init__(prompt, "Generate a creative brand name", "brand_name") + class TaglineProcessor(ASI1MiniProcessor): """Processor for generating taglines""" + def __init__(self, prompt: str): super().__init__(prompt, "Write a catchy tagline", "tagline") + class WebLayoutProcessor(ASI1MiniProcessor): """Processor for generating HTML layouts""" + def __init__(self, prompt: str): - super().__init__(prompt, "Design a minimal HTML layout for a landing page", "web_layout") + super().__init__( + prompt, "Design a minimal HTML layout for a landing page", "web_layout" + ) + class LogoImageProcessor(processor.Processor): """ Processor for generating and uploading logo images. Demonstrates multimodal processing (image generation + storage). """ + def __init__(self, prompt: str): self.prompt = prompt self.asset_id = None @@ -130,9 +148,9 @@ async def call(self, content: AsyncIterable[ProcessorPart]): n=1, size="1024x1024", quality="standard", - response_format="url" + response_format="url", ) - + if response.data and response.data[0].url: # Download and upload to external storage image_url = response.data[0].url @@ -140,21 +158,34 @@ async def call(self, content: AsyncIterable[ProcessorPart]): if response.status_code == 200: image_data = response.content mime_type = response.headers.get("Content-Type", "image/png") - + # Upload to external storage self.asset_id = external_storage.create_asset( name=f"logo_{uuid.uuid4().hex}", content=image_data, - mime_type=mime_type + mime_type=mime_type, ) self.asset_uri = f"agent-storage://{external_storage.storage_url}/{self.asset_id}" - yield ProcessorPart(f"Logo uploaded: {self.asset_id}", metadata={"type": "logo_image"}) + yield ProcessorPart( + f"Logo uploaded: {self.asset_id}", + metadata={"type": "logo_image"}, + ) else: - yield ProcessorPart("Failed to download image", metadata={"type": "logo_image", "error": True}) + yield ProcessorPart( + "Failed to download image", + metadata={"type": "logo_image", "error": True}, + ) else: - yield ProcessorPart("No image URL received", metadata={"type": "logo_image", "error": True}) + yield ProcessorPart( + "No image URL received", + metadata={"type": "logo_image", "error": True}, + ) except Exception as e: - yield ProcessorPart(f"Logo generation failed: {str(e)}", metadata={"type": "logo_image", "error": True}) + yield ProcessorPart( + f"Logo generation failed: {str(e)}", + metadata={"type": "logo_image", "error": True}, + ) + # === Synthesis Processor === class SynthesisProcessor: @@ -162,6 +193,7 @@ class SynthesisProcessor: Standalone processor for synthesizing parallel results. Demonstrates how to combine multiple processor outputs. """ + async def synthesize_results(self, collected_results: dict) -> str: """Synthesize all collected results into a final output""" try: @@ -188,38 +220,40 @@ async def synthesize_results(self, collected_results: dict) -> str: WEB LAYOUT: [complete HTML] """ - + async with httpx.AsyncClient() as client: response = await client.post( ASI1_BASE_URL, headers={ "Authorization": f"Bearer {ASI1_API_KEY}", - "Content-Type": "application/json" + "Content-Type": "application/json", }, json={ "model": "asi1-mini", "messages": [{"role": "user", "content": summary_prompt}], "temperature": 0.3, "max_tokens": 2048, - "stream": False + "stream": False, }, - timeout=45.0 + timeout=45.0, ) response.raise_for_status() result = response.json() return result["choices"][0]["message"]["content"].strip() - + except Exception as e: return f"Error generating final output: {e}" + # === Chat Protocol Handler === chat_proto = Protocol(spec=chat_protocol_spec) + @chat_proto.on_message(ChatMessage) async def handle_message(ctx: Context, sender: str, msg: ChatMessage): """ Main message handler demonstrating parallel processing. - + This is where the magic happens: 1. Creates multiple processors 2. Runs them in parallel using processor.parallel_concat() @@ -227,33 +261,40 @@ async def handle_message(ctx: Context, sender: str, msg: ChatMessage): 4. Synthesizes final output """ # Acknowledge message receipt - await ctx.send(sender, ChatAcknowledgement(timestamp=datetime.utcnow(), acknowledged_msg_id=msg.msg_id)) + await ctx.send( + sender, + ChatAcknowledgement( + timestamp=datetime.utcnow(), acknowledged_msg_id=msg.msg_id + ), + ) for item in msg.content: if isinstance(item, TextContent): prompt = item.text ctx.logger.info(f"Processing: {prompt}") - + print(f"🔄 Starting parallel execution for: '{prompt}'") - + # === PARALLEL PROCESSING SETUP === # Create processors for each task logo_processor = LogoImageProcessor(prompt) - + # This is the key line - runs all processors in parallel - mm_agent = processor.parallel_concat([ - BrandNameProcessor(prompt), # Task 1: Brand name - TaglineProcessor(prompt), # Task 2: Tagline - logo_processor, # Task 3: Logo image - WebLayoutProcessor(prompt) # Task 4: Web layout - ]) + mm_agent = processor.parallel_concat( + [ + BrandNameProcessor(prompt), # Task 1: Brand name + TaglineProcessor(prompt), # Task 2: Tagline + logo_processor, # Task 3: Logo image + WebLayoutProcessor(prompt), # Task 4: Web layout + ] + ) # === STREAM PROCESSING === # Create input stream to trigger all processors input_stream = streams.stream_content([ProcessorPart("start")]) collected = {} total_tasks = 4 - + # Process results as they arrive (not necessarily in order) async for part in mm_agent(input_stream): if part.metadata and not part.metadata.get("error"): @@ -271,46 +312,59 @@ async def handle_message(ctx: Context, sender: str, msg: ChatMessage): reply = await synthesis_processor.synthesize_results(collected) # Send text response - await ctx.send(sender, ChatMessage( - timestamp=datetime.utcnow(), - msg_id=uuid.uuid4(), - content=[TextContent(type="text", text=reply)] - )) + await ctx.send( + sender, + ChatMessage( + timestamp=datetime.utcnow(), + msg_id=uuid.uuid4(), + content=[TextContent(type="text", text=reply)], + ), + ) # Send logo image as resource if logo_processor.asset_id and logo_processor.asset_uri: try: - external_storage.set_permissions(asset_id=logo_processor.asset_id, agent_address=sender) + external_storage.set_permissions( + asset_id=logo_processor.asset_id, agent_address=sender + ) print(f"📎 Logo uploaded: {logo_processor.asset_id}") - - await ctx.send(sender, ChatMessage( - timestamp=datetime.utcnow(), - msg_id=uuid.uuid4(), - content=[ - ResourceContent( - type="resource", - resource_id=UUID4(logo_processor.asset_id), - resource=Resource( - uri=logo_processor.asset_uri, - metadata={"mime_type": "image/png", "role": "generated-image"} + + await ctx.send( + sender, + ChatMessage( + timestamp=datetime.utcnow(), + msg_id=uuid.uuid4(), + content=[ + ResourceContent( + type="resource", + resource_id=UUID4(logo_processor.asset_id), + resource=Resource( + uri=logo_processor.asset_uri, + metadata={ + "mime_type": "image/png", + "role": "generated-image", + }, + ), ) - ) - ] - )) + ], + ), + ) except Exception as e: ctx.logger.error(f"Error sending logo: {e}") + @chat_proto.on_message(ChatAcknowledgement) async def handle_ack(ctx: Context, sender: str, msg: ChatAcknowledgement): """Handle acknowledgments from other agents""" ctx.logger.info(f"Received ack from {sender} for {msg.acknowledged_msg_id}") + # === Agent Setup === agent = Agent( name="brand_management_agent", seed="brand_management_parallel_processing_demo", port=8002, - mailbox=True # Enable Agentverse integration + mailbox=True, # Enable Agentverse integration ) # Include the chat protocol @@ -318,5 +372,7 @@ async def handle_ack(ctx: Context, sender: str, msg: ChatAcknowledgement): if __name__ == "__main__": print("🚀 Starting Brand Management Agent with Parallel Processing...") - print("💡 Send a message like 'build a around sustainable clothing' to see parallel processing in action!") - agent.run() \ No newline at end of file + print( + "💡 Send a message like 'build a around sustainable clothing' to see parallel processing in action!" + ) + agent.run() diff --git a/image-agent-payment-protocol/agent.py b/image-agent-payment-protocol/agent.py index 3b389cc6..c46ae677 100644 --- a/image-agent-payment-protocol/agent.py +++ b/image-agent-payment-protocol/agent.py @@ -6,8 +6,8 @@ # Load .env BEFORE importing modules that read env at import time dotenv.load_dotenv() -from chat_proto import chat_proto -from payment_proto import payment_proto, set_agent_wallet +from chat_proto import chat_proto # noqa: E402 +from payment_proto import payment_proto, set_agent_wallet # noqa: E402 agent = Agent( @@ -20,6 +20,7 @@ # Set the agent wallet for payment operations set_agent_wallet(agent.wallet) + @agent.on_event("startup") async def startup(ctx: Context): ctx.logger.info(f"ASI1 Image Agent started: {agent.wallet.address()}") @@ -27,7 +28,11 @@ async def startup(ctx: Context): skyfire_configured = bool( (os.getenv("SKYFIRE_API_KEY") or os.getenv("SELLER_SKYFIRE_API_KEY")) and (os.getenv("SKYFIRE_SERVICE_ID") or os.getenv("SELLER_SERVICE_ID")) - and (os.getenv("SELLER_ACCOUNT_ID") or os.getenv("JWT_AUDIENCE") or os.getenv("SKYFIRE_ACCOUNT_ID")) + and ( + os.getenv("SELLER_ACCOUNT_ID") + or os.getenv("JWT_AUDIENCE") + or os.getenv("SKYFIRE_ACCOUNT_ID") + ) ) ctx.logger.info("=== ASI1 Image Generation Agent ===") @@ -35,7 +40,9 @@ async def startup(ctx: Context): if skyfire_configured: ctx.logger.info(" • $0.001 USDC (via Skyfire)") else: - ctx.logger.info(" • Skyfire USDC: Not configured (set SKYFIRE_API_KEY and SKYFIRE_SERVICE_ID)") + ctx.logger.info( + " • Skyfire USDC: Not configured (set SKYFIRE_API_KEY and SKYFIRE_SERVICE_ID)" + ) ctx.logger.info("🎨 Ready to generate images with ASI1!") @@ -45,5 +52,3 @@ async def startup(ctx: Context): if __name__ == "__main__": agent.run() - - diff --git a/image-agent-payment-protocol/chat_proto.py b/image-agent-payment-protocol/chat_proto.py index 6fe046cd..72b47321 100644 --- a/image-agent-payment-protocol/chat_proto.py +++ b/image-agent-payment-protocol/chat_proto.py @@ -48,7 +48,9 @@ async def handle_message(ctx: Context, sender: str, msg: ChatMessage): if (ctx.storage.has(awaiting_key) or ctx.storage.get(awaiting_key)) and ( ctx.storage.has(verified_key) or ctx.storage.get(verified_key) ): - ctx.logger.info("Consuming prompt post-payment and generating one image") + ctx.logger.info( + "Consuming prompt post-payment and generating one image" + ) ctx.storage.remove(awaiting_key) ctx.storage.remove(verified_key) ctx.storage.set(f"prompt:{sender}:{session_id}", text) @@ -57,11 +59,21 @@ async def handle_message(ctx: Context, sender: str, msg: ChatMessage): return if text.lower().startswith(("hello", "hi", "hey")) and len(text) < 20: - await ctx.send(sender, create_text_chat("Hello! Please complete a small payment first, then I'll ask for your image prompt.")) + await ctx.send( + sender, + create_text_chat( + "Hello! Please complete a small payment first, then I'll ask for your image prompt." + ), + ) await request_payment_from_user(ctx, sender) return - await ctx.send(sender, create_text_chat("Please complete the payment first. After that, I'll ask for your prompt and generate one image.")) + await ctx.send( + sender, + create_text_chat( + "Please complete the payment first. After that, I'll ask for your prompt and generate one image." + ), + ) await request_payment_from_user(ctx, sender) @@ -70,5 +82,3 @@ async def handle_ack(ctx: Context, sender: str, msg: ChatAcknowledgement): ctx.logger.info( f"Got an acknowledgement from {sender} for {msg.acknowledged_msg_id}" ) - - diff --git a/image-agent-payment-protocol/payment_proto.py b/image-agent-payment-protocol/payment_proto.py index 0423e128..2301bfb9 100644 --- a/image-agent-payment-protocol/payment_proto.py +++ b/image-agent-payment-protocol/payment_proto.py @@ -6,7 +6,6 @@ RequestPayment, RejectPayment, CommitPayment, - CancelPayment, CompletePayment, payment_protocol_spec, ) @@ -61,7 +60,9 @@ async def handle_commit_payment(ctx: Context, sender: str, msg: CommitPayment): if msg.funds.payment_method == "skyfire" and msg.funds.currency == "USDC": try: - payment_verified = await verify_and_charge(msg.transaction_id, "0.001", ctx.logger) + payment_verified = await verify_and_charge( + msg.transaction_id, "0.001", ctx.logger + ) except Exception as e: ctx.logger.error(f"Skyfire verify/charge error: {e}") payment_verified = False @@ -75,7 +76,12 @@ async def handle_commit_payment(ctx: Context, sender: str, msg: CommitPayment): ctx.storage.set(f"{sender}:{session_id}:awaiting_prompt", True) ctx.storage.set(f"{sender}:{session_id}:verified_payment", True) await ctx.send(sender, CompletePayment(transaction_id=msg.transaction_id)) - await ctx.send(sender, create_text_chat("Please send your image prompt (one image will be generated).")) + await ctx.send( + sender, + create_text_chat( + "Please send your image prompt (one image will be generated)." + ), + ) else: ctx.logger.error(f"Payment verification failed from {sender}") await ctx.send(sender, RejectPayment(reason="Payment verification failed")) @@ -85,7 +91,9 @@ async def generate_image_after_payment(ctx: Context, user_address: str): from chat_proto import create_text_chat session_id = str(ctx.session) - prompt = ctx.storage.get(f"prompt:{user_address}:{session_id}") or ctx.storage.get("current_prompt") + prompt = ctx.storage.get(f"prompt:{user_address}:{session_id}") or ctx.storage.get( + "current_prompt" + ) if not prompt: ctx.logger.error("No prompt found in storage") await ctx.send(user_address, create_text_chat("Error: No prompt found")) @@ -102,6 +110,7 @@ def _sanitize_prompt(raw: str) -> str: p = p.split("")[0] # Remove any XML/HTML-like tags import re as _re + p = _re.sub(r"<[^>]+>", " ", p) # Collapse whitespace and trim p = " ".join(p.split()) @@ -117,10 +126,15 @@ def _sanitize_prompt(raw: str) -> str: try: import requests as _r + pollinations_url = f"https://image.pollinations.ai/prompt/{quote(clean_prompt)}?width=512&height=512" resp = _r.get(pollinations_url, timeout=90) ctype = resp.headers.get("Content-Type", "") - if resp.status_code != 200 or not resp.content or not ctype.startswith("image/"): + if ( + resp.status_code != 200 + or not resp.content + or not ctype.startswith("image/") + ): await ctx.send(user_address, create_text_chat("Image generation failed")) return @@ -133,7 +147,12 @@ def _sanitize_prompt(raw: str) -> str: storage_url = f"{base_url}/v1/storage" if not api_key: - await ctx.send(user_address, create_text_chat("Storage not configured. Please set AGENTVERSE_API_KEY to deliver the image.")) + await ctx.send( + user_address, + create_text_chat( + "Storage not configured. Please set AGENTVERSE_API_KEY to deliver the image." + ), + ) return from uagents_core.storage import ExternalStorage @@ -143,21 +162,32 @@ def _sanitize_prompt(raw: str) -> str: from uagents_core.contrib.protocols.chat import ChatMessage as AvChatMessage storage = ExternalStorage(api_token=api_key, storage_url=storage_url) - asset_id = storage.create_asset(name=str(ctx.session), content=image_bytes, mime_type=mime_type) + asset_id = storage.create_asset( + name=str(ctx.session), content=image_bytes, mime_type=mime_type + ) storage.set_permissions(asset_id=asset_id, agent_address=user_address) asset_uri = f"agent-storage://{storage.storage_url}/{asset_id}" - await ctx.send(user_address, AvChatMessage( - timestamp=datetime.now(timezone.utc), - msg_id=uuid4(), - content=[ - ResourceContent( - type="resource", - resource_id=asset_id, - resource=Resource(uri=asset_uri, metadata={"mime_type": mime_type, "role": "generated-image"}), - ) - ], - )) + await ctx.send( + user_address, + AvChatMessage( + timestamp=datetime.now(timezone.utc), + msg_id=uuid4(), + content=[ + ResourceContent( + type="resource", + resource_id=asset_id, + resource=Resource( + uri=asset_uri, + metadata={ + "mime_type": mime_type, + "role": "generated-image", + }, + ), + ) + ], + ), + ) except Exception as e: ctx.logger.error(f"Image generation error: {e}") await ctx.send(user_address, create_text_chat(f"Error generating image: {e}")) @@ -172,4 +202,3 @@ async def handle_reject_payment(ctx: Context, sender: str, msg: RejectPayment): "You rejected the payment. If you'd like to continue, reply and I'll send a new payment request." ), ) - diff --git a/image-agent-payment-protocol/skyfire.py b/image-agent-payment-protocol/skyfire.py index 4b925aeb..608c1f0a 100644 --- a/image-agent-payment-protocol/skyfire.py +++ b/image-agent-payment-protocol/skyfire.py @@ -14,11 +14,17 @@ # Normalize issuer to avoid trailing-slash mismatches JWT_ISSUER = (os.getenv("JWT_ISSUER") or APP_BASE).rstrip("/") # Prefer explicit audience, then seller account id, then legacy account id -JWT_AUDIENCE = os.getenv("JWT_AUDIENCE") or os.getenv("SELLER_ACCOUNT_ID") or os.getenv("SKYFIRE_ACCOUNT_ID", "") +JWT_AUDIENCE = ( + os.getenv("JWT_AUDIENCE") + or os.getenv("SELLER_ACCOUNT_ID") + or os.getenv("SKYFIRE_ACCOUNT_ID", "") +) JWT_ALGORITHM = "ES256" # Charge API (provider flow) -SKYFIRE_TOKENS_API_URL = os.getenv("SKYFIRE_TOKENS_API_URL", f"{API_BASE}/api/v1/tokens/charge") +SKYFIRE_TOKENS_API_URL = os.getenv( + "SKYFIRE_TOKENS_API_URL", f"{API_BASE}/api/v1/tokens/charge" +) # Prefer SELLER_* variables if provided; fallback to legacy names SKYFIRE_API_KEY = os.getenv("SKYFIRE_API_KEY") or os.getenv("SELLER_SKYFIRE_API_KEY") @@ -61,7 +67,9 @@ async def verify_token_claims(skyfire_token: str, logger: logging.Logger) -> boo ) ssi = claims.get("ssi") if SKYFIRE_SERVICE_ID and ssi != SKYFIRE_SERVICE_ID: - raise JWTError(f"Token is not issued for this service: {SKYFIRE_SERVICE_ID}") + raise JWTError( + f"Token is not issued for this service: {SKYFIRE_SERVICE_ID}" + ) return True except JWTError as err: logger.error(f"Skyfire token JWT verification failed: {err}") @@ -71,7 +79,9 @@ async def verify_token_claims(skyfire_token: str, logger: logging.Logger) -> boo return False -async def charge_token(token: str, amount_to_charge: str, logger: logging.Logger) -> bool: +async def charge_token( + token: str, amount_to_charge: str, logger: logging.Logger +) -> bool: try: async with aiohttp.ClientSession() as session: payload = {"token": token, "chargeAmount": amount_to_charge} @@ -94,8 +104,12 @@ async def charge_token(token: str, amount_to_charge: str, logger: logging.Logger return False -async def verify_and_charge(token: str, amount_usdc: str, logger: logging.Logger) -> bool: - if not (SKYFIRE_API_KEY and SKYFIRE_SERVICE_ID and (SELLER_ACCOUNT_ID or JWT_AUDIENCE)): +async def verify_and_charge( + token: str, amount_usdc: str, logger: logging.Logger +) -> bool: + if not ( + SKYFIRE_API_KEY and SKYFIRE_SERVICE_ID and (SELLER_ACCOUNT_ID or JWT_AUDIENCE) + ): logger.error("Skyfire seller variables not configured") return False ok = await verify_token_claims(token, logger) @@ -106,5 +120,3 @@ async def verify_and_charge(token: str, amount_usdc: str, logger: logging.Logger def get_skyfire_service_id() -> Optional[str]: return SKYFIRE_SERVICE_ID - - diff --git a/launch-your-a2a-agent/launch_your_a2a_agent.py b/launch-your-a2a-agent/launch_your_a2a_agent.py index b6151158..55310bd2 100644 --- a/launch-your-a2a-agent/launch_your_a2a_agent.py +++ b/launch-your-a2a-agent/launch_your_a2a_agent.py @@ -10,14 +10,14 @@ if AGENTVERSE_A2A_URI: agentverse_sdk.init(AGENTVERSE_A2A_URI) -import uvicorn -from a2a.server.agent_execution import AgentExecutor, RequestContext -from a2a.server.events import EventQueue -from a2a.server.request_handlers.default_request_handler import DefaultRequestHandler -from a2a.server.tasks.inmemory_task_store import InMemoryTaskStore -from a2a.server.apps import A2AStarletteApplication -from a2a.types import AgentCapabilities, AgentCard, AgentSkill -from a2a.utils import new_agent_text_message +import uvicorn # noqa: E402 +from a2a.server.agent_execution import AgentExecutor, RequestContext # noqa: E402 +from a2a.server.events import EventQueue # noqa: E402 +from a2a.server.request_handlers.default_request_handler import DefaultRequestHandler # noqa: E402 +from a2a.server.tasks.inmemory_task_store import InMemoryTaskStore # noqa: E402 +from a2a.server.apps import A2AStarletteApplication # noqa: E402 +from a2a.types import AgentCapabilities, AgentCard, AgentSkill # noqa: E402 +from a2a.utils import new_agent_text_message # noqa: E402 HOST = os.getenv("HOST", "0.0.0.0") diff --git a/launch-your-a2a-research-team/common.py b/launch-your-a2a-research-team/common.py index 5e37cef2..25e6e696 100644 --- a/launch-your-a2a-research-team/common.py +++ b/launch-your-a2a-research-team/common.py @@ -133,7 +133,9 @@ async def call_remote_agent(agent_url: str, prompt: str) -> str: async with httpx.AsyncClient(timeout=90) as httpx_client: resolver = A2ACardResolver(httpx_client=httpx_client, base_url=agent_url) agent_card = await resolver.get_agent_card() - client = A2AClient(httpx_client=httpx_client, agent_card=agent_card, url=agent_url) + client = A2AClient( + httpx_client=httpx_client, agent_card=agent_card, url=agent_url + ) payload = { "message": { diff --git a/launch-your-a2a-research-team/orchestrator_agent.py b/launch-your-a2a-research-team/orchestrator_agent.py index d2031515..07e03f03 100644 --- a/launch-your-a2a-research-team/orchestrator_agent.py +++ b/launch-your-a2a-research-team/orchestrator_agent.py @@ -68,7 +68,8 @@ def _extract_text_from_response(response) -> str: return text if result.artifacts: artifact_texts = [ - _extract_text_from_parts(artifact.parts) for artifact in result.artifacts + _extract_text_from_parts(artifact.parts) + for artifact in result.artifacts ] final_text = "\n\n".join(text for text in artifact_texts if text) if final_text: @@ -81,7 +82,9 @@ async def call_remote_agent(agent_url: str, prompt: str) -> str: async with httpx.AsyncClient(timeout=90) as httpx_client: resolver = A2ACardResolver(httpx_client=httpx_client, base_url=agent_url) agent_card = await resolver.get_agent_card() - client = A2AClient(httpx_client=httpx_client, agent_card=agent_card, url=agent_url) + client = A2AClient( + httpx_client=httpx_client, agent_card=agent_card, url=agent_url + ) payload = { "message": { diff --git a/llama-index/handlers.py b/llama-index/handlers.py index 9c820a6b..6af5f924 100644 --- a/llama-index/handlers.py +++ b/llama-index/handlers.py @@ -33,18 +33,22 @@ # ── Reply helper ────────────────────────────────────────────────────── + async def reply(ctx: Context, sender: str, text: str, *, end: bool = False): content = [TextContent(type="text", text=text)] if end: content.append(EndSessionContent(type="end-session")) await ctx.send( sender, - ChatMessage(timestamp=datetime.now(timezone.utc), msg_id=uuid4(), content=content), + ChatMessage( + timestamp=datetime.now(timezone.utc), msg_id=uuid4(), content=content + ), ) # ── Ingestion flow (post-payment) ──────────────────────────────────── + async def run_pending_ingestion(ctx: Context, sender: str): pending_text = ctx.storage.get(f"pending_url:{sender}") if not pending_text: @@ -54,17 +58,25 @@ async def run_pending_ingestion(ctx: Context, sender: str): match = URL_PATTERN.search(pending_text) if not match: ctx.storage.remove(f"pending_url:{sender}") - await reply(ctx, sender, "Could not find a valid URL in your message. Please resend.") + await reply( + ctx, sender, "Could not find a valid URL in your message. Please resend." + ) return url = match.group(0) collection = _collection_name(sender) try: - await reply(ctx, sender, f"Payment verified! Downloading and ingesting your document...\n\nURL: {url}") + await reply( + ctx, + sender, + f"Payment verified! Downloading and ingesting your document...\n\nURL: {url}", + ) file_path = await asyncio.to_thread(download_document, url) - num_chunks = await asyncio.to_thread(ingest_document, file_path, collection, cleanup=True) + num_chunks = await asyncio.to_thread( + ingest_document, file_path, collection, cleanup=True + ) # Only remove pending URL after successful ingestion ctx.storage.remove(f"pending_url:{sender}") @@ -72,7 +84,8 @@ async def run_pending_ingestion(ctx: Context, sender: str): reset_user_session(sender) await reply( - ctx, sender, + ctx, + sender, f"Document ingested successfully!\n\n" f"Chunks stored: {num_chunks}\n" f"Collection: {collection}\n\n" @@ -85,7 +98,8 @@ async def run_pending_ingestion(ctx: Context, sender: str): ctx.logger.error(f"Ingestion failed for {sender[:20]}...: {e}") traceback.print_exc() await reply( - ctx, sender, + ctx, + sender, f"Document ingestion failed: {e}\n\n" f"Your payment has been recorded. Send the same URL again to retry without paying.", ) @@ -93,19 +107,25 @@ async def run_pending_ingestion(ctx: Context, sender: str): # ── Payment protocol handlers ──────────────────────────────────────── + @payment_proto.on_message(CommitPayment) async def handle_commit_payment(ctx: Context, sender: str, msg: CommitPayment): ctx.logger.info(f"CommitPayment from {sender[:20]}... tx={msg.transaction_id}") if msg.funds.payment_method != "stripe" or not msg.transaction_id: - await ctx.send(sender, RejectPayment(reason="Unsupported payment method (expected stripe).")) + await ctx.send( + sender, + RejectPayment(reason="Unsupported payment method (expected stripe)."), + ) return # Verify the transaction_id matches the checkout session we created for this user pending = ctx.storage.get(f"pending_stripe:{sender}") if not pending or pending.get("checkout_session_id") != msg.transaction_id: ctx.logger.warning(f"Unknown or mismatched Stripe session for {sender[:20]}...") - await ctx.send(sender, RejectPayment(reason="No matching payment session found.")) + await ctx.send( + sender, RejectPayment(reason="No matching payment session found.") + ) return paid = await asyncio.to_thread(verify_checkout_session_paid, msg.transaction_id) @@ -119,28 +139,43 @@ async def handle_commit_payment(ctx: Context, sender: str, msg: CommitPayment): ctx.logger.warning(f"Stripe payment NOT completed for {sender[:20]}...") await ctx.send( sender, - RejectPayment(reason="Stripe payment not completed yet. Please finish checkout."), + RejectPayment( + reason="Stripe payment not completed yet. Please finish checkout." + ), + ) + await reply( + ctx, + sender, + "Payment not completed. Please finish the Stripe checkout and try again.", ) - await reply(ctx, sender, "Payment not completed. Please finish the Stripe checkout and try again.") @payment_proto.on_message(RejectPayment) async def handle_reject_payment(ctx: Context, sender: str, msg: RejectPayment): ctx.logger.info(f"Payment rejected by {sender[:20]}...") ctx.storage.remove(f"pending_url:{sender}") - await reply(ctx, sender, "Payment declined. Send another document URL whenever you're ready.") + await reply( + ctx, + sender, + "Payment declined. Send another document URL whenever you're ready.", + ) # ── Chat protocol handlers ─────────────────────────────────────────── + @chat_proto.on_message(ChatMessage) async def handle_message(ctx: Context, sender: str, msg: ChatMessage): await ctx.send( sender, - ChatAcknowledgement(timestamp=datetime.now(timezone.utc), acknowledged_msg_id=msg.msg_id), + ChatAcknowledgement( + timestamp=datetime.now(timezone.utc), acknowledged_msg_id=msg.msg_id + ), ) - text = "".join(item.text for item in msg.content if isinstance(item, TextContent)).strip() + text = "".join( + item.text for item in msg.content if isinstance(item, TextContent) + ).strip() if not text: return @@ -150,7 +185,8 @@ async def handle_message(ctx: Context, sender: str, msg: ChatMessage): if URL_PATTERN.search(text): ctx.storage.set(f"pending_url:{sender}", text) await request_payment_from_user( - ctx, sender, + ctx, + sender, description=f"RAG document ingestion — {format_price()}", ) return @@ -159,7 +195,8 @@ async def handle_message(ctx: Context, sender: str, msg: ChatMessage): collection = _collection_name(sender) if not collection_has_points(collection): await reply( - ctx, sender, + ctx, + sender, "No documents ingested yet. Send a document URL first " "(e.g., a PDF link) and I'll ingest it after payment.", ) diff --git a/llama-index/payment.py b/llama-index/payment.py index f8cfb558..06c4022c 100644 --- a/llama-index/payment.py +++ b/llama-index/payment.py @@ -4,14 +4,16 @@ from uagents import Context from uagents_core.contrib.protocols.payment import Funds, RequestPayment -from config import STRIPE_AMOUNT_CENTS, STRIPE_CURRENCY +from config import STRIPE_AMOUNT_CENTS from stripe_payments import ( create_embedded_checkout_session, format_price, ) -async def request_payment_from_user(ctx: Context, user_address: str, description: str | None = None): +async def request_payment_from_user( + ctx: Context, user_address: str, description: str | None = None +): """Create a Stripe embedded checkout and send RequestPayment to the user.""" desc = description or f"RAG document ingestion — {format_price()}" @@ -27,11 +29,13 @@ async def request_payment_from_user(ctx: Context, user_address: str, description ctx.storage.set(f"pending_stripe:{user_address}", checkout) payment_request = RequestPayment( - accepted_funds=[Funds( - currency="USD", - amount=f"{STRIPE_AMOUNT_CENTS / 100:.2f}", - payment_method="stripe", - )], + accepted_funds=[ + Funds( + currency="USD", + amount=f"{STRIPE_AMOUNT_CENTS / 100:.2f}", + payment_method="stripe", + ) + ], recipient=str(ctx.agent.address), deadline_seconds=300, reference=checkout["checkout_session_id"], diff --git a/llama-index/rag.py b/llama-index/rag.py index 24c41595..b1992a0a 100644 --- a/llama-index/rag.py +++ b/llama-index/rag.py @@ -19,8 +19,14 @@ from llama_index.vector_stores.qdrant import QdrantVectorStore from config import ( - qclient, async_qclient, embed_model, llm, - CHUNK_SIZE, CHUNK_OVERLAP, SIMILARITY_TOP_K, CHAT_MEMORY_TOKEN_LIMIT, + qclient, + async_qclient, + embed_model, + llm, + CHUNK_SIZE, + CHUNK_OVERLAP, + SIMILARITY_TOP_K, + CHAT_MEMORY_TOKEN_LIMIT, ) URL_PATTERN = re.compile(r"https?://[^\s<>\"']+", re.IGNORECASE) @@ -37,6 +43,7 @@ def _collection_name(sender: str) -> str: # ── SSRF protection ─────────────────────────────────────────────────── + def _validate_url(url: str) -> str: """Validate URL scheme and resolve hostname to reject private/loopback IPs.""" parsed = urlparse(url) @@ -49,7 +56,9 @@ def _validate_url(url: str) -> str: # Resolve hostname and check all resulting IPs try: - addr_infos = socket.getaddrinfo(hostname, parsed.port or 443, proto=socket.IPPROTO_TCP) + addr_infos = socket.getaddrinfo( + hostname, parsed.port or 443, proto=socket.IPPROTO_TCP + ) except socket.gaierror as e: raise ValueError(f"Cannot resolve hostname '{hostname}': {e}") from e @@ -71,6 +80,7 @@ def _sanitize_filename(fname: str) -> str: # ── Document download ───────────────────────────────────────────────── + def download_document(url: str) -> str: """Download a document URL to a temp directory. Returns the local file path.""" tmp_dir = tempfile.mkdtemp(prefix="rag_") @@ -125,6 +135,7 @@ def download_document(url: str) -> str: # ── Ingestion ───────────────────────────────────────────────────────── + def _is_collection_not_found(exc: Exception) -> bool: """Check if an exception indicates a missing Qdrant collection.""" if isinstance(exc, UnexpectedResponse) and exc.status_code == 404: @@ -145,7 +156,9 @@ def clear_collection(collection_name: str): raise -def ingest_document(file_path: str, collection_name: str, *, cleanup: bool = False) -> int: +def ingest_document( + file_path: str, collection_name: str, *, cleanup: bool = False +) -> int: """Ingest a local file into a Qdrant collection. Wipes old data first. Returns chunk count.""" clear_collection(collection_name) @@ -177,6 +190,7 @@ def ingest_document(file_path: str, collection_name: str, *, cleanup: bool = Fal # ── Agentic RAG (ReAct agent) ──────────────────────────────────────── + def _build_agent_for_sender(sender: str) -> tuple[ReActAgent, ChatMemoryBuffer]: """Build a fresh ReAct agent backed by the user's Qdrant collection.""" collection = _collection_name(sender) @@ -245,7 +259,11 @@ async def query_agent(sender: str, query: str) -> tuple[str, list[dict]]: tool_outputs.append(event.tool_output) result = await handler - answer = result.response.content if hasattr(result.response, "content") else str(result.response) + answer = ( + result.response.content + if hasattr(result.response, "content") + else str(result.response) + ) # Extract source nodes from query engine tool outputs citations = [] @@ -253,12 +271,14 @@ async def query_agent(sender: str, query: str) -> tuple[str, list[dict]]: raw = tool_output.raw_output source_nodes = getattr(raw, "source_nodes", None) or [] for node in source_nodes: - citations.append({ - "page": node.node.metadata.get("page_label", "N/A"), - "file": node.node.metadata.get("file_name", "unknown"), - "score": node.score if node.score is not None else 0.0, - "excerpt": node.node.get_content().strip().replace("\n", " ")[:300], - }) + citations.append( + { + "page": node.node.metadata.get("page_label", "N/A"), + "file": node.node.metadata.get("file_name", "unknown"), + "score": node.score if node.score is not None else 0.0, + "excerpt": node.node.get_content().strip().replace("\n", " ")[:300], + } + ) return answer, citations @@ -308,7 +328,9 @@ def format_response_with_citations(answer: str, citations: list[dict]) -> str: ] for i, c in enumerate(shown, 1): - score_pct = int(c["score"] * 100) if isinstance(c["score"], float) else int(c["score"]) + score_pct = ( + int(c["score"] * 100) if isinstance(c["score"], float) else int(c["score"]) + ) badge = _confidence_badge(score_pct) excerpt = _clean_excerpt(c["excerpt"]) diff --git a/llama-index/stripe_payments.py b/llama-index/stripe_payments.py index 0a4e71f0..cd348601 100644 --- a/llama-index/stripe_payments.py +++ b/llama-index/stripe_payments.py @@ -20,6 +20,7 @@ def _get_stripe_sdk(): import stripe + stripe.api_key = STRIPE_SECRET_KEY return stripe diff --git a/mcp-agents/Context7-MCP-Agent/agent.py b/mcp-agents/Context7-MCP-Agent/agent.py index 52c8d03f..addeb1e5 100644 --- a/mcp-agents/Context7-MCP-Agent/agent.py +++ b/mcp-agents/Context7-MCP-Agent/agent.py @@ -8,19 +8,16 @@ import os import asyncio import time -from typing import Dict, Any, Optional, List, Tuple +from typing import Dict, Any, List from contextlib import AsyncExitStack import mcp from mcp.client.stdio import stdio_client -import anthropic -from uagents import Agent, Context, Protocol, Model +from uagents import Agent, Context, Protocol from uagents_core.contrib.protocols.chat import ( chat_protocol_spec, ChatMessage, ChatAcknowledgement, TextContent, - EndSessionContent, - StartSessionContent, ) from datetime import datetime, timezone from uuid import uuid4 @@ -48,19 +45,20 @@ # --- Enhanced MCP Client Logic --- + class Context7MCPClient: """ Enhanced Context7 MCP Client with iterative search refinement. Automatically tries multiple search strategies to find the best library match. """ - + def __init__(self, ctx: Context): self._ctx = ctx self._session: mcp.ClientSession = None self._exit_stack = AsyncExitStack() self.anthropic = Anthropic(api_key=ANTHROPIC_API_KEY) self.tools = [] # Will be populated after connection - + # Search refinement strategies self.search_strategies = [ self._strategy_exact_match, @@ -69,40 +67,41 @@ def __init__(self, ctx: Context): self._strategy_try_popular_alternatives, self._strategy_extract_keywords, ] - + async def connect(self): """Connect to Context7 MCP server via local npx execution""" try: self._ctx.logger.info("Connecting to Context7 MCP server via npx...") - + # Use npx to run Context7 MCP server locally params = mcp.StdioServerParameters( - command="npx", - args=["-y", "@upstash/context7-mcp"] + command="npx", args=["-y", "@upstash/context7-mcp"] ) - + read_stream, write_stream = await self._exit_stack.enter_async_context( stdio_client(params) ) - + self._session = await self._exit_stack.enter_async_context( mcp.ClientSession(read_stream, write_stream) ) - + await self._session.initialize() - + # List available tools list_tools_result = await self._session.list_tools() self.tools = list_tools_result.tools - - self._ctx.logger.info(f"Connected to Context7 MCP server with {len(self.tools)} tools") + + self._ctx.logger.info( + f"Connected to Context7 MCP server with {len(self.tools)} tools" + ) for tool in self.tools: self._ctx.logger.info(f"Available tool: {tool.name}") - + except Exception as e: self._ctx.logger.error(f"Failed to connect to Context7 MCP server: {e}") raise - + def _convert_mcp_tools_to_anthropic_format(self, mcp_tools): """Convert MCP tool definitions to Anthropic tool format""" anthropic_tools = [] @@ -110,11 +109,12 @@ def _convert_mcp_tools_to_anthropic_format(self, mcp_tools): anthropic_tool = { "name": tool.name, "description": tool.description or f"Context7 tool: {tool.name}", - "input_schema": tool.inputSchema or {"type": "object", "properties": {}} + "input_schema": tool.inputSchema + or {"type": "object", "properties": {}}, } anthropic_tools.append(anthropic_tool) return anthropic_tools - + async def process_query(self, query: str) -> str: """ Enhanced query processing with iterative search refinement. @@ -123,83 +123,94 @@ async def process_query(self, query: str) -> str: try: query = query.strip() self._ctx.logger.info(f"Processing query: '{query}'") - + # Try each search strategy until we find good results for i, strategy in enumerate(self.search_strategies, 1): self._ctx.logger.info(f"Attempt {i}: Trying {strategy.__name__}") - + # Generate search terms for this strategy search_terms = await strategy(query) - + # Try each search term from this strategy for term in search_terms: self._ctx.logger.info(f" Searching for: '{term}'") - + # Step 1: Search for libraries library_result = await self._session.call_tool( - "resolve-library-id", - {"libraryName": term} + "resolve-library-id", {"libraryName": term} ) - + # Step 2: Evaluate result quality evaluation = await self._evaluate_search_results( query, term, library_result.content ) - + if evaluation["is_relevant"]: - self._ctx.logger.info(f"✅ Found relevant results with term: '{term}'") - + self._ctx.logger.info( + f"✅ Found relevant results with term: '{term}'" + ) + # Step 3: Select best library ID library_id = evaluation["selected_library_id"] - + # Step 4: Get documentation with enhanced search - docs_result = await self._get_targeted_documentation(library_id, query, term) - + docs_result = await self._get_targeted_documentation( + library_id, query, term + ) + # Step 5: Format response return await self._format_documentation_response( query, docs_result.content, library_id, term ) else: self._ctx.logger.info(f"❌ No relevant results for: '{term}'") - + # If we get here, no strategy worked return await self._generate_no_results_response(query) - + except Exception as e: self._ctx.logger.error(f"Error processing query: {e}") return f"❌ Error processing your query: {str(e)}" - + # === Enhanced Library Documentation Retrieval === - - async def _get_targeted_documentation(self, library_id: str, original_query: str, search_term: str) -> Any: + + async def _get_targeted_documentation( + self, library_id: str, original_query: str, search_term: str + ) -> Any: """ Get targeted documentation from a library using optimized search parameters. This is the key improvement that adds the 'topic' parameter for better results. """ try: # Strategy 1: Use original query as topic (most specific) - self._ctx.logger.info(f"Searching {library_id} with topic: '{original_query}'") - + self._ctx.logger.info( + f"Searching {library_id} with topic: '{original_query}'" + ) + docs_result = await self._session.call_tool( "get-library-docs", { "context7CompatibleLibraryID": library_id, "tokens": 12000, # High token count for comprehensive results - "topic": original_query # This is the game-changer! - } + "topic": original_query, # This is the game-changer! + }, ) - + # Check if we got good results - content_quality = await self._assess_content_quality(docs_result.content, original_query) - + content_quality = await self._assess_content_quality( + docs_result.content, original_query + ) + if content_quality["is_sufficient"]: - self._ctx.logger.info(f"✅ High-quality content found with topic: '{original_query}'") + self._ctx.logger.info( + f"✅ High-quality content found with topic: '{original_query}'" + ) return docs_result - + # Strategy 2: Try with just keywords if original query was too specific - self._ctx.logger.info(f"Trying keyword-based search...") + self._ctx.logger.info("Trying keyword-based search...") keywords = await self._extract_search_keywords(original_query) - + for keyword in keywords: self._ctx.logger.info(f" Trying keyword: '{keyword}'") docs_result = await self._session.call_tool( @@ -207,45 +218,50 @@ async def _get_targeted_documentation(self, library_id: str, original_query: str { "context7CompatibleLibraryID": library_id, "tokens": 10000, - "topic": keyword - } + "topic": keyword, + }, + ) + + content_quality = await self._assess_content_quality( + docs_result.content, original_query ) - - content_quality = await self._assess_content_quality(docs_result.content, original_query) if content_quality["is_sufficient"]: - self._ctx.logger.info(f"✅ Good content found with keyword: '{keyword}'") + self._ctx.logger.info( + f"✅ Good content found with keyword: '{keyword}'" + ) return docs_result - + # Strategy 3: Fallback to general search (no topic filter) - self._ctx.logger.info(f"Fallback: General search without topic filter") + self._ctx.logger.info("Fallback: General search without topic filter") docs_result = await self._session.call_tool( "get-library-docs", { "context7CompatibleLibraryID": library_id, - "tokens": 8000 # Lower tokens since we're getting broader results - } + "tokens": 8000, # Lower tokens since we're getting broader results + }, ) - + return docs_result - + except Exception as e: self._ctx.logger.error(f"Error getting documentation: {e}") # Fallback to simple call if enhanced search fails return await self._session.call_tool( - "get-library-docs", - {"context7CompatibleLibraryID": library_id} + "get-library-docs", {"context7CompatibleLibraryID": library_id} ) - async def _assess_content_quality(self, content: Any, original_query: str) -> Dict[str, Any]: + async def _assess_content_quality( + self, content: Any, original_query: str + ) -> Dict[str, Any]: """ Assess if the retrieved content is sufficient for the user's query. """ try: text_content = self._extract_text_content(content) - + if len(text_content.strip()) < 100: # Too little content return {"is_sufficient": False, "reason": "Insufficient content"} - + # Use Claude to assess relevance assessment_prompt = f""" User's original query: "{original_query}" @@ -262,30 +278,27 @@ async def _assess_content_quality(self, content: Any, original_query: str) -> Di SUFFICIENT: [YES/NO] REASON: [Brief explanation] """ - + response = await asyncio.get_event_loop().run_in_executor( None, lambda: self.anthropic.messages.create( model="claude-3-5-sonnet-20241022", max_tokens=100, - messages=[{"role": "user", "content": assessment_prompt}] - ) + messages=[{"role": "user", "content": assessment_prompt}], + ), ) - + result_text = response.content[0].text.strip() - + is_sufficient = "SUFFICIENT: YES" in result_text reason = "Good quality content found" - - for line in result_text.split('\n'): - if line.startswith('REASON:'): - reason = line.split(':', 1)[1].strip() - - return { - "is_sufficient": is_sufficient, - "reason": reason - } - + + for line in result_text.split("\n"): + if line.startswith("REASON:"): + reason = line.split(":", 1)[1].strip() + + return {"is_sufficient": is_sufficient, "reason": reason} + except Exception as e: self._ctx.logger.error(f"Error assessing content quality: {e}") return {"is_sufficient": True, "reason": "Assessment failed, proceeding"} @@ -305,31 +318,35 @@ async def _extract_search_keywords(self, query: str) -> List[str]: Return just the terms, one per line: """ - + response = await asyncio.get_event_loop().run_in_executor( None, lambda: self.anthropic.messages.create( model="claude-3-5-sonnet-20241022", max_tokens=80, - messages=[{"role": "user", "content": keyword_prompt}] - ) + messages=[{"role": "user", "content": keyword_prompt}], + ), ) - - keywords = [line.strip() for line in response.content[0].text.strip().split('\n') if line.strip()] + + keywords = [ + line.strip() + for line in response.content[0].text.strip().split("\n") + if line.strip() + ] return keywords[:3] - + except Exception as e: self._ctx.logger.error(f"Error extracting keywords: {e}") # Fallback to simple word extraction words = query.split() return [word for word in words if len(word) > 3][:3] - + # === Search Strategies === - + async def _strategy_exact_match(self, query: str) -> List[str]: """Strategy 1: Try the query exactly as provided""" return [query] - + async def _strategy_add_framework_context(self, query: str) -> List[str]: """Strategy 2: Add common framework contexts""" # Use Claude to intelligently add framework context @@ -345,31 +362,42 @@ async def _strategy_add_framework_context(self, query: str) -> List[str]: Respond with just the search terms, one per line: """ - + try: response = await asyncio.get_event_loop().run_in_executor( None, lambda: self.anthropic.messages.create( model="claude-3-5-sonnet-20241022", max_tokens=150, - messages=[{"role": "user", "content": context_prompt}] - ) + messages=[{"role": "user", "content": context_prompt}], + ), ) - - terms = [line.strip() for line in response.content[0].text.strip().split('\n') if line.strip()] + + terms = [ + line.strip() + for line in response.content[0].text.strip().split("\n") + if line.strip() + ] return terms[:3] # Limit to 3 terms - + except Exception as e: self._ctx.logger.error(f"Error generating framework context: {e}") # Fallback to basic framework additions - frameworks = ["react", "next.js", "node.js", "python", "javascript", "fetch.ai"] + frameworks = [ + "react", + "next.js", + "node.js", + "python", + "javascript", + "fetch.ai", + ] return [f"{framework} {query}" for framework in frameworks[:2]] - + async def _strategy_add_language_context(self, query: str) -> List[str]: """Strategy 3: Add programming language context""" languages = ["python", "javascript", "typescript", "java", "go", "rust"] return [f"{query} {lang}" for lang in languages[:3]] - + async def _strategy_try_popular_alternatives(self, query: str) -> List[str]: """Strategy 4: Try popular alternatives for common terms""" alternatives_map = { @@ -381,16 +409,16 @@ async def _strategy_try_popular_alternatives(self, query: str) -> List[str]: "auth": ["authentication", "authorization", "oauth"], "ml": ["machine learning", "tensorflow", "pytorch"], "web": ["web framework", "frontend", "backend"], - "handlers": ["message handlers", "event handlers", "request handlers"] + "handlers": ["message handlers", "event handlers", "request handlers"], } - + query_lower = query.lower() for key, alternatives in alternatives_map.items(): if key in query_lower: return alternatives - + return [] - + async def _strategy_extract_keywords(self, query: str) -> List[str]: """Strategy 5: Extract and search individual keywords""" # Use Claude to extract key terms @@ -405,29 +433,35 @@ async def _strategy_extract_keywords(self, query: str) -> List[str]: Respond with just the keywords, one per line: """ - + try: response = await asyncio.get_event_loop().run_in_executor( None, lambda: self.anthropic.messages.create( model="claude-3-5-sonnet-20241022", max_tokens=100, - messages=[{"role": "user", "content": extraction_prompt}] - ) + messages=[{"role": "user", "content": extraction_prompt}], + ), ) - - keywords = [line.strip() for line in response.content[0].text.strip().split('\n') if line.strip()] + + keywords = [ + line.strip() + for line in response.content[0].text.strip().split("\n") + if line.strip() + ] return keywords[:3] - + except Exception as e: self._ctx.logger.error(f"Error extracting keywords: {e}") # Fallback to simple word splitting words = query.split() return [word for word in words if len(word) > 2][:3] - + # === Result Evaluation === - - async def _evaluate_search_results(self, original_query: str, search_term: str, content: Any) -> Dict[str, Any]: + + async def _evaluate_search_results( + self, original_query: str, search_term: str, content: Any + ) -> Dict[str, Any]: """ Evaluate search results to determine if they're relevant to the original query. Returns evaluation with selected library ID if relevant. @@ -435,10 +469,10 @@ async def _evaluate_search_results(self, original_query: str, search_term: str, try: # Extract text content text_content = self._extract_text_content(content) - + if not text_content.strip(): return {"is_relevant": False, "reason": "No results found"} - + # Use Claude to evaluate relevance and select best library evaluation_prompt = f""" Original user query: "{original_query}" @@ -458,66 +492,72 @@ async def _evaluate_search_results(self, original_query: str, search_term: str, Focus on finding libraries that would help answer the original query, even if the search term was different. """ - + response = await asyncio.get_event_loop().run_in_executor( None, lambda: self.anthropic.messages.create( model="claude-3-5-sonnet-20241022", max_tokens=200, - messages=[{"role": "user", "content": evaluation_prompt}] - ) + messages=[{"role": "user", "content": evaluation_prompt}], + ), ) - + result_text = response.content[0].text.strip() - + # Parse the response - lines = result_text.split('\n') + lines = result_text.split("\n") is_relevant = False library_id = None reason = "Unknown" - + for line in lines: - if line.startswith('RELEVANT:'): - is_relevant = 'YES' in line.upper() - elif line.startswith('LIBRARY_ID:'): - lib_part = line.split(':', 1)[1].strip() + if line.startswith("RELEVANT:"): + is_relevant = "YES" in line.upper() + elif line.startswith("LIBRARY_ID:"): + lib_part = line.split(":", 1)[1].strip() if lib_part != "NONE": library_id = lib_part - elif line.startswith('REASON:'): - reason = line.split(':', 1)[1].strip() - + elif line.startswith("REASON:"): + reason = line.split(":", 1)[1].strip() + return { "is_relevant": is_relevant and library_id is not None, "selected_library_id": library_id, - "reason": reason + "reason": reason, } - + except Exception as e: self._ctx.logger.error(f"Error evaluating results: {e}") return {"is_relevant": False, "reason": f"Evaluation error: {str(e)}"} - + def _extract_text_content(self, content: Any) -> str: """Extract text content from various content formats""" text_content = "" if isinstance(content, list): for item in content: - if hasattr(item, 'text'): + if hasattr(item, "text"): text_content += item.text - elif isinstance(item, dict) and 'text' in item: - text_content += item['text'] + elif isinstance(item, dict) and "text" in item: + text_content += item["text"] elif isinstance(item, str): text_content += item else: text_content = str(content) return text_content - - async def _format_documentation_response(self, original_query: str, raw_content: Any, library_id: str, successful_search_term: str) -> str: + + async def _format_documentation_response( + self, + original_query: str, + raw_content: Any, + library_id: str, + successful_search_term: str, + ) -> str: """Format the final documentation response""" text_content = self._extract_text_content(raw_content) - + if not text_content or text_content.strip() == "": return "No documentation found for your query." - + formatting_prompt = f""" Original User Query: "{original_query}" Successful Search Term: "{successful_search_term}" @@ -537,28 +577,28 @@ async def _format_documentation_response(self, original_query: str, raw_content: Provide a professional response that directly addresses: "{original_query}" """ - + try: response = await asyncio.get_event_loop().run_in_executor( None, lambda: self.anthropic.messages.create( model="claude-3-5-sonnet-20241022", max_tokens=4000, - messages=[{"role": "user", "content": formatting_prompt}] - ) + messages=[{"role": "user", "content": formatting_prompt}], + ), ) - + formatted_content = response.content[0].text.strip() - + # Add source attribution with search context attribution = f"\n\n---\n*Found via search: '{successful_search_term}' | Source: Context7 MCP ({library_id})*" - + return formatted_content + attribution - + except Exception as e: self._ctx.logger.error(f"Error formatting response: {e}") return f"# Documentation for {original_query}\n\n{text_content}\n\n---\n*Source: Context7 MCP ({library_id})*" - + async def _generate_no_results_response(self, query: str) -> str: """Generate a helpful response when no relevant results are found""" return f"""❌ **No relevant documentation found for: "{query}"** @@ -585,7 +625,7 @@ async def _generate_no_results_response(self, query: str) -> str: - "FastAPI authentication" Feel free to try a more specific query!""" - + async def cleanup(self): """Clean up the MCP connection""" try: @@ -595,6 +635,7 @@ async def cleanup(self): except Exception as e: self._ctx.logger.error(f"Error during cleanup: {e}") + # --- uAgent Setup --- chat_proto = Protocol(spec=chat_protocol_spec) @@ -603,20 +644,22 @@ async def cleanup(self): # Store MCP clients per session session_clients: Dict[str, Context7MCPClient] = {} + def is_session_valid(session_id: str) -> bool: """Check if session is valid and hasn't expired""" if session_id not in user_sessions: return False - - last_activity = user_sessions[session_id].get('last_activity', 0) + + last_activity = user_sessions[session_id].get("last_activity", 0) if time.time() - last_activity > SESSION_TIMEOUT: # Session expired, clean it up if session_id in user_sessions: del user_sessions[session_id] return False - + return True + async def get_context7_client(ctx: Context, session_id: str) -> Context7MCPClient: """Get or create Context7 MCP client for session""" if session_id not in session_clients or not is_session_valid(session_id): @@ -624,33 +667,36 @@ async def get_context7_client(ctx: Context, session_id: str) -> Context7MCPClien client = Context7MCPClient(ctx) await client.connect() session_clients[session_id] = client - + return session_clients[session_id] + @chat_proto.on_message(model=ChatMessage) async def handle_chat_message(ctx: Context, sender: str, msg: ChatMessage): session_id = str(ctx.session) # Send acknowledgment first ack_msg = ChatAcknowledgement( - timestamp=datetime.now(timezone.utc), - acknowledged_msg_id=msg.msg_id + timestamp=datetime.now(timezone.utc), acknowledged_msg_id=msg.msg_id ) await ctx.send(sender, ack_msg) for item in msg.content: if isinstance(item, TextContent): ctx.logger.info(f"Received message from {sender}: '{item.text}'") - + # Update session activity if session_id not in user_sessions: user_sessions[session_id] = {} - user_sessions[session_id]['last_activity'] = time.time() - + user_sessions[session_id]["last_activity"] = time.time() + query = item.text.strip() - + # Check for help queries - if any(word in query.lower() for word in ['help', 'what can you do', 'capabilities']): + if any( + word in query.lower() + for word in ["help", "what can you do", "capabilities"] + ): response_text = """📚 **Context7 Documentation Agent** I can help you get up-to-date documentation and code examples for any library or framework using intelligent search strategies! @@ -690,14 +736,19 @@ async def handle_chat_message(ctx: Context, sender: str, msg: ChatMessage): processing_msg = ChatMessage( msg_id=str(uuid4()), timestamp=datetime.now(timezone.utc), - content=[TextContent(type="text", text="🔍 Searching for documentation... This may take a moment as I try multiple search strategies with topic-focused retrieval.")] + content=[ + TextContent( + type="text", + text="🔍 Searching for documentation... This may take a moment as I try multiple search strategies with topic-focused retrieval.", + ) + ], ) await ctx.send(sender, processing_msg) - + # Get Context7 client and process query with enhanced search client = await get_context7_client(ctx, session_id) response_text = await client.process_query(query) - + except Exception as e: ctx.logger.error(f"Error processing query: {e}") response_text = f"""❌ **Error processing your request** @@ -710,34 +761,37 @@ async def handle_chat_message(ctx: Context, sender: str, msg: ChatMessage): • Make sure the MCP server is running 🆘 **Need help?** Try asking for 'help' to see available features.""" - + # Create and send final response response_msg = ChatMessage( msg_id=str(uuid4()), timestamp=datetime.now(timezone.utc), - content=[TextContent(type="text", text=response_text)] + content=[TextContent(type="text", text=response_text)], ) await ctx.send(sender, response_msg) + @chat_proto.on_message(model=ChatAcknowledgement) async def handle_chat_ack(ctx: Context, sender: str, msg: ChatAcknowledgement): pass + @agent.on_event("shutdown") async def on_shutdown(ctx: Context): for client in session_clients.values(): await client.cleanup() + agent.include(chat_proto) if __name__ == "__main__": print(f"Context7 Agent starting on http://localhost:{AGENT_PORT}") print(f"Agent address: {agent.address}") print("📚 Ready to provide up-to-date documentation with enhanced search!") - + try: agent.run() except KeyboardInterrupt: print("Agent stopped by user") except Exception as e: - print(f"Agent error: {e}") \ No newline at end of file + print(f"Agent error: {e}") diff --git a/mcp-agents/Github MCP Agent/agent.py b/mcp-agents/Github MCP Agent/agent.py index 42245f0f..3f03e111 100644 --- a/mcp-agents/Github MCP Agent/agent.py +++ b/mcp-agents/Github MCP Agent/agent.py @@ -1,15 +1,13 @@ import os import json import asyncio -import secrets -import urllib.parse from typing import Dict, Any, Optional from contextlib import AsyncExitStack from cryptography.fernet import Fernet import time import mcp from mcp.client.stdio import stdio_client -from uagents import Agent, Context, Protocol, Model +from uagents import Agent, Context, Protocol from uagents_core.contrib.protocols.chat import ( chat_protocol_spec, ChatMessage, @@ -35,7 +33,9 @@ raise ValueError("ANTHROPIC_API_KEY not found in .env file") # GitHub App Configuration (for Device Flow - no secrets needed!) -GITHUB_CLIENT_ID = os.getenv("GITHUB_CLIENT_ID", "Iv1.b507a08c87ecfe98") # Public GitHub CLI client ID +GITHUB_CLIENT_ID = os.getenv( + "GITHUB_CLIENT_ID", "Iv1.b507a08c87ecfe98" +) # Public GitHub CLI client ID AGENT_NAME = "github_agent" AGENT_PORT = 8005 @@ -52,108 +52,113 @@ # --- GitHub Device Flow OAuth Handler --- + class GitHubDeviceFlowHandler: def __init__(self): self.client_id = GITHUB_CLIENT_ID self.device_url = "https://github.com/login/device/code" self.token_url = "https://github.com/login/oauth/access_token" self.api_base = "https://api.github.com" - + async def start_device_flow(self, session_id: str) -> Dict[str, str]: """Start GitHub Device Flow OAuth""" async with httpx.AsyncClient() as client: response = await client.post( self.device_url, data={ - 'client_id': self.client_id, - 'scope': 'repo user read:org workflow' + "client_id": self.client_id, + "scope": "repo user read:org workflow", }, - headers={'Accept': 'application/json'} + headers={"Accept": "application/json"}, ) - + if response.status_code == 200: data = response.json() - + # Store device flow data in session if session_id not in user_sessions: user_sessions[session_id] = {} - - user_sessions[session_id].update({ - 'device_code': data['device_code'], - 'user_code': data['user_code'], - 'verification_uri': data['verification_uri'], - 'expires_in': data['expires_in'], - 'interval': data['interval'] - }) - + + user_sessions[session_id].update( + { + "device_code": data["device_code"], + "user_code": data["user_code"], + "verification_uri": data["verification_uri"], + "expires_in": data["expires_in"], + "interval": data["interval"], + } + ) + return data else: raise Exception(f"Failed to start device flow: {response.text}") - + async def poll_for_token(self, session_id: str) -> Optional[str]: """Poll GitHub for access token""" if session_id not in user_sessions: return None - + session_data = user_sessions[session_id] - device_code = session_data.get('device_code') - + device_code = session_data.get("device_code") + if not device_code: return None - + async with httpx.AsyncClient() as client: response = await client.post( self.token_url, data={ - 'client_id': self.client_id, - 'device_code': device_code, - 'grant_type': 'urn:ietf:params:oauth:grant-type:device_code' + "client_id": self.client_id, + "device_code": device_code, + "grant_type": "urn:ietf:params:oauth:grant-type:device_code", }, - headers={'Accept': 'application/json'} + headers={"Accept": "application/json"}, ) - + if response.status_code == 200: data = response.json() - if 'access_token' in data: - return data['access_token'] - elif data.get('error') == 'authorization_pending': - return 'pending' - elif data.get('error') == 'slow_down': - return 'slow_down' + if "access_token" in data: + return data["access_token"] + elif data.get("error") == "authorization_pending": + return "pending" + elif data.get("error") == "slow_down": + return "slow_down" else: return None - + return None - + async def get_user_info(self, access_token: str) -> Optional[Dict]: """Get GitHub user information""" async with httpx.AsyncClient() as client: # Get user info response = await client.get( f"{self.api_base}/user", - headers={'Authorization': f'token {access_token}'} + headers={"Authorization": f"token {access_token}"}, ) - + if response.status_code == 200: user_info = response.json() - + # Also check token scopes for debugging scopes_response = await client.get( f"{self.api_base}/user", - headers={'Authorization': f'token {access_token}'} + headers={"Authorization": f"token {access_token}"}, ) - + # GitHub returns scopes in the X-OAuth-Scopes header - if 'X-OAuth-Scopes' in scopes_response.headers: - scopes = scopes_response.headers['X-OAuth-Scopes'] - user_info['token_scopes'] = scopes + if "X-OAuth-Scopes" in scopes_response.headers: + scopes = scopes_response.headers["X-OAuth-Scopes"] + user_info["token_scopes"] = scopes print(f" Token scopes: {scopes}") - + return user_info return None + # --- MCP Client Logic --- + class GitHubMCPClient: def __init__(self, ctx: Context, access_token: str): self._ctx = ctx @@ -165,15 +170,20 @@ def __init__(self, ctx: Context, access_token: str): async def connect(self): """Connects to the GitHub MCP server via Docker with user's access token.""" - self._ctx.logger.info("Connecting to GitHub MCP server with user authentication...") + self._ctx.logger.info( + "Connecting to GitHub MCP server with user authentication..." + ) try: # GitHub MCP server command with user's access token docker_args = [ - "run", "-i", "--rm", - "-e", f"GITHUB_PERSONAL_ACCESS_TOKEN={self._access_token}", - "ghcr.io/github/github-mcp-server" + "run", + "-i", + "--rm", + "-e", + f"GITHUB_PERSONAL_ACCESS_TOKEN={self._access_token}", + "ghcr.io/github/github-mcp-server", ] - + params = mcp.StdioServerParameters(command="docker", args=docker_args) read_stream, write_stream = await self._exit_stack.enter_async_context( stdio_client(params) @@ -182,12 +192,14 @@ async def connect(self): mcp.ClientSession(read_stream, write_stream) ) await self._session.initialize() - + # Get available tools from GitHub MCP server tools_result = await self._session.list_tools() self.tools = self._convert_mcp_tools_to_anthropic_format(tools_result.tools) - - self._ctx.logger.info(f"Successfully connected to GitHub MCP. Available tools: {[t.name for t in tools_result.tools]}") + + self._ctx.logger.info( + f"Successfully connected to GitHub MCP. Available tools: {[t.name for t in tools_result.tools]}" + ) except Exception as e: self._ctx.logger.error(f"Failed to connect to GitHub MCP server: {e}") raise @@ -199,11 +211,8 @@ def _convert_mcp_tools_to_anthropic_format(self, mcp_tools) -> list: anthropic_tool = { "name": tool.name, "description": tool.description, - "input_schema": tool.inputSchema or { - "type": "object", - "properties": {}, - "required": [] - } + "input_schema": tool.inputSchema + or {"type": "object", "properties": {}, "required": []}, } anthropic_tools.append(anthropic_tool) return anthropic_tools @@ -216,34 +225,46 @@ async def process_query(self, query: str) -> str: response = self.anthropic.messages.create( model="claude-3-5-sonnet-20240620", max_tokens=2048, - messages=[{ - "role": "user", - "content": f"Help me with this GitHub request: {query}" - }], + messages=[ + { + "role": "user", + "content": f"Help me with this GitHub request: {query}", + } + ], tools=self.tools, ) - tool_use = next((content for content in response.content if content.type == 'tool_use'), None) + tool_use = next( + (content for content in response.content if content.type == "tool_use"), + None, + ) # If the model wants to use a tool if tool_use: tool_name = tool_use.name tool_input = tool_use.input - self._ctx.logger.info(f"Claude selected GitHub tool: {tool_name} with input: {tool_input}") + self._ctx.logger.info( + f"Claude selected GitHub tool: {tool_name} with input: {tool_input}" + ) # Call the selected tool on the GitHub MCP server mcp_response = await self._session.call_tool(tool_name, tool_input) - + # Debug: Log the raw MCP response self._ctx.logger.info(f"MCP Response type: {type(mcp_response)}") - self._ctx.logger.info(f"MCP Response content type: {type(mcp_response.content)}") + self._ctx.logger.info( + f"MCP Response content type: {type(mcp_response.content)}" + ) self._ctx.logger.info(f"MCP Response content: {mcp_response.content}") - + # Format the response for the user return self._format_response_for_user(tool_name, mcp_response.content) - + # If the model just wants to chat - text_response = next((content for content in response.content if content.type == 'text'), None) + text_response = next( + (content for content in response.content if content.type == "text"), + None, + ) if text_response: return text_response.text @@ -259,7 +280,7 @@ def _format_response_for_user(self, tool_name: str, content: Any) -> str: # Parse the MCP response content if isinstance(content, list) and len(content) > 0: # Handle text content from MCP response - if hasattr(content[0], 'text'): + if hasattr(content[0], "text"): response_text = content[0].text # Parse JSON if it's a string if isinstance(response_text, str): @@ -267,27 +288,33 @@ def _format_response_for_user(self, tool_name: str, content: Any) -> str: data = json.loads(response_text) except json.JSONDecodeError: # If not JSON, treat as plain text - return response_text[:1000] + "..." if len(response_text) > 1000 else response_text + return ( + response_text[:1000] + "..." + if len(response_text) > 1000 + else response_text + ) else: data = response_text else: data = content # Format based on tool type - if 'repositories' in tool_name or 'search_repositories' in tool_name: + if "repositories" in tool_name or "search_repositories" in tool_name: return self._format_repositories(data) - elif 'issues' in tool_name: + elif "issues" in tool_name: return self._format_issues(data) - elif 'pull_request' in tool_name: + elif "pull_request" in tool_name: return self._format_pull_requests(data) - elif 'user' in tool_name or tool_name == 'get_me': + elif "user" in tool_name or tool_name == "get_me": return self._format_user_info(data) else: # For other tools, return a truncated version text_data = str(data) if len(text_data) > 1000: return f"**{tool_name.replace('_', ' ').title()} Results:**\n\n{text_data[:1000]}...\n\n*Response truncated for readability*" - return f"**{tool_name.replace('_', ' ').title()} Results:**\n\n{text_data}" + return ( + f"**{tool_name.replace('_', ' ').title()} Results:**\n\n{text_data}" + ) except Exception as e: self._ctx.logger.error(f"Error formatting response: {e}") @@ -302,12 +329,12 @@ def _format_user_info(self, data: Dict) -> str: if isinstance(data, dict): return f""" 👤 **GitHub Profile:** -• **Name:** {data.get('name', 'N/A')} -• **Username:** {data.get('login', 'N/A')} -• **Bio:** {data.get('bio', 'N/A')} -• **Public Repos:** {data.get('public_repos', 'N/A')} -• **Followers:** {data.get('followers', 'N/A')} -• **Following:** {data.get('following', 'N/A')} +• **Name:** {data.get("name", "N/A")} +• **Username:** {data.get("login", "N/A")} +• **Bio:** {data.get("bio", "N/A")} +• **Public Repos:** {data.get("public_repos", "N/A")} +• **Followers:** {data.get("followers", "N/A")} +• **Following:** {data.get("following", "N/A")} """ return str(data) @@ -316,22 +343,26 @@ def _format_issues(self, data: Any) -> str: if isinstance(data, list): if not data: return "No issues found." - + formatted = "📋 **GitHub Issues:**\n\n" for issue in data[:10]: # Limit to 10 issues - formatted += f"**#{issue.get('number')}** {issue.get('title', 'No title')}\n" + formatted += ( + f"**#{issue.get('number')}** {issue.get('title', 'No title')}\n" + ) formatted += f"• State: {issue.get('state', 'unknown')}\n" - formatted += f"• Author: {issue.get('user', {}).get('login', 'unknown')}\n" + formatted += ( + f"• Author: {issue.get('user', {}).get('login', 'unknown')}\n" + ) formatted += f"• URL: {issue.get('html_url', 'N/A')}\n\n" return formatted elif isinstance(data, dict): return f""" -📋 **Issue #{data.get('number')}:** -• **Title:** {data.get('title', 'N/A')} -• **State:** {data.get('state', 'N/A')} -• **Author:** {data.get('user', {}).get('login', 'N/A')} -• **Body:** {data.get('body', 'No description')[:200]}... -• **URL:** {data.get('html_url', 'N/A')} +📋 **Issue #{data.get("number")}:** +• **Title:** {data.get("title", "N/A")} +• **State:** {data.get("state", "N/A")} +• **Author:** {data.get("user", {}).get("login", "N/A")} +• **Body:** {data.get("body", "No description")[:200]}... +• **URL:** {data.get("html_url", "N/A")} """ return str(data) @@ -339,61 +370,73 @@ def _format_repositories(self, data: Any) -> str: """Format repository data.""" try: # Handle GitHub search API response structure - if isinstance(data, dict) and 'items' in data: + if isinstance(data, dict) and "items" in data: # This is a search response with pagination info - repos = data['items'] - total_count = data.get('total_count', len(repos)) - + repos = data["items"] + total_count = data.get("total_count", len(repos)) + if not repos: return "No repositories found." - + formatted = f"📁 **Found {total_count} repositories** (showing top {min(len(repos), 5)}):\n\n" for repo in repos[:5]: # Limit to 5 repos - formatted += f"**{repo.get('full_name', repo.get('name', 'Unknown'))}**\n" - formatted += f"• Description: {repo.get('description', 'No description')}\n" + formatted += ( + f"**{repo.get('full_name', repo.get('name', 'Unknown'))}**\n" + ) + formatted += ( + f"• Description: {repo.get('description', 'No description')}\n" + ) formatted += f"• Language: {repo.get('language', 'N/A')}\n" formatted += f"• ⭐ {repo.get('stargazers_count', 0)} stars\n" formatted += f"• 🍴 {repo.get('forks_count', 0)} forks\n" formatted += f"• URL: {repo.get('html_url', 'N/A')}\n\n" return formatted - + elif isinstance(data, list): # This is a direct list of repositories if not data: return "No repositories found." - - formatted = f"📁 **GitHub Repositories** (showing {min(len(data), 5)}):\n\n" + + formatted = ( + f"📁 **GitHub Repositories** (showing {min(len(data), 5)}):\n\n" + ) for repo in data[:5]: # Limit to 5 repos - formatted += f"**{repo.get('full_name', repo.get('name', 'Unknown'))}**\n" - formatted += f"• Description: {repo.get('description', 'No description')}\n" + formatted += ( + f"**{repo.get('full_name', repo.get('name', 'Unknown'))}**\n" + ) + formatted += ( + f"• Description: {repo.get('description', 'No description')}\n" + ) formatted += f"• Language: {repo.get('language', 'N/A')}\n" formatted += f"• ⭐ {repo.get('stargazers_count', 0)} stars\n" formatted += f"• 🍴 {repo.get('forks_count', 0)} forks\n" formatted += f"• URL: {repo.get('html_url', 'N/A')}\n\n" return formatted - + elif isinstance(data, dict): # Single repository return f""" -📁 **Repository: {data.get('full_name', 'Unknown')}** -• **Description:** {data.get('description', 'No description')} -• **Language:** {data.get('language', 'N/A')} -• **⭐ Stars:** {data.get('stargazers_count', 0)} -• **🍴 Forks:** {data.get('forks_count', 0)} -• **URL:** {data.get('html_url', 'N/A')} +📁 **Repository: {data.get("full_name", "Unknown")}** +• **Description:** {data.get("description", "No description")} +• **Language:** {data.get("language", "N/A")} +• **⭐ Stars:** {data.get("stargazers_count", 0)} +• **🍴 Forks:** {data.get("forks_count", 0)} +• **URL:** {data.get("html_url", "N/A")} """ else: return f"Repository data: {str(data)[:500]}..." - + except Exception as e: - return f"Error formatting repositories: {e}\n\nRaw data: {str(data)[:200]}..." + return ( + f"Error formatting repositories: {e}\n\nRaw data: {str(data)[:200]}..." + ) def _format_pull_requests(self, data: Any) -> str: """Format pull request data.""" if isinstance(data, list): if not data: return "No pull requests found." - + formatted = "🔀 **Pull Requests:**\n\n" for pr in data[:10]: # Limit to 10 PRs formatted += f"**#{pr.get('number')}** {pr.get('title', 'No title')}\n" @@ -403,12 +446,12 @@ def _format_pull_requests(self, data: Any) -> str: return formatted elif isinstance(data, dict): return f""" -🔀 **Pull Request #{data.get('number')}:** -• **Title:** {data.get('title', 'N/A')} -• **State:** {data.get('state', 'N/A')} -• **Author:** {data.get('user', {}).get('login', 'N/A')} -• **Body:** {data.get('body', 'No description')[:200]}... -• **URL:** {data.get('html_url', 'N/A')} +🔀 **Pull Request #{data.get("number")}:** +• **Title:** {data.get("title", "N/A")} +• **State:** {data.get("state", "N/A")} +• **Author:** {data.get("user", {}).get("login", "N/A")} +• **Body:** {data.get("body", "No description")[:200]}... +• **URL:** {data.get("html_url", "N/A")} """ return str(data) @@ -417,6 +460,7 @@ async def cleanup(self): self._ctx.logger.info("Cleaning up GitHub MCP connection...") await self._exit_stack.aclose() + # --- uAgent Setup --- chat_proto = Protocol(spec=chat_protocol_spec) @@ -426,150 +470,167 @@ async def cleanup(self): # This dictionary will hold a client for each user session session_clients: Dict[str, GitHubMCPClient] = {} + def is_user_authenticated(session_id: str) -> bool: """Check if user is authenticated and session hasn't expired""" if session_id not in user_sessions: return False - + session = user_sessions[session_id] - if 'access_token' not in session: + if "access_token" not in session: return False - + # Check session timeout - last_activity = session.get('last_activity', 0) + last_activity = session.get("last_activity", 0) if time.time() - last_activity > SESSION_TIMEOUT: # Session expired, clean up del user_sessions[session_id] return False - + # Update last activity - session['last_activity'] = time.time() + session["last_activity"] = time.time() return True -async def get_authenticated_client(ctx: Context, session_id: str) -> Optional[GitHubMCPClient]: + +async def get_authenticated_client( + ctx: Context, session_id: str +) -> Optional[GitHubMCPClient]: """Get or create authenticated MCP client for user session""" if session_id in session_clients: return session_clients[session_id] - + if not is_user_authenticated(session_id): return None - + # Create new MCP client with user's access token - access_token = cipher_suite.decrypt(user_sessions[session_id]['access_token'].encode()).decode() + access_token = cipher_suite.decrypt( + user_sessions[session_id]["access_token"].encode() + ).decode() client = GitHubMCPClient(ctx, access_token) await client.connect() session_clients[session_id] = client return client + async def handle_device_flow_polling(ctx: Context, session_id: str): """Handle Device Flow polling for token""" session_data = user_sessions[session_id] - interval = session_data.get('interval', 5) - expires_in = session_data.get('expires_in', 900) # 15 minutes - + interval = session_data.get("interval", 5) + expires_in = session_data.get("expires_in", 900) # 15 minutes + start_time = datetime.now(timezone.utc) - + while True: # Check if expired if (datetime.now(timezone.utc) - start_time).seconds > expires_in: ctx.logger.info("Device flow expired") break - + # Poll for token result = await device_flow_handler.poll_for_token(session_id) - - if result and result != 'pending' and result != 'slow_down': + + if result and result != "pending" and result != "slow_down": # Got access token! access_token = result user_info = await device_flow_handler.get_user_info(access_token) - + # Store in session - user_sessions[session_id]['access_token'] = cipher_suite.encrypt(access_token.encode()).decode() - user_sessions[session_id]['user_info'] = user_info - user_sessions[session_id]['last_activity'] = time.time() - - ctx.logger.info(f" User {user_info.get('login', 'Unknown')} authenticated successfully") + user_sessions[session_id]["access_token"] = cipher_suite.encrypt( + access_token.encode() + ).decode() + user_sessions[session_id]["user_info"] = user_info + user_sessions[session_id]["last_activity"] = time.time() + + ctx.logger.info( + f" User {user_info.get('login', 'Unknown')} authenticated successfully" + ) break - elif result == 'slow_down': + elif result == "slow_down": interval += 5 # Increase polling interval - + await asyncio.sleep(interval) + @chat_proto.on_message(model=ChatMessage) async def handle_chat_message(ctx: Context, sender: str, msg: ChatMessage): session_id = str(ctx.session) # Send acknowledgment first ack_msg = ChatAcknowledgement( - timestamp=datetime.now(timezone.utc), - acknowledged_msg_id=msg.msg_id + timestamp=datetime.now(timezone.utc), acknowledged_msg_id=msg.msg_id ) await ctx.send(sender, ack_msg) for item in msg.content: if isinstance(item, TextContent): ctx.logger.info(f"Received message from {sender}: '{item.text}'") - + # Check if user is authenticated if not is_user_authenticated(session_id): # Check if this looks like a GitHub token - if item.text.startswith('ghp_') and len(item.text) > 20: + if item.text.startswith("ghp_") and len(item.text) > 20: # User provided a GitHub token directly ctx.logger.info("User provided GitHub token directly") - + # Validate the token async with httpx.AsyncClient() as client: response = await client.get( "https://api.github.com/user", - headers={'Authorization': f'token {item.text}'} + headers={"Authorization": f"token {item.text}"}, ) - + if response.status_code == 200: user_info = response.json() - + # Also check token scopes for debugging scopes_response = await client.get( device_flow_handler.api_base + "/user", - headers={'Authorization': f'token {item.text}'} + headers={"Authorization": f"token {item.text}"}, ) - + # GitHub returns scopes in the X-OAuth-Scopes header - if 'X-OAuth-Scopes' in scopes_response.headers: - scopes = scopes_response.headers['X-OAuth-Scopes'] - user_info['token_scopes'] = scopes - ctx.logger.info(f"Token validated with required scopes") - - if 'repo' not in scopes: + if "X-OAuth-Scopes" in scopes_response.headers: + scopes = scopes_response.headers["X-OAuth-Scopes"] + user_info["token_scopes"] = scopes + ctx.logger.info("Token validated with required scopes") + + if "repo" not in scopes: response_text = "❌ **Token Missing Permissions**\n\nYour token doesn't have 'repo' scope needed to create repositories.\n\nPlease create a new token at https://github.com/settings/tokens/new with these scopes:\n- ✅ repo\n- ✅ user:email\n- ✅ read:user" else: # Store token and user info if session_id not in user_sessions: user_sessions[session_id] = {} - user_sessions[session_id]['access_token'] = cipher_suite.encrypt(item.text.encode()).decode() - user_sessions[session_id]['user_info'] = user_info - user_sessions[session_id]['last_activity'] = time.time() - - ctx.logger.info(f"✅ User {user_info.get('login', 'Unknown')} authenticated with manual token") + user_sessions[session_id]["access_token"] = ( + cipher_suite.encrypt(item.text.encode()).decode() + ) + user_sessions[session_id]["user_info"] = user_info + user_sessions[session_id]["last_activity"] = time.time() + + ctx.logger.info( + f"✅ User {user_info.get('login', 'Unknown')} authenticated with manual token" + ) response_text = f"✅ **Authenticated Successfully!**\n\n👤 Welcome {user_info.get('login', 'Unknown')}!\n\nYou can now use all GitHub operations. Try:\n- 'Show my repositories'\n- 'Create a new repository'\n- 'Search for Python projects'" else: response_text = "❌ **Invalid Token**\n\nThe token you provided is invalid. Please check your token and try again.\n\nCreate a new token at: https://github.com/settings/tokens/new" - + response_msg = ChatMessage( timestamp=datetime.now(timezone.utc), msg_id=str(uuid4()), - content=[TextContent(type="text", text=response_text)] + content=[TextContent(type="text", text=response_text)], ) await ctx.send(sender, response_msg) return - + # Start OAuth flow if no valid token provided ctx.logger.info("Starting GitHub Device Flow authentication...") try: - device_data = await device_flow_handler.start_device_flow(session_id) - + device_data = await device_flow_handler.start_device_flow( + session_id + ) + # Start polling in background asyncio.create_task(handle_device_flow_polling(ctx, session_id)) - + response_text = f""" 🔐 **GitHub Authentication Required** @@ -582,7 +643,7 @@ async def handle_chat_message(ctx: Context, sender: str, msg: ChatMessage): **OPTION 2: Device Flow OAuth** 1. Visit: https://github.com/login/device -2. Enter code: **{device_data['user_code']}** +2. Enter code: **{device_data["user_code"]}** I'll automatically detect which method you choose! @@ -601,14 +662,14 @@ async def handle_chat_message(ctx: Context, sender: str, msg: ChatMessage): response_text = await client.process_query(item.text) else: response_text = "Error: Unable to connect to GitHub. Please try authenticating again." - + response_msg = ChatMessage( timestamp=datetime.now(timezone.utc), msg_id=uuid4(), - content=[TextContent(type="text", text=response_text)] + content=[TextContent(type="text", text=response_text)], ) await ctx.send(sender, response_msg) - + elif isinstance(item, EndSessionContent): ctx.logger.info(f"Session ended by {sender}") if session_id in session_clients: @@ -616,21 +677,24 @@ async def handle_chat_message(ctx: Context, sender: str, msg: ChatMessage): del session_clients[session_id] if session_id in user_sessions: del user_sessions[session_id] - + elif isinstance(item, StartSessionContent): ctx.logger.info(f"Session started by {sender}") # Handle session start + @chat_proto.on_message(model=ChatAcknowledgement) async def handle_chat_ack(ctx: Context, sender: str, msg: ChatAcknowledgement): ctx.logger.info(f"Received acknowledgment from {sender}") + @agent.on_event("shutdown") async def on_shutdown(ctx: Context): ctx.logger.info("Agent shutting down, cleaning up all active GitHub sessions.") for client in session_clients.values(): await client.cleanup() + agent.include(chat_proto) if __name__ == "__main__": diff --git a/mcp-agents/Perplexity MCP Agent/agent.py b/mcp-agents/Perplexity MCP Agent/agent.py index 36a87927..5f2054ff 100644 --- a/mcp-agents/Perplexity MCP Agent/agent.py +++ b/mcp-agents/Perplexity MCP Agent/agent.py @@ -1,15 +1,11 @@ import os import json -import asyncio -import secrets -import urllib.parse from typing import Dict, Any, Optional from contextlib import AsyncExitStack -from cryptography.fernet import Fernet import time import mcp from mcp.client.stdio import stdio_client -from uagents import Agent, Context, Protocol, Model +from uagents import Agent, Context, Protocol from uagents_core.contrib.protocols.chat import ( chat_protocol_spec, ChatMessage, @@ -22,7 +18,6 @@ from uuid import uuid4 from dotenv import load_dotenv from anthropic import Anthropic -import httpx # --- Agent Configuration --- @@ -49,6 +44,7 @@ # --- MCP Client Logic --- + class PerplexityMCPClient: def __init__(self, ctx: Context): self._ctx = ctx @@ -63,11 +59,14 @@ async def connect(self): try: # Perplexity MCP server command with API key docker_args = [ - "run", "-i", "--rm", - "-e", f"PERPLEXITY_API_KEY={PERPLEXITY_API_KEY}", - "mcp/perplexity-ask" + "run", + "-i", + "--rm", + "-e", + f"PERPLEXITY_API_KEY={PERPLEXITY_API_KEY}", + "mcp/perplexity-ask", ] - + params = mcp.StdioServerParameters(command="docker", args=docker_args) read_stream, write_stream = await self._exit_stack.enter_async_context( stdio_client(params) @@ -76,12 +75,14 @@ async def connect(self): mcp.ClientSession(read_stream, write_stream) ) await self._session.initialize() - + # Get available tools from Perplexity MCP server tools_result = await self._session.list_tools() self.tools = self._convert_mcp_tools_to_anthropic_format(tools_result.tools) - - self._ctx.logger.info(f"Successfully connected to Perplexity MCP. Available tools: {[t.name for t in tools_result.tools]}") + + self._ctx.logger.info( + f"Successfully connected to Perplexity MCP. Available tools: {[t.name for t in tools_result.tools]}" + ) except Exception as e: self._ctx.logger.error(f"Failed to connect to Perplexity MCP server: {e}") raise @@ -93,11 +94,8 @@ def _convert_mcp_tools_to_anthropic_format(self, mcp_tools) -> list: anthropic_tool = { "name": tool.name, "description": tool.description, - "input_schema": tool.inputSchema or { - "type": "object", - "properties": {}, - "required": [] - } + "input_schema": tool.inputSchema + or {"type": "object", "properties": {}, "required": []}, } anthropic_tools.append(anthropic_tool) return anthropic_tools @@ -110,33 +108,45 @@ async def process_query(self, query: str) -> str: response = self.anthropic.messages.create( model="claude-3-5-sonnet-20240620", max_tokens=4096, - messages=[{ - "role": "user", - "content": f"Please search for information about: {query}. Use the perplexity_ask tool to get real-time web information." - }], + messages=[ + { + "role": "user", + "content": f"Please search for information about: {query}. Use the perplexity_ask tool to get real-time web information.", + } + ], tools=self.tools, ) - tool_use = next((content for content in response.content if content.type == 'tool_use'), None) + tool_use = next( + (content for content in response.content if content.type == "tool_use"), + None, + ) # If the model wants to use a tool if tool_use: tool_name = tool_use.name tool_input = tool_use.input - self._ctx.logger.info(f"Claude selected Perplexity tool: {tool_name} with input: {tool_input}") + self._ctx.logger.info( + f"Claude selected Perplexity tool: {tool_name} with input: {tool_input}" + ) # Call the selected tool on the Perplexity MCP server mcp_response = await self._session.call_tool(tool_name, tool_input) - + # Debug: Log the raw MCP response self._ctx.logger.info(f"MCP Response type: {type(mcp_response)}") - self._ctx.logger.info(f"MCP Response content type: {type(mcp_response.content)}") - + self._ctx.logger.info( + f"MCP Response content type: {type(mcp_response.content)}" + ) + # Format the response for the user return self._format_response_for_user(tool_name, mcp_response.content) - + # If the model just wants to chat - text_response = next((content for content in response.content if content.type == 'text'), None) + text_response = next( + (content for content in response.content if content.type == "text"), + None, + ) if text_response: return text_response.text @@ -152,7 +162,7 @@ def _format_response_for_user(self, tool_name: str, content: Any) -> str: # Parse the MCP response content if isinstance(content, list) and len(content) > 0: # Handle text content from MCP response - if hasattr(content[0], 'text'): + if hasattr(content[0], "text"): response_text = content[0].text # Parse JSON if it's a string if isinstance(response_text, str): @@ -173,35 +183,39 @@ def _format_response_for_user(self, tool_name: str, content: Any) -> str: except Exception as e: self._ctx.logger.error(f"Error formatting Perplexity response: {e}") - return f"Search completed, but there was an issue formatting the response: {e}" + return ( + f"Search completed, but there was an issue formatting the response: {e}" + ) def _format_perplexity_response(self, data: Dict) -> str: """Format Perplexity API response for better readability.""" try: # Extract key information from Perplexity response - if 'choices' in data and len(data['choices']) > 0: - choice = data['choices'][0] - if 'message' in choice and 'content' in choice['message']: - content = choice['message']['content'] - + if "choices" in data and len(data["choices"]) > 0: + choice = data["choices"][0] + if "message" in choice and "content" in choice["message"]: + content = choice["message"]["content"] + # Add metadata if available response_parts = [f"🔍 **Search Results:**\n\n{content}"] - + # Add sources if available - if 'citations' in choice: + if "citations" in choice: response_parts.append("\n\n📚 **Sources:**") - for i, citation in enumerate(choice['citations'][:5], 1): + for i, citation in enumerate(choice["citations"][:5], 1): if isinstance(citation, str): response_parts.append(f"{i}. {citation}") - elif isinstance(citation, dict) and 'url' in citation: - title = citation.get('title', citation['url']) - response_parts.append(f"{i}. [{title}]({citation['url']})") - + elif isinstance(citation, dict) and "url" in citation: + title = citation.get("title", citation["url"]) + response_parts.append( + f"{i}. [{title}]({citation['url']})" + ) + return "\n".join(response_parts) - + # Fallback formatting return f"🔍 **Search Results:**\n\n{json.dumps(data, indent=2)}" - + except Exception as e: self._ctx.logger.error(f"Error in _format_perplexity_response: {e}") return f"Search completed: {str(data)}" @@ -214,6 +228,7 @@ async def cleanup(self): except Exception as e: self._ctx.logger.error(f"Error during cleanup: {e}") + # --- uAgent Setup --- chat_proto = Protocol(spec=chat_protocol_spec) @@ -222,56 +237,65 @@ async def cleanup(self): # Store MCP clients per session session_clients: Dict[str, PerplexityMCPClient] = {} + def is_session_valid(session_id: str) -> bool: """Check if session is valid and hasn't expired.""" if session_id not in user_sessions: return False - - last_activity = user_sessions[session_id].get('last_activity', 0) + + last_activity = user_sessions[session_id].get("last_activity", 0) if time.time() - last_activity > SESSION_TIMEOUT: # Session expired, clean up if session_id in user_sessions: del user_sessions[session_id] return False - + return True -async def get_perplexity_client(ctx: Context, session_id: str) -> Optional[PerplexityMCPClient]: + +async def get_perplexity_client( + ctx: Context, session_id: str +) -> Optional[PerplexityMCPClient]: """Get or create Perplexity MCP client for session.""" if session_id not in session_clients: try: client = PerplexityMCPClient(ctx) await client.connect() session_clients[session_id] = client - ctx.logger.info(f"Created new Perplexity MCP client for session {session_id}") + ctx.logger.info( + f"Created new Perplexity MCP client for session {session_id}" + ) except Exception as e: ctx.logger.error(f"Failed to create Perplexity MCP client: {e}") return None - + return session_clients[session_id] + @chat_proto.on_message(model=ChatMessage) async def handle_chat_message(ctx: Context, sender: str, msg: ChatMessage): session_id = str(ctx.session) # Send acknowledgment first ack_msg = ChatAcknowledgement( - timestamp=datetime.now(timezone.utc), - acknowledged_msg_id=msg.msg_id + timestamp=datetime.now(timezone.utc), acknowledged_msg_id=msg.msg_id ) await ctx.send(sender, ack_msg) for item in msg.content: if isinstance(item, TextContent): ctx.logger.info(f"Received message from {sender}: '{item.text}'") - + # Update session activity if session_id not in user_sessions: user_sessions[session_id] = {} - user_sessions[session_id]['last_activity'] = time.time() - + user_sessions[session_id]["last_activity"] = time.time() + # Check if this is a help request - if any(keyword in item.text.lower() for keyword in ['help', 'what can you do', 'commands']): + if any( + keyword in item.text.lower() + for keyword in ["help", "what can you do", "commands"] + ): response_text = """ 🔍 **Perplexity Web Search Agent** @@ -299,14 +323,14 @@ async def handle_chat_message(ctx: Context, sender: str, msg: ChatMessage): response_text = await client.process_query(item.text) else: response_text = "Sorry, I'm having trouble connecting to the search service. Please try again in a moment." - + response_msg = ChatMessage( timestamp=datetime.now(timezone.utc), msg_id=str(uuid4()), - content=[TextContent(type="text", text=response_text)] + content=[TextContent(type="text", text=response_text)], ) await ctx.send(sender, response_msg) - + elif isinstance(item, EndSessionContent): ctx.logger.info(f"Session ended by {sender}") if session_id in session_clients: @@ -314,29 +338,34 @@ async def handle_chat_message(ctx: Context, sender: str, msg: ChatMessage): del session_clients[session_id] if session_id in user_sessions: del user_sessions[session_id] - + elif isinstance(item, StartSessionContent): ctx.logger.info(f"Session started by {sender}") # Send welcome message welcome_msg = ChatMessage( timestamp=datetime.now(timezone.utc), msg_id=str(uuid4()), - content=[TextContent( - type="text", - text="🔍 **Welcome to Perplexity Search Agent!**\n\nI can search the web for real-time information on any topic. What would you like to know?" - )] + content=[ + TextContent( + type="text", + text="🔍 **Welcome to Perplexity Search Agent!**\n\nI can search the web for real-time information on any topic. What would you like to know?", + ) + ], ) await ctx.send(sender, welcome_msg) + @chat_proto.on_message(model=ChatAcknowledgement) async def handle_chat_ack(ctx: Context, sender: str, msg: ChatAcknowledgement): pass + @agent.on_event("shutdown") async def on_shutdown(ctx: Context): for client in session_clients.values(): await client.cleanup() + agent.include(chat_proto) if __name__ == "__main__": diff --git a/mcp-agents/airbnb-mcp-agent/agent.py b/mcp-agents/airbnb-mcp-agent/agent.py index 79f0518f..7aa1355e 100644 --- a/mcp-agents/airbnb-mcp-agent/agent.py +++ b/mcp-agents/airbnb-mcp-agent/agent.py @@ -2,27 +2,23 @@ """ Airbnb MCP Agent for Agentverse -Provides real-time Airbnb property search and booking information +Provides real-time Airbnb property search and booking information via Airbnb MCP server integration. Makes it discoverable on ASI:One LLM. """ import os import json -import asyncio -import secrets -from typing import Dict, Any, Optional +from typing import Dict, Any from contextlib import AsyncExitStack import time import mcp from mcp.client.stdio import stdio_client -from uagents import Agent, Context, Protocol, Model +from uagents import Agent, Context, Protocol from uagents_core.contrib.protocols.chat import ( chat_protocol_spec, ChatMessage, ChatAcknowledgement, TextContent, - EndSessionContent, - StartSessionContent, ) from datetime import datetime, timezone from uuid import uuid4 @@ -50,9 +46,10 @@ # --- MCP Client Logic --- + class AirbnbMCPClient: """Airbnb MCP Client for property search and booking assistance""" - + def __init__(self, ctx: Context): self._ctx = ctx self._session: mcp.ClientSession = None @@ -64,35 +61,37 @@ async def connect(self): """Connect to Airbnb MCP server via npx""" try: self._ctx.logger.info("Connecting to Airbnb MCP server via npx...") - + # Connect to the Airbnb MCP server using npx server_params = mcp.StdioServerParameters( command="npx", args=["-y", "@openbnb/mcp-server-airbnb", "--ignore-robots-txt"], - env=None + env=None, ) - + stdio_transport = await self._exit_stack.enter_async_context( stdio_client(server_params) ) - + self._session = await self._exit_stack.enter_async_context( mcp.ClientSession(stdio_transport[0], stdio_transport[1]) ) - + await self._session.initialize() - + # Get available tools list_tools_response = await self._session.list_tools() mcp_tools = list_tools_response.tools - - self._ctx.logger.info(f"Connected to Airbnb MCP server with {len(mcp_tools)} tools") + + self._ctx.logger.info( + f"Connected to Airbnb MCP server with {len(mcp_tools)} tools" + ) for tool in mcp_tools: self._ctx.logger.info(f"Available tool: {tool.name}") - + # Convert MCP tools to Anthropic format self.tools = self._convert_mcp_tools_to_anthropic_format(mcp_tools) - + except Exception as e: self._ctx.logger.error(f"Failed to connect to Airbnb MCP server: {e}") raise @@ -104,7 +103,8 @@ def _convert_mcp_tools_to_anthropic_format(self, mcp_tools): anthropic_tool = { "name": tool.name, "description": tool.description or f"Airbnb tool: {tool.name}", - "input_schema": tool.inputSchema or {"type": "object", "properties": {}} + "input_schema": tool.inputSchema + or {"type": "object", "properties": {}}, } anthropic_tools.append(anthropic_tool) return anthropic_tools @@ -117,36 +117,48 @@ async def process_query(self, query: str) -> str: response = self.anthropic.messages.create( model="claude-3-5-sonnet-20241022", max_tokens=4096, - messages=[{ - "role": "user", - "content": f"""Help me with this Airbnb request: {query} + messages=[ + { + "role": "user", + "content": f"""Help me with this Airbnb request: {query} Please use the available Airbnb tools to search for properties, get listing details, or provide accommodation assistance. -Make sure to provide helpful, accurate information about available properties and booking options.""" - }], +Make sure to provide helpful, accurate information about available properties and booking options.""", + } + ], tools=self.tools, ) - tool_use = next((content for content in response.content if content.type == 'tool_use'), None) + tool_use = next( + (content for content in response.content if content.type == "tool_use"), + None, + ) # If the model wants to use a tool if tool_use: tool_name = tool_use.name tool_input = tool_use.input - self._ctx.logger.info(f"Claude selected Airbnb tool: {tool_name} with input: {tool_input}") + self._ctx.logger.info( + f"Claude selected Airbnb tool: {tool_name} with input: {tool_input}" + ) # Call the selected tool on the Airbnb MCP server mcp_response = await self._session.call_tool(tool_name, tool_input) - + # Debug: Log the raw MCP response self._ctx.logger.info(f"MCP Response type: {type(mcp_response)}") - self._ctx.logger.info(f"MCP Response content type: {type(mcp_response.content)}") - + self._ctx.logger.info( + f"MCP Response content type: {type(mcp_response.content)}" + ) + # Format the response for the user return self.format_response(mcp_response.content) - + # If the model just wants to chat - text_response = next((content for content in response.content if content.type == 'text'), None) + text_response = next( + (content for content in response.content if content.type == "text"), + None, + ) if text_response: return text_response.text @@ -161,7 +173,7 @@ def format_response(self, content: Any) -> str: try: # Handle different response formats if isinstance(content, list): - if len(content) > 0 and hasattr(content[0], 'text'): + if len(content) > 0 and hasattr(content[0], "text"): # TextContent format raw_data = content[0].text elif len(content) > 0 and isinstance(content[0], dict): @@ -199,15 +211,15 @@ def _format_search_results(self, data: Dict) -> str: """Format search results based on actual Airbnb JSON structure""" search_results = data.get("searchResults", []) search_url = data.get("searchUrl", "") - + if not search_results: return "🏠 No Airbnb listings found for your search criteria." - + # Header with search URL formatted_response = f"🏠 **Found {len(search_results)} Airbnb Properties**\n\n" if search_url: formatted_response += f"🔗 [View all results on Airbnb]({search_url})\n\n" - + # Format top 5 results for i, result in enumerate(search_results[:5], 1): # Extract data from the complex structure @@ -216,35 +228,48 @@ def _format_search_results(self, data: Dict) -> str: rating = "" bed_info = "" badges = "" - + try: # Extract name - if "demandStayListing" in result and "description" in result["demandStayListing"]: - name_obj = result["demandStayListing"]["description"].get("name", {}) + if ( + "demandStayListing" in result + and "description" in result["demandStayListing"] + ): + name_obj = result["demandStayListing"]["description"].get( + "name", {} + ) if "localizedStringWithTranslationPreference" in name_obj: name = name_obj["localizedStringWithTranslationPreference"] - + # Extract price - if "structuredDisplayPrice" in result and "primaryLine" in result["structuredDisplayPrice"]: - price = result["structuredDisplayPrice"]["primaryLine"].get("accessibilityLabel", "") - + if ( + "structuredDisplayPrice" in result + and "primaryLine" in result["structuredDisplayPrice"] + ): + price = result["structuredDisplayPrice"]["primaryLine"].get( + "accessibilityLabel", "" + ) + # Extract rating if "avgRatingA11yLabel" in result: rating = result["avgRatingA11yLabel"] - + # Extract bed info - if "structuredContent" in result and "primaryLine" in result["structuredContent"]: + if ( + "structuredContent" in result + and "primaryLine" in result["structuredContent"] + ): bed_info = result["structuredContent"]["primaryLine"] - + # Extract badges badges = result.get("badges", "") - + # Get URL url = result.get("url", "") - + except (KeyError, TypeError) as e: self._ctx.logger.warning(f"Error extracting data from result {i}: {e}") - + # Format the listing formatted_response += f"**{i}. {name or 'Property Name Not Available'}**\n" if url: @@ -258,10 +283,12 @@ def _format_search_results(self, data: Dict) -> str: if badges and badges.strip(): formatted_response += f"🏆 {badges}\n" formatted_response += "\n" - + if len(search_results) > 5: - formatted_response += f"... and {len(search_results) - 5} more properties available.\n" - + formatted_response += ( + f"... and {len(search_results) - 5} more properties available.\n" + ) + return formatted_response def _format_airbnb_response(self, data: Dict) -> str: @@ -279,25 +306,27 @@ def _format_airbnb_listings(self, listings: list) -> str: """Format multiple Airbnb listings""" if not listings: return "🏠 No Airbnb listings found for your search criteria." - + formatted_response = f"🏠 **Found {len(listings)} Airbnb Properties**\n\n" - + for i, listing in enumerate(listings[:5], 1): # Limit to top 5 name = listing.get("name", "Property Name Not Available") price = listing.get("price", {}).get("total", "Price not available") location = listing.get("location", "Location not specified") rating = listing.get("rating", "No rating") listing_id = listing.get("id", "N/A") - + formatted_response += f"**{i}. {name}**\n" formatted_response += f"📍 Location: {location}\n" formatted_response += f"💰 Price: {price}\n" formatted_response += f"⭐ Rating: {rating}\n" formatted_response += f"🆔 ID: {listing_id}\n\n" - + if len(listings) > 5: - formatted_response += f"... and {len(listings) - 5} more properties available.\n" - + formatted_response += ( + f"... and {len(listings) - 5} more properties available.\n" + ) + return formatted_response def _format_single_listing(self, listing: Dict) -> str: @@ -309,21 +338,21 @@ def _format_single_listing(self, listing: Dict) -> str: rating = listing.get("rating", "No rating") amenities = listing.get("amenities", []) host = listing.get("host", {}).get("name", "Host information not available") - + formatted_response = f"🏠 **{name}**\n\n" formatted_response += f"📍 **Location:** {location}\n" formatted_response += f"💰 **Price:** {price}\n" formatted_response += f"⭐ **Rating:** {rating}\n" formatted_response += f"👤 **Host:** {host}\n\n" - + if description: formatted_response += f"📝 **Description:**\n{description[:300]}{'...' if len(description) > 300 else ''}\n\n" - + if amenities: formatted_response += f"🏠 **Amenities:** {', '.join(amenities[:10])}\n" if len(amenities) > 10: formatted_response += f"... and {len(amenities) - 10} more amenities\n" - + return formatted_response async def cleanup(self): @@ -335,6 +364,7 @@ async def cleanup(self): except Exception as e: self._ctx.logger.error(f"Error during Airbnb MCP cleanup: {e}") + # --- uAgent Setup --- chat_proto = Protocol(spec=chat_protocol_spec) @@ -343,23 +373,25 @@ async def cleanup(self): # Store MCP clients per session airbnb_clients: Dict[str, AirbnbMCPClient] = {} + def is_session_valid(session_id: str) -> bool: """Check if session is valid and hasn't expired""" if session_id not in user_sessions: return False - + session = user_sessions[session_id] current_time = time.time() - + if current_time - session["last_activity"] > SESSION_TIMEOUT: # Session expired del user_sessions[session_id] return False - + # Update last activity session["last_activity"] = current_time return True + async def get_airbnb_client(ctx: Context, session_id: str) -> AirbnbMCPClient: """Get or create Airbnb MCP client for session""" if session_id not in airbnb_clients: @@ -368,22 +400,23 @@ async def get_airbnb_client(ctx: Context, session_id: str) -> AirbnbMCPClient: airbnb_clients[session_id] = client return airbnb_clients[session_id] + @chat_proto.on_message(model=ChatMessage) async def handle_chat_message(ctx: Context, sender: str, msg: ChatMessage): """Handle incoming chat messages""" - + # Extract text from content (handle both list and direct text formats) try: if isinstance(msg.content, list): # Content is a list, extract text from first item if len(msg.content) > 0: - if hasattr(msg.content[0], 'text'): + if hasattr(msg.content[0], "text"): user_text = msg.content[0].text else: user_text = str(msg.content[0]) else: user_text = "[Empty message]" - elif hasattr(msg.content, 'text'): + elif hasattr(msg.content, "text"): # Content has direct text attribute user_text = msg.content.text else: @@ -392,49 +425,55 @@ async def handle_chat_message(ctx: Context, sender: str, msg: ChatMessage): except Exception as e: ctx.logger.error(f"Error extracting message text: {e}") user_text = "[Could not parse message]" - + ctx.logger.info(f"Received message from {sender}: '{user_text}'") - + # Extract or create session ID - session_id = getattr(msg, 'session_id', None) or str(uuid4()) - + session_id = getattr(msg, "session_id", None) or str(uuid4()) + # Validate session if not is_session_valid(session_id): user_sessions[session_id] = { "authenticated": True, - "last_activity": time.time() + "last_activity": time.time(), } - + try: # Get Airbnb MCP client for this session airbnb_client = await get_airbnb_client(ctx, session_id) - + # Process the query response_text = await airbnb_client.process_query(user_text) - + # Send response response_msg = ChatMessage( timestamp=datetime.now(timezone.utc), msg_id=str(uuid4()), - content=[TextContent(type="text", text=response_text)] + content=[TextContent(type="text", text=response_text)], ) - + await ctx.send(sender, response_msg) ctx.logger.info(f"Sent response to {sender}") - + except Exception as e: ctx.logger.error(f"Error handling message: {e}") error_msg = ChatMessage( timestamp=datetime.now(timezone.utc), msg_id=str(uuid4()), - content=[TextContent(type="text", text=f"Sorry, I encountered an error: {str(e)}")] + content=[ + TextContent( + type="text", text=f"Sorry, I encountered an error: {str(e)}" + ) + ], ) await ctx.send(sender, error_msg) + @chat_proto.on_message(model=ChatAcknowledgement) async def handle_chat_ack(ctx: Context, sender: str, msg: ChatAcknowledgement): ctx.logger.info(f"Received acknowledgement from {sender}") + @agent.on_event("shutdown") async def on_shutdown(ctx: Context): """Clean up resources on shutdown""" @@ -442,6 +481,7 @@ async def on_shutdown(ctx: Context): for client in airbnb_clients.values(): await client.cleanup() + agent.include(chat_proto) if __name__ == "__main__": diff --git a/mcp-agents/calendar_chat_uagent/__init__.py b/mcp-agents/calendar_chat_uagent/__init__.py index ec1167ca..0eaa8ae6 100644 --- a/mcp-agents/calendar_chat_uagent/__init__.py +++ b/mcp-agents/calendar_chat_uagent/__init__.py @@ -1 +1 @@ -# Calendar Chat uAgent package \ No newline at end of file +# Calendar Chat uAgent package diff --git a/mcp-agents/calendar_chat_uagent/calendar_chat_agent.py b/mcp-agents/calendar_chat_uagent/calendar_chat_agent.py index e7fa63f0..9087dffa 100644 --- a/mcp-agents/calendar_chat_uagent/calendar_chat_agent.py +++ b/mcp-agents/calendar_chat_uagent/calendar_chat_agent.py @@ -1,6 +1,9 @@ from __future__ import annotations -import os, sys, pathlib, json +import os +import sys +import pathlib +import json from dotenv import load_dotenv from uagents import Agent, Context, Model @@ -11,9 +14,14 @@ sys.path.insert(0, str(PARENT)) -from calendar_chat_proto import chat_proto -import json, pathlib -from calendar_chat_proto import (_run_cal_tool, CURRENT_SESSION_DATA, _create_msg, SESSIONS_KEY) +from calendar_chat_proto import chat_proto # noqa: E402 +import pathlib # noqa: E402 +from calendar_chat_proto import ( # noqa: E402 + _run_cal_tool, + CURRENT_SESSION_DATA, + _create_msg, + SESSIONS_KEY, +) class OAuthRequest(Model): @@ -47,16 +55,22 @@ async def _cb(ctx: Context, req: OAuthRequest) -> OAuthResponse: # Load existing sessions from storage try: sessions_raw = ctx.storage.get(SESSIONS_KEY) or "{}" - sessions = json.loads(sessions_raw) if isinstance(sessions_raw, str) else sessions_raw + sessions = ( + json.loads(sessions_raw) if isinstance(sessions_raw, str) else sessions_raw + ) except Exception: sessions = {} data = sessions.get(sid, {}) - ctx.logger.info("🌐 [REST] Session found=%s keys=%s", bool(sid in sessions), list(data.keys())) + ctx.logger.info( + "🌐 [REST] Session found=%s keys=%s", bool(sid in sessions), list(data.keys()) + ) # Ignore duplicate callbacks if already authenticated if data.get("cal_authenticated"): - ctx.logger.info("🌐 [REST] Session already authenticated – ignoring duplicate callback") + ctx.logger.info( + "🌐 [REST] Session already authenticated – ignoring duplicate callback" + ) return OAuthResponse(success=True, message="Already authenticated") # Prepare a unique token file for this session @@ -75,8 +89,13 @@ async def _cb(ctx: Context, req: OAuthRequest) -> OAuthResponse: out = await _run_cal_tool("complete_oauth", {"auth_code": code}) ctx.logger.info("🌐 [REST] complete_oauth raw response: %s", out[:200]) res = json.loads(out) - if not res.get("success") and "flow not initialised" in res.get("error", "").lower(): - ctx.logger.info("🌐 [REST] Flow missing – calling setup_oauth then retrying complete_oauth") + if ( + not res.get("success") + and "flow not initialised" in res.get("error", "").lower() + ): + ctx.logger.info( + "🌐 [REST] Flow missing – calling setup_oauth then retrying complete_oauth" + ) await _run_cal_tool("setup_oauth", {"session_id": sid}) out = await _run_cal_tool("complete_oauth", {"auth_code": code}) res = json.loads(out) @@ -96,7 +115,9 @@ async def _cb(ctx: Context, req: OAuthRequest) -> OAuthResponse: # both the JSON and the *canonical* path in the session so that # _run_cal_tool can later rebuild temp files if needed. - from server import TOKENS_PATH as CANONICAL_TOKENS_PATH # local import to avoid cycles + from server import ( + TOKENS_PATH as CANONICAL_TOKENS_PATH, + ) # local import to avoid cycles try: with open(CANONICAL_TOKENS_PATH, "r", encoding="utf-8") as cf: @@ -109,9 +130,15 @@ async def _cb(ctx: Context, req: OAuthRequest) -> OAuthResponse: tpath.unlink() except Exception: pass - ctx.logger.info("🌐 [REST] Token JSON loaded from canonical path %s", CANONICAL_TOKENS_PATH) + ctx.logger.info( + "🌐 [REST] Token JSON loaded from canonical path %s", CANONICAL_TOKENS_PATH + ) except Exception as f_err: - ctx.logger.warning("🌐 [REST] Failed to load canonical token file %s: %s", CANONICAL_TOKENS_PATH, f_err) + ctx.logger.warning( + "🌐 [REST] Failed to load canonical token file %s: %s", + CANONICAL_TOKENS_PATH, + f_err, + ) data["cal_authenticated"] = True data.pop("awaiting_auth_code", None) @@ -124,7 +151,12 @@ async def _cb(ctx: Context, req: OAuthRequest) -> OAuthResponse: # Notify the chat user if we know their address if addr := data.get("sender_address"): ctx.logger.info("🌐 [REST] Notifying sender %s", addr) - await ctx.send(addr, _create_msg("✅ Calendar authorisation successful! You can now ask me about your calendar.")) + await ctx.send( + addr, + _create_msg( + "✅ Calendar authorisation successful! You can now ask me about your calendar." + ), + ) return OAuthResponse(success=True, message="OAuth completed and tokens stored") @@ -140,4 +172,4 @@ async def _stop(ctx: Context): if __name__ == "__main__": - agent.run() \ No newline at end of file + agent.run() diff --git a/mcp-agents/calendar_chat_uagent/calendar_chat_proto.py b/mcp-agents/calendar_chat_uagent/calendar_chat_proto.py index 5aa7dbec..4cb1d5ee 100644 --- a/mcp-agents/calendar_chat_uagent/calendar_chat_proto.py +++ b/mcp-agents/calendar_chat_uagent/calendar_chat_proto.py @@ -6,14 +6,19 @@ FastMCP tools defined in `calendar_chat_uagent.server`. """ -import asyncio, contextvars, json, logging, os, pathlib, sys -from datetime import datetime, timezone -from typing import Any, Dict, List -from uuid import uuid4 - -import httpx -from uagents import Context, Protocol -from uagents_core.contrib.protocols.chat import ( +import asyncio # noqa: E402 +import contextvars # noqa: E402 +import json # noqa: E402 +import logging # noqa: E402 +import os # noqa: E402 +import pathlib # noqa: E402 +from datetime import datetime, timezone # noqa: E402 +from typing import Any, Dict, List # noqa: E402 +from uuid import uuid4 # noqa: E402 + +import httpx # noqa: E402 +from uagents import Context, Protocol # noqa: E402 +from uagents_core.contrib.protocols.chat import ( # noqa: E402 ChatAcknowledgement, ChatMessage, EndSessionContent, @@ -21,7 +26,7 @@ TextContent, chat_protocol_spec, ) -from dotenv import load_dotenv +from dotenv import load_dotenv # noqa: E402 load_dotenv() @@ -38,11 +43,14 @@ MAX_HISTORY = int(os.getenv("MAX_CAL_CHAT_HISTORY", "10")) # Import FastMCP server -import server as cal_server +import server as cal_server # noqa: E402 + cal_mcp = cal_server.mcp cal_auth = cal_server.calendar_auth -CURRENT_SESSION_DATA: contextvars.ContextVar[dict] = contextvars.ContextVar("CURRENT_SESSION_DATA") +CURRENT_SESSION_DATA: contextvars.ContextVar[dict] = contextvars.ContextVar( + "CURRENT_SESSION_DATA" +) # --------------------------------------------------------------------------- # Helper to normalise FastMCP return values (copied from Gmail proto) @@ -61,6 +69,7 @@ def _unwrap(result: Any) -> str: return json.dumps(result) return str(result) + # --------------------------------------------------------------------------- # OpenAI *Responses* streaming helper – executes tool calls when present # --------------------------------------------------------------------------- @@ -69,7 +78,9 @@ def _unwrap(result: Any) -> str: async def _call_openai_responses(messages: List[Dict[str, str]]) -> str: """Stream /v1/responses events, execute MCP tool calls, and return final text.""" - transcript = "\n".join(f"{m['role'].capitalize()}: {m['content']}" for m in messages) + transcript = "\n".join( + f"{m['role'].capitalize()}: {m['content']}" for m in messages + ) TOOLS_BLOCK = [ { @@ -103,7 +114,12 @@ async def _call_openai_responses(messages: List[Dict[str, str]]) -> str: follow_payload = dict(payload) for _hop in range(10): # limit hops to prevent loops - async with client.stream("POST", "https://api.openai.com/v1/responses", json=follow_payload, headers=headers) as stream: + async with client.stream( + "POST", + "https://api.openai.com/v1/responses", + json=follow_payload, + headers=headers, + ) as stream: async for raw in stream.aiter_lines(): if not raw.startswith("data:"): continue @@ -117,7 +133,11 @@ async def _call_openai_responses(messages: List[Dict[str, str]]) -> str: kind = event.get("type") - if kind in ("content_block_delta", "message_delta", "response.output_text.delta"): + if kind in ( + "content_block_delta", + "message_delta", + "response.output_text.delta", + ): delta = event.get("delta", {}) if isinstance(delta, dict): assistant_chunks.append(delta.get("text", "")) @@ -132,7 +152,9 @@ async def _call_openai_responses(messages: List[Dict[str, str]]) -> str: args_json = call["function"].get("arguments") or "{}" args_dict = json.loads(args_json) out = await _run_cal_tool(fn, args_dict) - tool_outputs.append({"tool_call_id": call["id"], "output": out}) + tool_outputs.append( + {"tool_call_id": call["id"], "output": out} + ) follow_payload = { "model": OPENAI_MODEL, @@ -148,6 +170,7 @@ async def _call_openai_responses(messages: List[Dict[str, str]]) -> str: # Fallback – no response return "(no response)" + async def _run_cal_tool(fn: str, args: Dict[str, Any]) -> str: """Execute *fn* with *args* against Calendar MCP, with retry and temp-token file handling.""" @@ -212,14 +235,15 @@ async def _call_once(): # noqa: D401 pass return json.dumps({"success": False, "error": "Unknown tool failure"}) + _SYSTEM_PROMPT = ( "You are an AI assistant for Google Calendar and may only operate via the MCP\n" "tools provided: setup_oauth, complete_oauth, list_calendars, list_events,\n" "create_event, update_event, delete_event, search_events, get_free_busy,\n" "get_current_time.\n\n" "• list_events is the main tool for a user's agenda. Always call it with\n" - " { calendar_id: \"primary\", date: \"today\"|\"tomorrow\"|\"YYYY-MM-DD\" }.\n" - " If the user doesn't specify a date use \"today\" by default.\n" + ' { calendar_id: "primary", date: "today"|"tomorrow"|"YYYY-MM-DD" }.\n' + ' If the user doesn\'t specify a date use "today" by default.\n' " When replying, show each meeting's title, time range and its link field so\n" " the user can open the Calendar event.\n" "• search_events is only for keyword searches across events.\n" @@ -242,12 +266,19 @@ def _create_msg(text: str, end: bool = False) -> ChatMessage: content: List[Any] = [TextContent(type="text", text=text)] if end: content.append(EndSessionContent(type="end-session")) - return ChatMessage(timestamp=datetime.now(timezone.utc), msg_id=uuid4(), content=content) + return ChatMessage( + timestamp=datetime.now(timezone.utc), msg_id=uuid4(), content=content + ) @chat_proto.on_message(ChatMessage) async def _handle(ctx: Context, sender: str, msg: ChatMessage): - await ctx.send(sender, ChatAcknowledgement(timestamp=datetime.now(timezone.utc), acknowledged_msg_id=msg.msg_id)) + await ctx.send( + sender, + ChatAcknowledgement( + timestamp=datetime.now(timezone.utc), acknowledged_msg_id=msg.msg_id + ), + ) sessions = json.loads(ctx.storage.get(SESSIONS_KEY) or "{}") sid = str(ctx.session) data = sessions.get(sid, {"messages": []}) @@ -269,7 +300,9 @@ async def _handle(ctx: Context, sender: str, msg: ChatMessage): # not already waiting for an OAuth code – prevents sending the auth link # right after the session starts. if not inbound and not data.get("awaiting_auth_code"): - ctx.logger.info("🔕 StartSessionContent only – waiting for user input before responding") + ctx.logger.info( + "🔕 StartSessionContent only – waiting for user input before responding" + ) sessions[sid] = data ctx.storage.set(SESSIONS_KEY, json.dumps(sessions)) return @@ -310,10 +343,12 @@ async def _handle(ctx: Context, sender: str, msg: ChatMessage): # 1. Handle authentication workflow (setup_oauth / complete_oauth) # ------------------------------------------------------------------ - ctx.logger.info("🔍 Auth status: authenticated=%s, awaiting_code=%s, inbound_text=%s", - data.get("cal_authenticated"), - data.get("awaiting_auth_code"), - bool(inbound)) + ctx.logger.info( + "🔍 Auth status: authenticated=%s, awaiting_code=%s, inbound_text=%s", + data.get("cal_authenticated"), + data.get("awaiting_auth_code"), + bool(inbound), + ) if not data.get("cal_authenticated"): # Guard against duplicate sends @@ -323,7 +358,12 @@ async def _handle(ctx: Context, sender: str, msg: ChatMessage): # If awaiting OAuth completion, gently remind the user if data.get("awaiting_auth_code"): if inbound: - await ctx.send(sender, _create_msg("🔄 Still waiting for you to grant access in the browser popup. Once finished, just ask me your question again.")) + await ctx.send( + sender, + _create_msg( + "🔄 Still waiting for you to grant access in the browser popup. Once finished, just ask me your question again." + ), + ) ctx.logger.debug("Waiting for OAuth completion; inbound=%s", inbound) sessions[sid] = data ctx.storage.set(SESSIONS_KEY, json.dumps(sessions)) @@ -337,7 +377,9 @@ async def _handle(ctx: Context, sender: str, msg: ChatMessage): if not oauth_data.get("success"): err_msg = oauth_data.get("error", "Unknown error during OAuth setup") - await ctx.send(sender, _create_msg(f"❌ Authentication setup failed: {err_msg}")) + await ctx.send( + sender, _create_msg(f"❌ Authentication setup failed: {err_msg}") + ) ctx.logger.error("❌ OAuth setup failed: %s", err_msg) return @@ -345,9 +387,12 @@ async def _handle(ctx: Context, sender: str, msg: ChatMessage): data["awaiting_auth_code"] = True data["oauth_link_sent"] = True - await ctx.send(sender, _create_msg( - f"Click the **Authorize Calendar** link below.\n\n[Authorize Calendar]({auth_url})\n\nOnce authorised, paste the code here or just ask your question again." - )) + await ctx.send( + sender, + _create_msg( + f"Click the **Authorize Calendar** link below.\n\n[Authorize Calendar]({auth_url})\n\nOnce authorised, paste the code here or just ask your question again." + ), + ) ctx.logger.info("➡️ Auth link sent: %s", auth_url) sessions[sid] = data @@ -357,13 +402,18 @@ async def _handle(ctx: Context, sender: str, msg: ChatMessage): # After authentication, remove old OAuth-instruction messages to prevent # the model from repeating them. if data.get("cal_authenticated") and data.get("messages"): + def _looks_like_oauth_prompt(text: str) -> bool: t = text.lower() return "authorize" in t and "calendar" in t or "authentication" in t filtered = [ - m for m in data["messages"] - if not (m.get("source") == "assistant" and _looks_like_oauth_prompt(m.get("content", ""))) + m + for m in data["messages"] + if not ( + m.get("source") == "assistant" + and _looks_like_oauth_prompt(m.get("content", "")) + ) ] if len(filtered) != len(data["messages"]): data["messages"] = filtered @@ -374,12 +424,16 @@ def _looks_like_oauth_prompt(text: str) -> bool: messages: List[Dict[str, str]] = [{"role": "system", "content": _SYSTEM_PROMPT}] # Add authenticated note immediately so it has high priority if data.get("cal_authenticated"): - messages.append({ - "role": "system", - "content": "The user is already authenticated with valid Google Calendar tokens. Use the calendar tools directly; do NOT request OAuth again."}) + messages.append( + { + "role": "system", + "content": "The user is already authenticated with valid Google Calendar tokens. Use the calendar tools directly; do NOT request OAuth again.", + } + ) messages += [ - {"role": m.get("source", "assistant"), "content": m.get("content", "")} for m in data.get("messages", []) + {"role": m.get("source", "assistant"), "content": m.get("content", "")} + for m in data.get("messages", []) ] if inbound: @@ -394,7 +448,12 @@ def _looks_like_oauth_prompt(text: str) -> bool: answer = await _call_openai_responses(messages) data["messages"].append({"source": "assistant", "content": answer}) await ctx.send(sender, _create_msg(answer)) - ctx.logger.info(">> session=%s to=%s content=%s", sid, sender, answer[:120].replace("\n", " ")) + ctx.logger.info( + ">> session=%s to=%s content=%s", + sid, + sender, + answer[:120].replace("\n", " "), + ) sessions[sid] = data ctx.storage.set(SESSIONS_KEY, json.dumps(sessions)) finally: @@ -403,4 +462,4 @@ def _looks_like_oauth_prompt(text: str) -> bool: @chat_proto.on_message(ChatAcknowledgement) async def _ack(ctx: Context, sender: str, msg: ChatAcknowledgement): - pass \ No newline at end of file + pass diff --git a/mcp-agents/calendar_chat_uagent/oauth_server.py b/mcp-agents/calendar_chat_uagent/oauth_server.py index 6bf25ddb..c9948640 100644 --- a/mcp-agents/calendar_chat_uagent/oauth_server.py +++ b/mcp-agents/calendar_chat_uagent/oauth_server.py @@ -29,13 +29,17 @@ AGENTVERSE_API_KEY = os.getenv("AGENTVERSE_API_KEY") STORAGE_URL = os.getenv("AGENTVERSE_URL", "https://agentverse.ai") + "/v1/storage" -print(f"🔑 API Key loaded: {AGENTVERSE_API_KEY[:20] + '…' if AGENTVERSE_API_KEY else 'None'}") +print( + f"🔑 API Key loaded: {AGENTVERSE_API_KEY[:20] + '…' if AGENTVERSE_API_KEY else 'None'}" +) print(f"🌐 Storage URL: {STORAGE_URL}") _storage: ExternalStorage | None = None if AGENTVERSE_API_KEY: try: - _storage = ExternalStorage(api_token=AGENTVERSE_API_KEY, storage_url=STORAGE_URL) + _storage = ExternalStorage( + api_token=AGENTVERSE_API_KEY, storage_url=STORAGE_URL + ) print("🔐 ExternalStorage initialised successfully") except Exception as err: print(f"⚠️ ExternalStorage init failed: {err}") @@ -47,6 +51,7 @@ # Helper for persisting the OAuth code in the agent's local storage JSON file # --------------------------------------------------------------------------- + def save_oauth_code_to_agent_storage(session_id: str, auth_code: str) -> bool: """Save OAuth code into the agent*_data.json under `calendar_chat_sessions`.""" try: @@ -62,7 +67,9 @@ def save_oauth_code_to_agent_storage(session_id: str, auth_code: str) -> bool: # calendar_chat_sessions is a JSON-serialised string in the storage JSON sessions_str = storage_data.get("calendar_chat_sessions", "{}") - sessions = json.loads(sessions_str) if isinstance(sessions_str, str) else sessions_str + sessions = ( + json.loads(sessions_str) if isinstance(sessions_str, str) else sessions_str + ) if session_id in sessions: sessions[session_id]["oauth_code"] = auth_code @@ -81,6 +88,7 @@ def save_oauth_code_to_agent_storage(session_id: str, auth_code: str) -> bool: print(f"⚠️ Failed to save OAuth code to agent storage: {e}") return False + # --------------------------------------------------------------------------- # HTML pages returned to the browser # --------------------------------------------------------------------------- @@ -118,6 +126,7 @@ def save_oauth_code_to_agent_storage(session_id: str, auth_code: str) -> bool: # HTTP handler # --------------------------------------------------------------------------- + class OAuthCallbackHandler(http.server.SimpleHTTPRequestHandler): def do_GET(self): # noqa: N802 parsed = urllib.parse.urlparse(self.path) @@ -129,13 +138,17 @@ def do_GET(self): # noqa: N802 self.send_header("Content-type", "text/html") self.end_headers() self.wfile.write(SUCCESS_PAGE_TEMPLATE.encode("utf-8")) - print(f"\n🎉 Authorization code received for session {state}: {code[:10]}…\n") + print( + f"\n🎉 Authorization code received for session {state}: {code[:10]}…\n" + ) # 1️⃣ Upload to Agentverse asset storage stored_externally = False if _storage and state: try: - _storage.create_asset(name=state, content=code.encode(), mime_type="text/plain") + _storage.create_asset( + name=state, content=code.encode(), mime_type="text/plain" + ) print("💾 Code uploaded to Agentverse asset", state) stored_externally = True except Exception as up_err: @@ -148,15 +161,20 @@ def do_GET(self): # noqa: N802 try: with open(f"calendar_oauth_code_{state}.txt", "w") as f: f.write(code) - print(f"💾 Code saved locally to calendar_oauth_code_{state}.txt as final fallback") + print( + f"💾 Code saved locally to calendar_oauth_code_{state}.txt as final fallback" + ) except Exception as local_err: print("⚠️ Failed to save code locally:", local_err) # Notify the calendar_chat_agent REST endpoint so it can finish the flow automatically try: import requests as _req + payload = {"session_id": state, "auth_code": code} - resp = _req.post("http://localhost:8089/oauth/callback", json=payload, timeout=3) + resp = _req.post( + "http://localhost:8089/oauth/callback", json=payload, timeout=3 + ) print("➡️ Posted code to chat agent – status", resp.status_code) except Exception as notify_err: print("⚠️ Could not notify chat agent:", notify_err) @@ -170,10 +188,12 @@ def log_message(self, fmt, *args): # noqa: N802 # Suppress the default noisy HTTP request logs from http.server pass + # --------------------------------------------------------------------------- # Server entrypoint # --------------------------------------------------------------------------- + def start_oauth_server(): port = 8080 with socketserver.TCPServer(("", port), OAuthCallbackHandler) as httpd: @@ -184,5 +204,6 @@ def start_oauth_server(): except KeyboardInterrupt: print("\n🛑 OAuth server stopped") + if __name__ == "__main__": - start_oauth_server() \ No newline at end of file + start_oauth_server() diff --git a/mcp-agents/calendar_chat_uagent/server.py b/mcp-agents/calendar_chat_uagent/server.py index 6c2ca99c..172ad355 100644 --- a/mcp-agents/calendar_chat_uagent/server.py +++ b/mcp-agents/calendar_chat_uagent/server.py @@ -3,7 +3,9 @@ from __future__ import annotations -import os, json, logging +import os +import json +import logging from dotenv import load_dotenv from fastmcp import FastMCP from google.oauth2.credentials import Credentials @@ -11,7 +13,6 @@ from google.auth.transport.requests import Request as GoogleRequest from googleapiclient.discovery import build from datetime import datetime, timezone, timedelta -from typing import Any, Dict, List load_dotenv() @@ -65,14 +66,18 @@ def exchange_code_for_token(self, code: str): self._flow.fetch_token(code=code) creds = self._flow.credentials with open(self.tokens_path, "w", encoding="utf-8") as f: - json.dump({ - "token": creds.token, - "refresh_token": creds.refresh_token, - "token_uri": creds.token_uri, - "client_id": creds.client_id, - "client_secret": creds.client_secret, - "scopes": creds.scopes, - }, f, indent=2) + json.dump( + { + "token": creds.token, + "refresh_token": creds.refresh_token, + "token_uri": creds.token_uri, + "client_id": creds.client_id, + "client_secret": creds.client_secret, + "scopes": creds.scopes, + }, + f, + indent=2, + ) def service(self): if self._service: @@ -134,6 +139,7 @@ def list_calendars() -> str: except Exception as e: return json.dumps({"success": False, "error": str(e)}) + # --------------------------------------------------------------------------- # New tool: list_events (primary calendar by default, date or range support) # --------------------------------------------------------------------------- @@ -147,7 +153,9 @@ def _parse_date_span(date: str) -> tuple[str, str]: if date in ("today", ""): # default start = now.replace(hour=0, minute=0, second=0, microsecond=0) elif date == "tomorrow": - start = (now + timedelta(days=1)).replace(hour=0, minute=0, second=0, microsecond=0) + start = (now + timedelta(days=1)).replace( + hour=0, minute=0, second=0, microsecond=0 + ) else: try: # Expect YYYY-MM-DD format @@ -165,7 +173,9 @@ def _parse_date_span(date: str) -> tuple[str, str]: @mcp.tool() -def list_events(calendar_id: str = "primary", date: str = "", max_results: int = 20) -> str: +def list_events( + calendar_id: str = "primary", date: str = "", max_results: int = 20 +) -> str: """List events for a given date (default today) in the specified calendar. Args: @@ -174,7 +184,12 @@ def list_events(calendar_id: str = "primary", date: str = "", max_results: int = max_results: Limit number of events returned (default 20). """ try: - logger.info("🛠️ list_events called: calendar_id=%s date=%s max_results=%s", calendar_id, date or "today", max_results) + logger.info( + "🛠️ list_events called: calendar_id=%s date=%s max_results=%s", + calendar_id, + date or "today", + max_results, + ) service = calendar_auth.service() time_min, time_max = _parse_date_span(date) @@ -216,6 +231,7 @@ def list_events(calendar_id: str = "primary", date: str = "", max_results: int = logger.error("❌ list_events failed: %s", e) return json.dumps({"success": False, "error": str(e)}) + # --------------------------------------------------------------------------- # Additional tools: create, update, delete, search events and free/busy # --------------------------------------------------------------------------- @@ -357,14 +373,16 @@ def get_free_busy( @mcp.tool() def get_current_time() -> str: now = datetime.now(timezone.utc) - return json.dumps({ - "success": True, - "now": now.isoformat(), - "date": now.strftime("%Y-%m-%d"), - "time": now.strftime("%H:%M:%S"), - }) + return json.dumps( + { + "success": True, + "now": now.isoformat(), + "date": now.strftime("%Y-%m-%d"), + "time": now.strftime("%H:%M:%S"), + } + ) if __name__ == "__main__": port = int(os.getenv("CALENDAR_MCP_PORT", "8081")) - mcp.run("sse", host="0.0.0.0", port=port) \ No newline at end of file + mcp.run("sse", host="0.0.0.0", port=port) diff --git a/mcp-agents/events-finder-mcp-agent/agent.py b/mcp-agents/events-finder-mcp-agent/agent.py index 7167a5c3..f914cb1d 100644 --- a/mcp-agents/events-finder-mcp-agent/agent.py +++ b/mcp-agents/events-finder-mcp-agent/agent.py @@ -32,17 +32,17 @@ - Location + Event Type: "San Francisco concert", "New York music" - Be specific: Include city names in keywords - Don't rely on countryCode alone for location filtering -""" +""", ) agent = Agent( name="events-finder-mcp-agent", seed="events-finder-mcp-agent", port=8000, - mailbox=True + mailbox=True, ) for protocol in mcp_adapter.protocols: agent.include(protocol, publish_manifest=True) if __name__ == "__main__": - mcp_adapter.run(agent) \ No newline at end of file + mcp_adapter.run(agent) diff --git a/mcp-agents/events-finder-mcp-agent/new-adapter.py b/mcp-agents/events-finder-mcp-agent/new-adapter.py index 4e9582f2..a965ce87 100644 --- a/mcp-agents/events-finder-mcp-agent/new-adapter.py +++ b/mcp-agents/events-finder-mcp-agent/new-adapter.py @@ -4,7 +4,7 @@ import logging import threading from datetime import datetime, timezone -from typing import Any, Dict, List +from typing import Any, Dict from uuid import uuid4 import requests @@ -144,29 +144,43 @@ async def handle_chat_message(ctx: Context, sender: str, msg: ChatMessage): ctx.logger.info(f"[BEFORE CLEANING] Raw user message: {item.text}") cleaned_text = item.text if "[Additional Context]" in cleaned_text: - cleaned_text = cleaned_text.split("[Additional Context]")[0].strip() + cleaned_text = cleaned_text.split("[Additional Context]")[ + 0 + ].strip() import re - cleaned_text = re.sub(r'.*?', '', cleaned_text, flags=re.DOTALL) - cleaned_text = re.sub(r'<.*?>', '', cleaned_text) # Remove any remaining XML tags + + cleaned_text = re.sub( + r".*?", + "", + cleaned_text, + flags=re.DOTALL, + ) + cleaned_text = re.sub( + r"<.*?>", "", cleaned_text + ) # Remove any remaining XML tags cleaned_text = cleaned_text.strip() - ctx.logger.info(f"[AFTER CLEANING] Cleaned user message: {cleaned_text}") + ctx.logger.info( + f"[AFTER CLEANING] Cleaned user message: {cleaned_text}" + ) try: session_id = str(ctx.session) - + # Load session data (new format only) session_data = {} try: session_serialized = ctx.storage.get(session_id) if session_serialized: session_data = deserialize_session(session_serialized) - ctx.logger.info(f"[SESSION] Loaded session data") + ctx.logger.info("[SESSION] Loaded session data") except Exception as e: ctx.logger.error(f"Error loading session: {str(e)}") session_data = {} - + # Extract messages from session data messages = session_data.get("messages", []) - ctx.logger.info(f"[CHAT HISTORY] Loaded {len(messages)} messages from storage") + ctx.logger.info( + f"[CHAT HISTORY] Loaded {len(messages)} messages from storage" + ) system_prompt_template = { "role": "system", @@ -180,7 +194,7 @@ async def handle_chat_message(ctx: Context, sender: str, msg: ChatMessage): === Developer additions === {self.system_prompt or ""} === End additions === - """ + """, } messages = [m for m in messages if m.get("role") != "system"] @@ -189,7 +203,9 @@ async def handle_chat_message(ctx: Context, sender: str, msg: ChatMessage): user_message = {"role": "user", "content": cleaned_text} messages.append(user_message) - ctx.logger.info(f"[CHAT HISTORY] Added user message, total messages: {len(messages)}") + ctx.logger.info( + f"[CHAT HISTORY] Added user message, total messages: {len(messages)}" + ) # Only include full history if self.include_history is True if not self.include_history: @@ -199,7 +215,7 @@ async def handle_chat_message(ctx: Context, sender: str, msg: ChatMessage): # Get tools from session or fetch from MCP server if not available available_tools = [] tools_list = session_data.get("tools_list", []) - + if tools_list: # Use tools from session available_tools = [ @@ -213,7 +229,9 @@ async def handle_chat_message(ctx: Context, sender: str, msg: ChatMessage): } for tool in tools_list ] - ctx.logger.info(f"[TOOLS] Using cached tools from session: {len(tools_list)} tools") + ctx.logger.info( + f"[TOOLS] Using cached tools from session: {len(tools_list)} tools" + ) else: # Fetch tools from MCP server (for new sessions) try: @@ -226,7 +244,7 @@ async def handle_chat_message(ctx: Context, sender: str, msg: ChatMessage): } for tool in tools ] - + available_tools = [ { "type": "function", @@ -238,14 +256,18 @@ async def handle_chat_message(ctx: Context, sender: str, msg: ChatMessage): } for tool in tools ] - ctx.logger.info(f"[TOOLS] Fetched fresh tools from MCP server: {len(tools_list)} tools") + ctx.logger.info( + f"[TOOLS] Fetched fresh tools from MCP server: {len(tools_list)} tools" + ) except Exception as e: ctx.logger.error( f"Error: Failed to retrieve tools from MCP Server: {str(e)}" ) available_tools = [] - ctx.logger.info(f"Available tools: {len(available_tools)} tools") + ctx.logger.info( + f"Available tools: {len(available_tools)} tools" + ) payload = { "model": self.model, @@ -269,7 +291,7 @@ async def handle_chat_message(ctx: Context, sender: str, msg: ChatMessage): json=payload, ) response_json = response.json() - + except Exception as e: ctx.logger.error(f"Error calling ASI1 API: {str(e)}") error_msg = ( @@ -286,8 +308,6 @@ async def handle_chat_message(ctx: Context, sender: str, msg: ChatMessage): ) continue - - if response_json.get("choices"): assistant_message = response_json["choices"][0]["message"] @@ -303,7 +323,9 @@ async def handle_chat_message(ctx: Context, sender: str, msg: ChatMessage): messages.append(assistant_msg) - ctx.logger.info(f"[CHAT HISTORY] After adding assistant message: {json.dumps(messages, indent=2)}") + ctx.logger.info( + f"[CHAT HISTORY] After adding assistant message: {json.dumps(messages, indent=2)}" + ) if assistant_message.get("tool_calls"): for tool_call in assistant_message["tool_calls"]: @@ -341,17 +363,28 @@ async def handle_chat_message(ctx: Context, sender: str, msg: ChatMessage): response_text = "\n".join( str(r) for r in tool_results ) - + # Check if response has the problematic format - if response_text.startswith("type='text' text="): - ctx.logger.warning("Detected MCP response format issue - contains type annotations") + if response_text.startswith( + "type='text' text=" + ): + ctx.logger.warning( + "Detected MCP response format issue - contains type annotations" + ) # Clean up the format import re - match = re.search(r"type='text' text=\"(.*)\" annotations=", response_text, re.DOTALL) + + match = re.search( + r"type='text' text=\"(.*)\" annotations=", + response_text, + re.DOTALL, + ) if match: - cleaned_text = match.group(1).replace('\\n', '\n') + cleaned_text = match.group(1).replace( + "\\n", "\n" + ) response_text = cleaned_text - + ctx.logger.info( f"Tool '{selected_tool}' response: {response_text}" ) @@ -376,8 +409,10 @@ async def handle_chat_message(ctx: Context, sender: str, msg: ChatMessage): } ) - ctx.logger.info(f"[CHAT HISTORY] After tool call, total messages: {len(messages)}") - + ctx.logger.info( + f"[CHAT HISTORY] After tool call, total messages: {len(messages)}" + ) + # Get final response after tool calls try: follow_up_payload = { @@ -391,7 +426,9 @@ async def handle_chat_message(ctx: Context, sender: str, msg: ChatMessage): headers=headers, json=follow_up_payload, ) - ctx.logger.info(f"[FINAL LLM CALL] Response status: {follow_up_response.status_code}") + ctx.logger.info( + f"[FINAL LLM CALL] Response status: {follow_up_response.status_code}" + ) follow_up_json = follow_up_response.json() final_response = ( @@ -402,12 +439,13 @@ async def handle_chat_message(ctx: Context, sender: str, msg: ChatMessage): "I've processed your request. Let me know if you need anything else!", ) ) - ctx.logger.info(f"[FINAL LLM CALL] Final response extracted: {final_response}") + ctx.logger.info( + f"[FINAL LLM CALL] Final response extracted: {final_response}" + ) # Append the final LLM response to the chat history - messages.append({ - "role": "assistant", - "content": final_response - }) + messages.append( + {"role": "assistant", "content": final_response} + ) except Exception as e: ctx.logger.error( f"Error getting final response from ASI1: {str(e)}" @@ -438,15 +476,15 @@ async def handle_chat_message(ctx: Context, sender: str, msg: ChatMessage): session_data = { "tools_list": tools_list, "messages": [], - "sender": sender + "sender": sender, } - + session_data["messages"] = messages session_data["sender"] = sender - + # Save the complete session data ctx.storage.set(session_id, serialize_session(session_data)) - ctx.logger.info(f"[SESSION] Saved session data to storage") + ctx.logger.info("[SESSION] Saved session data to storage") except Exception as e: ctx.logger.error(f"Error saving session data: {str(e)}") @@ -473,10 +511,14 @@ async def handle_chat_message(ctx: Context, sender: str, msg: ChatMessage): content=[TextContent(type="text", text=error_msg)], ), ) - elif hasattr(item, 'type') and item.type == 'metadata': - ctx.logger.info(f"Got metadata from {sender}: {getattr(item, 'metadata', {})}") + elif hasattr(item, "type") and item.type == "metadata": + ctx.logger.info( + f"Got metadata from {sender}: {getattr(item, 'metadata', {})}" + ) else: - ctx.logger.info(f"Got unexpected content from {sender}: {type(item)}") + ctx.logger.info( + f"Got unexpected content from {sender}: {type(item)}" + ) @self.chat_proto.on_message(model=ChatAcknowledgement) async def handle_ack(ctx: Context, sender: str, msg: ChatAcknowledgement): diff --git a/mcp-agents/events-finder-mcp-agent/server.py b/mcp-agents/events-finder-mcp-agent/server.py index 482c7b33..ebcc2422 100644 --- a/mcp-agents/events-finder-mcp-agent/server.py +++ b/mcp-agents/events-finder-mcp-agent/server.py @@ -16,10 +16,14 @@ TICKETMASTER_API_KEY = os.getenv("TICKETMASTER_API_KEY") TICKETMASTER_API_URL = "https://app.ticketmaster.com/discovery/v2" + def _clean_params(params: Dict[str, Any]) -> Dict[str, Any]: return {k: v for k, v in params.items() if v is not None} -async def ticketmaster_get(path: str, params: Dict[str, Any]) -> Optional[Dict[str, Any]]: + +async def ticketmaster_get( + path: str, params: Dict[str, Any] +) -> Optional[Dict[str, Any]]: params = _clean_params(params) params["apikey"] = TICKETMASTER_API_KEY url = f"{TICKETMASTER_API_URL}{path}" @@ -31,6 +35,7 @@ async def ticketmaster_get(path: str, params: Dict[str, Any]) -> Optional[Dict[s except Exception: return None + @mcp.tool() async def search_events( keyword: Optional[str] = None, @@ -58,9 +63,12 @@ async def search_events( lines = [] for idx, event in enumerate(events, 1): venue = event.get("_embedded", {}).get("venues", [{}])[0] - lines.append(f"{idx}. {event.get('name')}\n Date: {event.get('dates', {}).get('start', {}).get('dateTime', 'N/A')}\n Venue: {venue.get('name', 'N/A')}\n Event ID: {event.get('id')}\n More info: {event.get('url', 'N/A')}\n") + lines.append( + f"{idx}. {event.get('name')}\n Date: {event.get('dates', {}).get('start', {}).get('dateTime', 'N/A')}\n Venue: {venue.get('name', 'N/A')}\n Event ID: {event.get('id')}\n More info: {event.get('url', 'N/A')}\n" + ) return "\n".join(lines) + @mcp.tool() async def get_event_details( id: str, @@ -74,9 +82,13 @@ async def get_event_details( return "Event not found." venue = data.get("_embedded", {}).get("venues", [{}])[0] price = data.get("priceRanges", [{}])[0] - genres = ", ".join([ - c.get("genre", {}).get("name", "") for c in data.get("classifications", []) if c.get("genre") - ]) + genres = ", ".join( + [ + c.get("genre", {}).get("name", "") + for c in data.get("classifications", []) + if c.get("genre") + ] + ) return ( f"Event ID: {data.get('id')}\n" f"Name: {data.get('name')}\n" @@ -89,6 +101,7 @@ async def get_event_details( f"Description: {data.get('info', '') or data.get('pleaseNote', '') or 'N/A'}" ) + @mcp.tool() async def search_venues( keyword: Optional[str] = None, @@ -117,9 +130,11 @@ async def search_venues( for idx, venue in enumerate(venues, 1): address = venue.get("address", {}).get("line1", "N/A") city = venue.get("city", {}).get("name", "N/A") - lines.append(f"{idx}. {venue.get('name')}\n Address: {address}, {city}\n Venue ID: {venue.get('id')}\n More info: {venue.get('url', 'N/A')}\n") + lines.append( + f"{idx}. {venue.get('name')}\n Address: {address}, {city}\n Venue ID: {venue.get('id')}\n More info: {venue.get('url', 'N/A')}\n" + ) return "\n".join(lines) if __name__ == "__main__": - mcp.run(transport="stdio") \ No newline at end of file + mcp.run(transport="stdio") diff --git a/mcp-agents/gmail_chat_uagent/__init__.py b/mcp-agents/gmail_chat_uagent/__init__.py index 545ebf91..582c456e 100644 --- a/mcp-agents/gmail_chat_uagent/__init__.py +++ b/mcp-agents/gmail_chat_uagent/__init__.py @@ -1,3 +1,3 @@ -# Placeholder to make gmail_chat_uagent a Python package +# Placeholder to make gmail_chat_uagent a Python package # Allows `python gmail_chat_agent.py` to resolve relative imports because -# parent directory is added to sys.path inside gmail_chat_agent. \ No newline at end of file +# parent directory is added to sys.path inside gmail_chat_agent. diff --git a/mcp-agents/gmail_chat_uagent/gmail_chat_agent.py b/mcp-agents/gmail_chat_uagent/gmail_chat_agent.py index 261d2caa..d89b1ce1 100644 --- a/mcp-agents/gmail_chat_uagent/gmail_chat_agent.py +++ b/mcp-agents/gmail_chat_uagent/gmail_chat_agent.py @@ -8,27 +8,33 @@ 8001 or `$GMAIL_MCP_PORT`) so OpenAI can reach the `/sse/` endpoint. """ -import os -import sys -import pathlib -import json -from dotenv import load_dotenv -from uagents import Agent, Context, Model -from gmail_chat_proto import ( - _run_gmail_tool, SESSIONS_KEY, CURRENT_SESSION_DATA, _create_chat_message +import os # noqa: E402 +import sys # noqa: E402 +import pathlib # noqa: E402 +import json # noqa: E402 +from dotenv import load_dotenv # noqa: E402 +from uagents import Agent, Context, Model # noqa: E402 +from gmail_chat_proto import ( # noqa: E402 + _run_gmail_tool, + SESSIONS_KEY, + CURRENT_SESSION_DATA, + _create_chat_message, ) -import server as gmail_server +import server as gmail_server # noqa: E402 gmail_auth = gmail_server.gmail_auth + class OAuthRequest(Model): session_id: str auth_code: str + class OAuthResponse(Model): success: bool message: str + load_dotenv() # Ensure the project root (parent of this file) is on sys.path so that @@ -56,6 +62,7 @@ class OAuthResponse(Model): # Lifecycle logging helpers + @agent.on_event("startup") async def _on_start(ctx: Context): ctx.logger.info("🚀 Gmail chat agent online – address: %s", agent.address) @@ -65,6 +72,7 @@ async def _on_start(ctx: Context): async def _on_shutdown(ctx: Context): ctx.logger.info("🛑 Gmail chat agent shutting down.") + # Include chat protocol agent.include(chat_proto, publish_manifest=True) @@ -83,16 +91,24 @@ async def _handle_oauth(ctx: Context, req: OAuthRequest) -> OAuthResponse: # no # Load existing sessions try: sessions_raw = ctx.storage.get(SESSIONS_KEY) or "{}" - sessions = json.loads(sessions_raw) if isinstance(sessions_raw, str) else sessions_raw + sessions = ( + json.loads(sessions_raw) if isinstance(sessions_raw, str) else sessions_raw + ) except Exception: sessions = {} session_data = sessions.get(sid, {"messages": []}) - ctx.logger.info("🌐 [REST] Session found=%s keys=%s", bool(sid in sessions), list(session_data.keys())) + ctx.logger.info( + "🌐 [REST] Session found=%s keys=%s", + bool(sid in sessions), + list(session_data.keys()), + ) # If already authenticated, ignore duplicate callback if session_data.get("gmail_authenticated"): - ctx.logger.info("🌐 [REST] Session already authenticated – ignoring duplicate callback") + ctx.logger.info( + "🌐 [REST] Session already authenticated – ignoring duplicate callback" + ) return OAuthResponse(success=True, message="Already authenticated") # Prepare token path @@ -113,8 +129,13 @@ async def _handle_oauth(ctx: Context, req: OAuthRequest) -> OAuthResponse: # no ctx.logger.info("🌐 [REST] complete_oauth raw response: %s", out[:200]) res = json.loads(out) # If flow not initialised, run setup_oauth then retry - if not res.get("success") and "flow not initialised" in res.get("error", "").lower(): - ctx.logger.info("🌐 [REST] Flow missing – calling setup_oauth then retrying complete_oauth") + if ( + not res.get("success") + and "flow not initialised" in res.get("error", "").lower() + ): + ctx.logger.info( + "🌐 [REST] Flow missing – calling setup_oauth then retrying complete_oauth" + ) await _run_gmail_tool("setup_oauth", {"session_id": sid}) out = await _run_gmail_tool("complete_oauth", {"auth_code": code}) res = json.loads(out) @@ -131,7 +152,9 @@ async def _handle_oauth(ctx: Context, req: OAuthRequest) -> OAuthResponse: # no session_data["token_json"] = json.load(tf) os.remove(tpath) except Exception as f_err: - ctx.logger.warning("🌐 [REST] Could not read / delete temp token file: %s", f_err) + ctx.logger.warning( + "🌐 [REST] Could not read / delete temp token file: %s", f_err + ) session_data["gmail_authenticated"] = True session_data.pop("awaiting_auth_code", None) @@ -144,11 +167,17 @@ async def _handle_oauth(ctx: Context, req: OAuthRequest) -> OAuthResponse: # no # Notify user if we know their address if addr := session_data.get("sender_address"): ctx.logger.info("🌐 [REST] Notifying sender %s", addr) - await ctx.send(addr, _create_chat_message(text="✅ Authentication successful! You can now ask me to read, send or manage your Gmail messages.")) + await ctx.send( + addr, + _create_chat_message( + text="✅ Authentication successful! You can now ask me to read, send or manage your Gmail messages." + ), + ) return OAuthResponse(success=True, message="OAuth completed and tokens stored") + # Entrypoint if __name__ == "__main__": - agent.run() \ No newline at end of file + agent.run() diff --git a/mcp-agents/gmail_chat_uagent/gmail_chat_proto.py b/mcp-agents/gmail_chat_uagent/gmail_chat_proto.py index 51ed5bd3..635a6247 100644 --- a/mcp-agents/gmail_chat_uagent/gmail_chat_proto.py +++ b/mcp-agents/gmail_chat_uagent/gmail_chat_proto.py @@ -24,21 +24,21 @@ agent to keep the logic lightweight and easy to reason about. """ -import asyncio -import contextvars -import importlib -import json -import logging -import os -import pathlib -import sys -from datetime import datetime, timezone -from typing import Any, Dict, List -from uuid import uuid4 - -import httpx -from uagents import Context, Protocol -from uagents_core.contrib.protocols.chat import ( +import asyncio # noqa: E402 +import contextvars # noqa: E402 +import importlib # noqa: E402 +import json # noqa: E402 +import logging # noqa: E402 +import os # noqa: E402 +import pathlib # noqa: E402 +import sys # noqa: E402 +from datetime import datetime, timezone # noqa: E402 +from typing import Any, Dict, List # noqa: E402 +from uuid import uuid4 # noqa: E402 + +import httpx # noqa: E402 +from uagents import Context, Protocol # noqa: E402 +from uagents_core.contrib.protocols.chat import ( # noqa: E402 ChatAcknowledgement, ChatMessage, EndSessionContent, @@ -46,7 +46,7 @@ TextContent, chat_protocol_spec, ) -from dotenv import load_dotenv +from dotenv import load_dotenv # noqa: E402 # ExternalStorage removed – we no longer back up tokens externally load_dotenv() @@ -67,7 +67,9 @@ # IO logger for raw chat traffic (optional) chat_io_logger = logging.getLogger("gmail_chat_io") if not any(isinstance(h, logging.FileHandler) for h in chat_io_logger.handlers): - _fh2 = logging.FileHandler(os.getenv("GMAIL_CHAT_IO_LOG", "gmail_chat_io.log"), mode="a", encoding="utf-8") + _fh2 = logging.FileHandler( + os.getenv("GMAIL_CHAT_IO_LOG", "gmail_chat_io.log"), mode="a", encoding="utf-8" + ) _fh2.setFormatter(logging.Formatter("%(asctime)s [%(levelname)s] %(message)s")) _fh2.setLevel(logging.DEBUG) chat_io_logger.addHandler(_fh2) @@ -92,7 +94,9 @@ LOG_FILE = os.getenv("GMAIL_CHAT_LOG", "gmail_chat_debug.log") if not any(isinstance(h, logging.FileHandler) for h in logger.handlers): _fh = logging.FileHandler(LOG_FILE, mode="a", encoding="utf-8") - _fh.setFormatter(logging.Formatter("%(asctime)s [%(levelname)s] %(name)s: %(message)s")) + _fh.setFormatter( + logging.Formatter("%(asctime)s [%(levelname)s] %(name)s: %(message)s") + ) _fh.setLevel(logging.DEBUG) logger.addHandler(_fh) @@ -105,7 +109,10 @@ try: from . import server as gmail_server # type: ignore except ImportError: # running as plain script – fallback to absolute import - import importlib, pathlib, sys + import importlib + import pathlib + import sys + CURRENT_DIR = pathlib.Path(__file__).resolve().parent if str(CURRENT_DIR) not in sys.path: sys.path.insert(0, str(CURRENT_DIR)) @@ -118,12 +125,15 @@ # Helpers # --------------------------------------------------------------------------- + def _create_chat_message(text: str, end_session: bool = False) -> ChatMessage: """Wrap *text* into a ChatMessage with correct timestamp.""" content: List[Any] = [TextContent(type="text", text=text)] if end_session: content.append(EndSessionContent(type="end-session")) - return ChatMessage(timestamp=datetime.now(timezone.utc), msg_id=uuid4(), content=content) + return ChatMessage( + timestamp=datetime.now(timezone.utc), msg_id=uuid4(), content=content + ) def _unwrap(result: Any) -> str: @@ -138,13 +148,17 @@ def _unwrap(result: Any) -> str: return json.dumps(result) return str(result) + # ContextVar to pass the current *session_data* to helper fns like _run_gmail_tool -CURRENT_SESSION_DATA: contextvars.ContextVar[dict] = contextvars.ContextVar("CURRENT_SESSION_DATA") +CURRENT_SESSION_DATA: contextvars.ContextVar[dict] = contextvars.ContextVar( + "CURRENT_SESSION_DATA" +) # --------------------------------------------------------------------------- # FastMCP tool execution helper (automatic retry + token-path injection) # --------------------------------------------------------------------------- + async def _run_gmail_tool(fn_name: str, args: Dict[str, Any]) -> str: """Execute *fn_name* with *args* against the Gmail FastMCP server.""" @@ -190,7 +204,9 @@ async def _call_once(): # noqa: D401 pass return out except Exception as e: # pylint: disable=broad-except - logger.warning("⚠️ TOOL ERROR (%s) attempt %s/2 – %s", fn_name, attempt + 1, e) + logger.warning( + "⚠️ TOOL ERROR (%s) attempt %s/2 – %s", fn_name, attempt + 1, e + ) if attempt == 0: await asyncio.sleep(0.5) continue @@ -209,14 +225,18 @@ async def _call_once(): # noqa: D401 pass return json.dumps({"success": False, "error": "Unknown tool failure"}) + # --------------------------------------------------------------------------- # OpenAI *Responses* streaming helper (spec compliant) # --------------------------------------------------------------------------- + async def _call_openai_responses(messages: List[Dict[str, str]]) -> str: """Stream /v1/responses events, execute tool calls, return final assistant text.""" - transcript = "\n".join(f"{m['role'].capitalize()}: {m['content']}" for m in messages) + transcript = "\n".join( + f"{m['role'].capitalize()}: {m['content']}" for m in messages + ) TOOLS_BLOCK = [ { @@ -250,7 +270,12 @@ async def _call_openai_responses(messages: List[Dict[str, str]]) -> str: follow_payload = dict(payload) for hop in range(10): # generous limit for tool chaining - async with client.stream("POST", "https://api.openai.com/v1/responses", json=follow_payload, headers=headers) as stream: + async with client.stream( + "POST", + "https://api.openai.com/v1/responses", + json=follow_payload, + headers=headers, + ) as stream: async for raw in stream.aiter_lines(): if not raw.startswith("data:"): continue @@ -265,7 +290,11 @@ async def _call_openai_responses(messages: List[Dict[str, str]]) -> str: kind = event.get("type") - if kind in ("content_block_delta", "message_delta", "response.output_text.delta"): + if kind in ( + "content_block_delta", + "message_delta", + "response.output_text.delta", + ): delta = event.get("delta", {}) if isinstance(delta, dict): assistant_chunks.append(delta.get("text", "")) @@ -280,7 +309,9 @@ async def _call_openai_responses(messages: List[Dict[str, str]]) -> str: args_json = call["function"].get("arguments") or "{}" args_dict = json.loads(args_json) out = await _run_gmail_tool(fn, args_dict) - tool_outputs.append({"tool_call_id": call["id"], "output": out}) + tool_outputs.append( + {"tool_call_id": call["id"], "output": out} + ) # Prepare follow-up request (still streaming) follow_payload = { @@ -297,6 +328,7 @@ async def _call_openai_responses(messages: List[Dict[str, str]]) -> str: else: raise RuntimeError("Streaming run exceeded hop limit – possible tool loop?") + # --------------------------------------------------------------------------- # System prompt – keeps the model focused on Gmail tasks only # --------------------------------------------------------------------------- @@ -312,7 +344,7 @@ async def _call_openai_responses(messages: List[Dict[str, str]]) -> str: " with `setup_oauth` followed by `complete_oauth`.\n" "3. When the user asks to *read* or *delete* an email but hasn’t supplied\n" " a message-ID, first call `list_emails` (optionally with a query such\n" - " as \"from:devpost.com\") and present the top results in a numbered\n" + ' as "from:devpost.com") and present the top results in a numbered\n' " list that shows Subject, From, Date **and** the message-ID.\n" " Then ask the user which ID they’d like to act on, or if they want the\n" " first one you can proceed automatically.\n" @@ -338,16 +370,24 @@ async def _call_openai_responses(messages: List[Dict[str, str]]) -> str: SESSIONS_KEY = "gmail_chat_sessions" + @chat_proto.on_message(ChatMessage) async def handle_chat_message(ctx: Context, sender: str, msg: ChatMessage): # noqa: C901 – complex but readable # Acknowledge immediately (for client UX) - await ctx.send(sender, ChatAcknowledgement(timestamp=datetime.now(timezone.utc), acknowledged_msg_id=msg.msg_id)) + await ctx.send( + sender, + ChatAcknowledgement( + timestamp=datetime.now(timezone.utc), acknowledged_msg_id=msg.msg_id + ), + ) # 1) Load per-session persisted data try: sessions_raw = ctx.storage.get(SESSIONS_KEY) or "{}" - sessions: Dict[str, Any] = json.loads(sessions_raw) if isinstance(sessions_raw, str) else sessions_raw + sessions: Dict[str, Any] = ( + json.loads(sessions_raw) if isinstance(sessions_raw, str) else sessions_raw + ) except Exception: sessions = {} @@ -369,7 +409,11 @@ async def handle_chat_message(ctx: Context, sender: str, msg: ChatMessage): # n tokens_dir.mkdir(exist_ok=True) tokens_path = tokens_dir / f"oauth_tokens_{session_id}.json" with open(tokens_path, "wb") as lf: - lf.write(data["contents"] if isinstance(data["contents"], bytes) else data["contents"].encode()) + lf.write( + data["contents"] + if isinstance(data["contents"], bytes) + else data["contents"].encode() + ) session_data["tokens_path"] = str(tokens_path) ctx.logger.info("📥 Downloaded tokens from Agentverse asset %s", asset_id) except Exception as dl_err: # pragma: no cover @@ -380,7 +424,9 @@ async def handle_chat_message(ctx: Context, sender: str, msg: ChatMessage): # n try: # Extract plain text blocks from incoming message - inbound_texts: List[str] = [b.text.strip() for b in msg.content if isinstance(b, TextContent)] + inbound_texts: List[str] = [ + b.text.strip() for b in msg.content if isinstance(b, TextContent) + ] ctx.logger.info("📨 inbound_texts=%s", inbound_texts) # Shortcut – start session reset @@ -404,12 +450,17 @@ async def handle_chat_message(ctx: Context, sender: str, msg: ChatMessage): # n # If we previously asked for an auth code but the agent restarted # (so gmail_auth lost its _flow) we must reset the flag, otherwise # every user message will be incorrectly treated as a code. - if session_data.get("awaiting_auth_code") and getattr(gmail_auth, "_flow", None) is None: + if ( + session_data.get("awaiting_auth_code") + and getattr(gmail_auth, "_flow", None) is None + ): ctx.logger.warning( "Stale awaiting_auth_code detected but OAuth flow is missing – resetting state" ) session_data.pop("awaiting_auth_code", None) - session_data.pop("oauth_link_sent", None) # also clear link flag so new flow can start + session_data.pop( + "oauth_link_sent", None + ) # also clear link flag so new flow can start if session_data.get("gmail_authenticated"): # Point gmail_auth at the session token file before the check @@ -430,13 +481,17 @@ async def handle_chat_message(ctx: Context, sender: str, msg: ChatMessage): # n # 1. Handle authentication workflow (setup_oauth / complete_oauth) # ------------------------------------------------------------------ - ctx.logger.info("🔍 Auth status: authenticated=%s, awaiting_code=%s, inbound_texts=%s", - session_data.get("gmail_authenticated"), - session_data.get("awaiting_auth_code"), - bool(inbound_texts)) + ctx.logger.info( + "🔍 Auth status: authenticated=%s, awaiting_code=%s, inbound_texts=%s", + session_data.get("gmail_authenticated"), + session_data.get("awaiting_auth_code"), + bool(inbound_texts), + ) if not session_data.get("gmail_authenticated"): - if session_data.get("oauth_link_sent") and not session_data.get("awaiting_auth_code"): + if session_data.get("oauth_link_sent") and not session_data.get( + "awaiting_auth_code" + ): # Shouldn't normally happen, but guard against duplicate sends return @@ -464,13 +519,20 @@ async def handle_chat_message(ctx: Context, sender: str, msg: ChatMessage): # n gmail_auth._service = None # reset cache try: - result = await _run_gmail_tool("complete_oauth", {"auth_code": auth_code}) + result = await _run_gmail_tool( + "complete_oauth", {"auth_code": auth_code} + ) parsed = json.loads(result) except RuntimeError as flow_err: # _flow not initialised (e.g. after restart). Reset and fall back. - ctx.logger.warning("OAuth flow missing – restarting auth: %s", flow_err) + ctx.logger.warning( + "OAuth flow missing – restarting auth: %s", flow_err + ) session_data.pop("awaiting_auth_code", None) - parsed = {"success": False, "error": "OAuth flow reset. Please authorise again."} + parsed = { + "success": False, + "error": "OAuth flow reset. Please authorise again.", + } if parsed.get("success"): session_data["gmail_authenticated"] = True @@ -488,12 +550,16 @@ async def handle_chat_message(ctx: Context, sender: str, msg: ChatMessage): # n except FileNotFoundError: pass except Exception as token_read_err: # pragma: no cover - logger.warning("Failed to load token JSON for storage: %s", token_read_err) + logger.warning( + "Failed to load token JSON for storage: %s", token_read_err + ) # External token backup disabled – tokens remain local reply = "✅ Authentication successful! You can now ask me to read, send or manage your Gmail messages." else: - session_data.pop("awaiting_auth_code", None) # reset so a new auth flow can start + session_data.pop( + "awaiting_auth_code", None + ) # reset so a new auth flow can start reply = f"❌ Authentication failed: {parsed.get('error', 'unknown error')}. Please try again." await ctx.send(sender, _create_chat_message(reply)) @@ -505,7 +571,7 @@ async def handle_chat_message(ctx: Context, sender: str, msg: ChatMessage): # n # If we're still waiting for the code but didn't get a valid one, remind user instead of starting a new flow if session_data.get("awaiting_auth_code"): reminder = ( - "⚠️ I\'m still waiting for the Google authorisation to complete. " + "⚠️ I'm still waiting for the Google authorisation to complete. " "Please click the Authorise Gmail link I sent above and grant access." ) await ctx.send(sender, _create_chat_message(reminder)) @@ -516,12 +582,21 @@ async def handle_chat_message(ctx: Context, sender: str, msg: ChatMessage): # n # Not authenticated & not waiting – start OAuth ctx.logger.info("🚀 Starting OAuth setup...") try: - oauth_json = await _run_gmail_tool("setup_oauth", {"session_id": session_id}) + oauth_json = await _run_gmail_tool( + "setup_oauth", {"session_id": session_id} + ) ctx.logger.info("📋 OAuth response: %s", oauth_json) oauth_data = json.loads(oauth_json) if not oauth_data.get("success"): - err_msg = oauth_data.get("error", "Unknown error during OAuth setup") - await ctx.send(sender, _create_chat_message(f"❌ Authentication setup failed: {err_msg}")) + err_msg = oauth_data.get( + "error", "Unknown error during OAuth setup" + ) + await ctx.send( + sender, + _create_chat_message( + f"❌ Authentication setup failed: {err_msg}" + ), + ) ctx.logger.error("❌ OAuth setup returned error: %s", err_msg) return @@ -536,7 +611,12 @@ async def handle_chat_message(ctx: Context, sender: str, msg: ChatMessage): # n session_data["oauth_link_sent"] = True except Exception as oauth_err: ctx.logger.error("❌ OAuth setup failed: %s", oauth_err) - await ctx.send(sender, _create_chat_message(f"❌ Authentication setup failed: {oauth_err}")) + await ctx.send( + sender, + _create_chat_message( + f"❌ Authentication setup failed: {oauth_err}" + ), + ) return reply = ( "Click the **Authorize Gmail** link below.\n" @@ -550,7 +630,9 @@ async def handle_chat_message(ctx: Context, sender: str, msg: ChatMessage): # n await ctx.send(sender, _create_chat_message(reply)) sessions[session_id] = session_data ctx.storage.set(SESSIONS_KEY, json.dumps(sessions)) - ctx.logger.info(">> session=%s to=%s content=%s", ctx.session, sender, reply) + ctx.logger.info( + ">> session=%s to=%s content=%s", ctx.session, sender, reply + ) return # 3) Build conversation context for OpenAI call @@ -558,14 +640,18 @@ def _to_openai(m: Dict[str, str]) -> Dict[str, str]: role = "user" if m.get("source") == "human" else "assistant" return {"role": role, "content": m.get("content", "")} - messages: List[Dict[str, str]] = [_to_openai(m) for m in session_data.get("messages", [])] + messages: List[Dict[str, str]] = [ + _to_openai(m) for m in session_data.get("messages", []) + ] messages.insert(0, {"role": "system", "content": _SYSTEM_PROMPT}) # Append current user input if inbound_texts: user_content = "\n".join(inbound_texts) messages.append({"role": "user", "content": user_content}) - session_data.setdefault("messages", []).append({"source": "human", "content": user_content}) + session_data.setdefault("messages", []).append( + {"source": "human", "content": user_content} + ) # Trim history if len(session_data["messages"]) > MAX_HISTORY * 2: @@ -576,10 +662,13 @@ def _to_openai(m: Dict[str, str]) -> Dict[str, str]: # ---------------- Fallback: model leaked raw tool_calls JSON ------- try: - import re as _re, ast as _ast + import re as _re + import ast as _ast # If assistant dumped a tool_calls block, parse & execute it, then re-call OpenAI - _match = _re.search(r"tool_calls\s*:?\s*(\{.*\})", assistant_reply, flags=_re.S) + _match = _re.search( + r"tool_calls\s*:?\s*(\{.*\})", assistant_reply, flags=_re.S + ) if _match: blob = _match.group(1) try: @@ -602,20 +691,39 @@ def _to_openai(m: Dict[str, str]) -> Dict[str, str]: if calls: outs = [] for c in calls: - name = c.get("tool") or c.get("method") or c.get("function", {}).get("name", "") + name = ( + c.get("tool") + or c.get("method") + or c.get("function", {}).get("name", "") + ) name = name.split(".")[-1] - params = c.get("params") or c.get("parameters") or c.get("input") or {} + params = ( + c.get("params") + or c.get("parameters") + or c.get("input") + or {} + ) out = await _run_gmail_tool(name, params) - outs.append({"tool_call_id": f"manual_{name}", "output": out}) + outs.append( + {"tool_call_id": f"manual_{name}", "output": out} + ) # get proper assistant reply with outputs - assistant_reply = await _call_openai_responses([ - {"role": "assistant", "content": "", "tool_outputs": outs}, - ]) + assistant_reply = await _call_openai_responses( + [ + { + "role": "assistant", + "content": "", + "tool_outputs": outs, + }, + ] + ) # Raw list variant starting with parallel_tool_calls [ ... ] if assistant_reply.strip().startswith("parallel_tool_calls"): - _m = _re.search(r"parallel_tool_calls\s*:?\s*(\[.*\])", assistant_reply, flags=_re.S) + _m = _re.search( + r"parallel_tool_calls\s*:?\s*(\[.*\])", assistant_reply, flags=_re.S + ) if _m: list_blob = _m.group(1) try: @@ -633,16 +741,26 @@ def _to_openai(m: Dict[str, str]) -> Dict[str, str]: name = name.split(".")[-1] params = c.get("params") or {} out = await _run_gmail_tool(name, params) - outs.append({"tool_call_id": f"manual_{name}", "output": out}) - - assistant_reply = await _call_openai_responses([ - {"role": "assistant", "content": "", "tool_outputs": outs}, - ]) + outs.append( + {"tool_call_id": f"manual_{name}", "output": out} + ) + + assistant_reply = await _call_openai_responses( + [ + { + "role": "assistant", + "content": "", + "tool_outputs": outs, + }, + ] + ) except Exception as _fallback_err: logger.debug("Fallback parsing skipped: %s", _fallback_err) - session_data.setdefault("messages", []).append({"source": "assistant", "content": assistant_reply}) + session_data.setdefault("messages", []).append( + {"source": "assistant", "content": assistant_reply} + ) await ctx.send(sender, _create_chat_message(assistant_reply)) @@ -653,10 +771,12 @@ def _to_openai(m: Dict[str, str]) -> Dict[str, str]: finally: CURRENT_SESSION_DATA.reset(token) + # --------------------------------------------------------------------------- # ACK handler – currently noop # --------------------------------------------------------------------------- + @chat_proto.on_message(ChatAcknowledgement) async def _handle_ack(ctx: Context, sender: str, msg: ChatAcknowledgement): # noqa: D401 - logger.debug("ACK from %s for message %s", sender, msg.acknowledged_msg_id) \ No newline at end of file + logger.debug("ACK from %s for message %s", sender, msg.acknowledged_msg_id) diff --git a/mcp-agents/gmail_chat_uagent/oauth_server.py b/mcp-agents/gmail_chat_uagent/oauth_server.py index a4f7124f..2e47fc5c 100644 --- a/mcp-agents/gmail_chat_uagent/oauth_server.py +++ b/mcp-agents/gmail_chat_uagent/oauth_server.py @@ -24,20 +24,25 @@ AGENTVERSE_API_KEY = os.getenv("AGENTVERSE_API_KEY") STORAGE_URL = os.getenv("AGENTVERSE_URL", "https://agentverse.ai") + "/v1/storage" -print(f"🔑 API Key loaded: {AGENTVERSE_API_KEY[:20] if AGENTVERSE_API_KEY else 'None'}...") +print( + f"🔑 API Key loaded: {AGENTVERSE_API_KEY[:20] if AGENTVERSE_API_KEY else 'None'}..." +) print(f"🌐 Storage URL: {STORAGE_URL}") _storage: ExternalStorage | None = None if AGENTVERSE_API_KEY: try: - _storage = ExternalStorage(api_token=AGENTVERSE_API_KEY, storage_url=STORAGE_URL) - print(f"🔐 ExternalStorage initialised successfully") + _storage = ExternalStorage( + api_token=AGENTVERSE_API_KEY, storage_url=STORAGE_URL + ) + print("🔐 ExternalStorage initialised successfully") except Exception as err: print(f"⚠️ ExternalStorage init failed: {err}") _storage = None else: print("❌ No AGENTVERSE_API_KEY found - external storage disabled") + def save_oauth_code_to_agent_storage(session_id: str, auth_code: str): """Save OAuth code to agent storage JSON file.""" try: @@ -46,39 +51,42 @@ def save_oauth_code_to_agent_storage(session_id: str, auth_code: str): if not storage_files: print("⚠️ No agent storage file found") return False - + storage_file = storage_files[0] # Use the first one found - + # Read current storage with open(storage_file, "r") as f: storage_data = json.load(f) - + # Parse gmail_chat_sessions sessions_str = storage_data.get("gmail_chat_sessions", "{}") - sessions = json.loads(sessions_str) if isinstance(sessions_str, str) else sessions_str - + sessions = ( + json.loads(sessions_str) if isinstance(sessions_str, str) else sessions_str + ) + # Update the session with the OAuth code if session_id in sessions: sessions[session_id]["oauth_code"] = auth_code sessions[session_id]["code_received"] = True - + # Update storage storage_data["gmail_chat_sessions"] = json.dumps(sessions) - + # Write back to file with open(storage_file, "w") as f: json.dump(storage_data, f, indent=4) - + print(f"💾 OAuth code saved to agent storage for session {session_id}") return True else: print(f"⚠️ Session {session_id} not found in agent storage") return False - + except Exception as e: print(f"⚠️ Failed to save to agent storage: {e}") return False + SUCCESS_PAGE_TEMPLATE = """ @@ -129,18 +137,22 @@ def do_GET(self): # noqa: N802 self.end_headers() page = SUCCESS_PAGE_TEMPLATE.replace("{AUTH_CODE}", code) self.wfile.write(page.encode("utf-8")) - print(f"\n🎉 Authorization code received for session {state}: {code[:10]}…\n") + print( + f"\n🎉 Authorization code received for session {state}: {code[:10]}…\n" + ) # Persist to Agentverse so chat agent can pick it up stored_externally = False if _storage and state: try: - _storage.create_asset(name=state, content=code.encode(), mime_type="text/plain") + _storage.create_asset( + name=state, content=code.encode(), mime_type="text/plain" + ) print("💾 Code uploaded to Agentverse asset", state) stored_externally = True except Exception as up_err: print("⚠️ Failed to upload code to Agentverse:", up_err) - + # Fallback to agent storage if not stored_externally and state: if not save_oauth_code_to_agent_storage(state, code): @@ -148,15 +160,20 @@ def do_GET(self): # noqa: N802 try: with open(f"oauth_code_{state}.txt", "w") as f: f.write(code) - print(f"💾 Code saved locally to oauth_code_{state}.txt as final fallback") + print( + f"💾 Code saved locally to oauth_code_{state}.txt as final fallback" + ) except Exception as local_err: print("⚠️ Failed to save code locally:", local_err) # --- Notify chat agent via REST so it can complete OAuth automatically try: - import requests, json as _json + import requests + payload = {"session_id": state, "auth_code": code} - resp = requests.post("http://localhost:8088/oauth/callback", json=payload, timeout=3) + resp = requests.post( + "http://localhost:8088/oauth/callback", json=payload, timeout=3 + ) print("➡️ Posted code to chat agent – status", resp.status_code) except Exception as notify_err: print("⚠️ Could not notify chat agent:", notify_err) @@ -183,4 +200,4 @@ def start_oauth_server(): if __name__ == "__main__": - start_oauth_server() \ No newline at end of file + start_oauth_server() diff --git a/mcp-agents/gmail_chat_uagent/server.py b/mcp-agents/gmail_chat_uagent/server.py index b67ec5f4..b935d6d7 100644 --- a/mcp-agents/gmail_chat_uagent/server.py +++ b/mcp-agents/gmail_chat_uagent/server.py @@ -40,13 +40,19 @@ logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) -VERBOSE_GMAIL_LOGS = os.getenv("VERBOSE_GMAIL_LOGS", "true").lower() in ("1", "true", "yes") +VERBOSE_GMAIL_LOGS = os.getenv("VERBOSE_GMAIL_LOGS", "true").lower() in ( + "1", + "true", + "yes", +) logger.setLevel(logging.DEBUG if VERBOSE_GMAIL_LOGS else logging.INFO) LOG_FILE = os.getenv("GMAIL_LOG", "gmail_debug.log") if not any(isinstance(h, logging.FileHandler) for h in logger.handlers): _fh = logging.FileHandler(LOG_FILE, mode="a", encoding="utf-8") - _fh.setFormatter(logging.Formatter("%(asctime)s [%(levelname)s] %(name)s: %(message)s")) + _fh.setFormatter( + logging.Formatter("%(asctime)s [%(levelname)s] %(name)s: %(message)s") + ) _fh.setLevel(logging.DEBUG) logger.addHandler(_fh) @@ -92,7 +98,9 @@ class GmailAuth: """Handles OAuth for Gmail and provides an authenticated service.""" - def __init__(self, credentials_path: str = CREDENTIALS_PATH, tokens_path: str = TOKENS_PATH): + def __init__( + self, credentials_path: str = CREDENTIALS_PATH, tokens_path: str = TOKENS_PATH + ): self.credentials_path = credentials_path self.tokens_path = tokens_path self._service = None # Cached Gmail service @@ -103,19 +111,23 @@ def __init__(self, credentials_path: str = CREDENTIALS_PATH, tokens_path: str = def get_oauth_url(self, session_id: str = None) -> str: logger.info("🔗 Starting OAuth flow - generating authorization URL") if not os.path.exists(self.credentials_path): - raise FileNotFoundError(f"Google OAuth client secrets not found at {self.credentials_path}.") + raise FileNotFoundError( + f"Google OAuth client secrets not found at {self.credentials_path}." + ) self._flow = Flow.from_client_secrets_file( self.credentials_path, scopes=SCOPES, redirect_uri="http://localhost:8080/callback", ) - + # Use session_id as state if provided, otherwise let Google generate one if session_id: - auth_url, _ = self._flow.authorization_url(prompt="consent", state=session_id) + auth_url, _ = self._flow.authorization_url( + prompt="consent", state=session_id + ) else: auth_url, _ = self._flow.authorization_url(prompt="consent") - + logger.info(f"✅ OAuth URL generated successfully: {auth_url[:100]}…") return auth_url @@ -143,7 +155,9 @@ def get_service(self): # noqa: D401 if self._service: return self._service if not os.path.exists(self.tokens_path): - raise RuntimeError("Not authenticated – run setup_oauth/complete_oauth first.") + raise RuntimeError( + "Not authenticated – run setup_oauth/complete_oauth first." + ) with open(self.tokens_path, "r") as f: token_info = json.load(f) creds = Credentials( @@ -169,6 +183,7 @@ def get_service(self): # noqa: D401 # Helper utilities # --------------------------------------------------------------------------- + def _create_message( to: str, subject: str, @@ -224,9 +239,13 @@ def check_auth_status() -> str: with open(token_file, "r") as f: scopes = json.load(f).get("scopes", []) gmail_auth.get_service() # refresh check - return json.dumps({"success": True, "authenticated": True, "scopes": scopes}, indent=2) + return json.dumps( + {"success": True, "authenticated": True, "scopes": scopes}, indent=2 + ) except Exception as e: - return json.dumps({"success": False, "authenticated": False, "error": str(e)}) + return json.dumps( + {"success": False, "authenticated": False, "error": str(e)} + ) return json.dumps({"success": True, "authenticated": False}, indent=2) @@ -238,13 +257,16 @@ def reset_oauth_tokens() -> str: return json.dumps({"success": True}, indent=2) return json.dumps({"success": False, "error": "No token file"}, indent=2) + # --------------------------------------------------------------------------- # Gmail action tools # --------------------------------------------------------------------------- @mcp.tool() -def send_email(to: str, subject: str, body: str, cc: str = "", bcc: str = "", is_html: bool = False) -> str: +def send_email( + to: str, subject: str, body: str, cc: str = "", bcc: str = "", is_html: bool = False +) -> str: try: service = gmail_auth.get_service() msg = _create_message(to, subject, body, cc or None, bcc or None, is_html) @@ -255,23 +277,45 @@ def send_email(to: str, subject: str, body: str, cc: str = "", bcc: str = "", is @mcp.tool() -def list_emails(query: str = "", label_ids: str = "INBOX", max_results: int = 10) -> str: +def list_emails( + query: str = "", label_ids: str = "INBOX", max_results: int = 10 +) -> str: try: service = gmail_auth.get_service() labels = [lbl.strip() for lbl in label_ids.split(",") if lbl.strip()] - resp = service.users().messages().list(userId="me", q=query, labelIds=labels, maxResults=max_results).execute() + resp = ( + service.users() + .messages() + .list(userId="me", q=query, labelIds=labels, maxResults=max_results) + .execute() + ) msgs = resp.get("messages", []) output: List[Dict[str, Any]] = [] for msg in msgs: - detail = service.users().messages().get(userId="me", id=msg["id"], format="metadata", metadataHeaders=["Subject", "From", "Date"]).execute() - headers = {h["name"]: h["value"] for h in detail.get("payload", {}).get("headers", [])} - output.append({ - "id": detail.get("id"), - "snippet": detail.get("snippet"), - "subject": headers.get("Subject", ""), - "from": headers.get("From", ""), - "date": headers.get("Date", ""), - }) + detail = ( + service.users() + .messages() + .get( + userId="me", + id=msg["id"], + format="metadata", + metadataHeaders=["Subject", "From", "Date"], + ) + .execute() + ) + headers = { + h["name"]: h["value"] + for h in detail.get("payload", {}).get("headers", []) + } + output.append( + { + "id": detail.get("id"), + "snippet": detail.get("snippet"), + "subject": headers.get("Subject", ""), + "from": headers.get("From", ""), + "date": headers.get("Date", ""), + } + ) return json.dumps({"success": True, "emails": output}, indent=2) except Exception as e: return json.dumps({"success": False, "error": str(e)}) @@ -281,18 +325,28 @@ def list_emails(query: str = "", label_ids: str = "INBOX", max_results: int = 10 def read_email(message_id: str) -> str: try: service = gmail_auth.get_service() - detail = service.users().messages().get(userId="me", id=message_id, format="full").execute() - headers = {h["name"]: h["value"] for h in detail.get("payload", {}).get("headers", [])} - return json.dumps({ - "success": True, - "id": detail.get("id"), - "snippet": detail.get("snippet"), - "subject": headers.get("Subject", ""), - "from": headers.get("From", ""), - "to": headers.get("To", ""), - "date": headers.get("Date", ""), - "payload": detail.get("payload"), - }, indent=2) + detail = ( + service.users() + .messages() + .get(userId="me", id=message_id, format="full") + .execute() + ) + headers = { + h["name"]: h["value"] for h in detail.get("payload", {}).get("headers", []) + } + return json.dumps( + { + "success": True, + "id": detail.get("id"), + "snippet": detail.get("snippet"), + "subject": headers.get("Subject", ""), + "from": headers.get("From", ""), + "to": headers.get("To", ""), + "date": headers.get("Date", ""), + "payload": detail.get("payload"), + }, + indent=2, + ) except Exception as e: return json.dumps({"success": False, "error": str(e)}) @@ -315,10 +369,22 @@ def delete_last_sent_email(to: str, query: str = "", label_ids: str = "SENT") -> try: service = gmail_auth.get_service() full_query = f"to:{to} {query}".strip() - resp = service.users().messages().list(userId="me", q=full_query, labelIds=[lbl.strip() for lbl in label_ids.split(",") if lbl.strip()], maxResults=1).execute() + resp = ( + service.users() + .messages() + .list( + userId="me", + q=full_query, + labelIds=[lbl.strip() for lbl in label_ids.split(",") if lbl.strip()], + maxResults=1, + ) + .execute() + ) msgs = resp.get("messages", []) if not msgs: - return json.dumps({"success": False, "error": "No matching email found"}, indent=2) + return json.dumps( + {"success": False, "error": "No matching email found"}, indent=2 + ) msg_id = msgs[0]["id"] try: service.users().messages().trash(userId="me", id=msg_id).execute() @@ -351,4 +417,4 @@ def main(): # pragma: no cover if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/openai-agent-sdk/Scholarship-finder/test_workflow.py b/openai-agent-sdk/Scholarship-finder/test_workflow.py index e7556634..cbb459e4 100644 --- a/openai-agent-sdk/Scholarship-finder/test_workflow.py +++ b/openai-agent-sdk/Scholarship-finder/test_workflow.py @@ -2,6 +2,7 @@ Simple test script to verify the OpenAI Agent SDK workflow works. Run this before deploying to ensure everything is configured correctly. """ + import asyncio import os from dotenv import load_dotenv @@ -21,6 +22,7 @@ # Import workflow try: from workflow import run_workflow, WorkflowInput + print("✅ Workflow module imported successfully") except Exception as e: print(f"❌ Error importing workflow: {e}") @@ -29,11 +31,11 @@ async def test_scholarship_search(): """Test the scholarship search with a sample student profile""" - - print("\n" + "="*60) + + print("\n" + "=" * 60) print("🧪 TESTING SCHOLARSHIP FINDER WORKFLOW") - print("="*60 + "\n") - + print("=" * 60 + "\n") + # Test profile test_profile = """ I'm a junior Computer Science major with 3.7 GPA in San Jose, California. @@ -41,35 +43,35 @@ async def test_scholarship_search(): President of coding club, volunteer tutor. Moderate financial need. """ - + print("📝 Test Student Profile:") print(test_profile.strip()) - print("\n" + "-"*60) + print("\n" + "-" * 60) print("🔍 Searching for scholarships...") - print("-"*60 + "\n") - + print("-" * 60 + "\n") + try: # Run the workflow result = await run_workflow(WorkflowInput(input_as_text=test_profile)) - + # Get the output output = result.get("output_text", "") - + if output: print("✅ SUCCESS! Scholarship search completed.\n") - print("="*60) + print("=" * 60) print("📊 RESULTS:") - print("="*60) + print("=" * 60) print(output) - print("\n" + "="*60) + print("\n" + "=" * 60) print("✅ Test completed successfully!") - print("="*60) + print("=" * 60) else: print("⚠️ Warning: No output received from workflow") print("This might indicate an issue with the OpenAI API or search") - + except Exception as e: - print(f"\n❌ ERROR during workflow execution:") + print("\n❌ ERROR during workflow execution:") print(f"{type(e).__name__}: {str(e)}") print("\nPossible issues:") print("1. Invalid OpenAI API key") @@ -77,16 +79,16 @@ async def test_scholarship_search(): print("3. Network connectivity issues") print("4. OpenAI Agent SDK not properly installed") return False - + return True if __name__ == "__main__": print("\n🎓 Scholarship Finder - Workflow Test\n") - + # Run the test success = asyncio.run(test_scholarship_search()) - + if success: print("\n✅ All tests passed! Agent is ready to deploy.") print("\nNext steps:") diff --git a/openai-agent-sdk/Scholarship-finder/uagent_bridge.py b/openai-agent-sdk/Scholarship-finder/uagent_bridge.py index 58ea2c3b..1d1f5339 100644 --- a/openai-agent-sdk/Scholarship-finder/uagent_bridge.py +++ b/openai-agent-sdk/Scholarship-finder/uagent_bridge.py @@ -2,6 +2,7 @@ Bridge between OpenAI Agent SDK and Fetch.ai uAgents platform. This makes the Scholarship Finder accessible via ASI-One chat interface. """ + from __future__ import annotations import os @@ -29,7 +30,9 @@ def _load_workflow_module(): """Dynamically load the workflow.py module containing OpenAI Agent SDK logic""" here = os.path.dirname(__file__) workflow_path = os.path.join(here, "workflow.py") - spec = importlib.util.spec_from_file_location("scholarship_finder_workflow", workflow_path) + spec = importlib.util.spec_from_file_location( + "scholarship_finder_workflow", workflow_path + ) if spec is None or spec.loader is None: raise RuntimeError("Failed to load workflow module spec") mod = importlib.util.module_from_spec(spec) @@ -72,9 +75,7 @@ def text_msg(text: str, *, end_session: bool = False) -> ChatMessage: if end_session: content.append(EndSessionContent(type="end-session")) return ChatMessage( - timestamp=datetime.now(timezone.utc), - msg_id=uuid4(), - content=content + timestamp=datetime.now(timezone.utc), msg_id=uuid4(), content=content ) @@ -105,8 +106,7 @@ async def on_chat(ctx: Context, sender: str, msg: ChatMessage): await ctx.send( sender, ChatAcknowledgement( - timestamp=datetime.now(timezone.utc), - acknowledged_msg_id=msg.msg_id + timestamp=datetime.now(timezone.utc), acknowledged_msg_id=msg.msg_id ), ) @@ -119,10 +119,11 @@ async def on_chat(ctx: Context, sender: str, msg: ChatMessage): ChatMessage( timestamp=datetime.now(timezone.utc), msg_id=uuid4(), - content=[MetadataContent( - type="metadata", - metadata={"attachments": "false"} - )], + content=[ + MetadataContent( + type="metadata", metadata={"attachments": "false"} + ) + ], ), ) # Send welcome message @@ -133,33 +134,61 @@ async def on_chat(ctx: Context, sender: str, msg: ChatMessage): if isinstance(item, TextContent): user_text = item.text.strip() user_text_lower = user_text.lower() - + # Handle empty messages if not user_text or len(user_text) < 3: await ctx.send( sender, - text_msg("Please share your student profile so I can find scholarships for you!") + text_msg( + "Please share your student profile so I can find scholarships for you!" + ), ) return - + # Handle help requests - check if ANY help keyword is in the message help_keywords = ["help", "how", "info", "what", "example", "start", "guide"] - if any(keyword in user_text_lower for keyword in help_keywords) and len(user_text) < 50: + if ( + any(keyword in user_text_lower for keyword in help_keywords) + and len(user_text) < 50 + ): ctx.logger.info(f"Help request detected: {user_text}") await ctx.send(sender, text_msg(WELCOME_MESSAGE)) return - + # Check if profile has minimum info - has_gpa = any(word in user_text.lower() for word in ["gpa", "grade", "3.", "2.", "4."]) - has_major = any(word in user_text.lower() for word in [ - "major", "computer", "engineering", "business", "science", - "psychology", "education", "nursing", "cs", "stem" - ]) - has_year = any(word in user_text.lower() for word in [ - "freshman", "sophomore", "junior", "senior", - "1st", "2nd", "3rd", "4th", "year" - ]) - + has_gpa = any( + word in user_text.lower() for word in ["gpa", "grade", "3.", "2.", "4."] + ) + has_major = any( + word in user_text.lower() + for word in [ + "major", + "computer", + "engineering", + "business", + "science", + "psychology", + "education", + "nursing", + "cs", + "stem", + ] + ) + has_year = any( + word in user_text.lower() + for word in [ + "freshman", + "sophomore", + "junior", + "senior", + "1st", + "2nd", + "3rd", + "4th", + "year", + ] + ) + if not (has_gpa or has_major or has_year): await ctx.send( sender, @@ -171,22 +200,26 @@ async def on_chat(ctx: Context, sender: str, msg: ChatMessage): "• Your year in school\n" "• Your location\n\n" "Type 'help' for an example." - ) + ), ) return - + # Send processing message await ctx.send( sender, - text_msg("🔍 Searching for scholarships matching your profile...\n\nThis may take 10-15 seconds.") + text_msg( + "🔍 Searching for scholarships matching your profile...\n\nThis may take 10-15 seconds." + ), ) - + # Run the OpenAI Agent SDK workflow try: - ctx.logger.info(f"Running scholarship search for profile: {user_text[:100]}...") + ctx.logger.info( + f"Running scholarship search for profile: {user_text[:100]}..." + ) result = await run_workflow(WorkflowInput(input_as_text=user_text)) answer = (result or {}).get("output_text", "") - + if answer: # Send the scholarship results await ctx.send(sender, text_msg(answer)) @@ -197,7 +230,7 @@ async def on_chat(ctx: Context, sender: str, msg: ChatMessage): text_msg( "Sorry, I couldn't find scholarships matching your profile. " "Try providing more details about your background and interests." - ) + ), ) except Exception as e: ctx.logger.exception("Workflow error") @@ -207,12 +240,14 @@ async def on_chat(ctx: Context, sender: str, msg: ChatMessage): f"❌ Sorry, I encountered an error while searching for scholarships.\n\n" f"Error: {str(e)}\n\n" f"Please try again or contact support if the issue persists." - ) + ), ) return # If no supported content found - await ctx.send(sender, text_msg("Unsupported message content. Please send text only.")) + await ctx.send( + sender, text_msg("Unsupported message content. Please send text only.") + ) @chat_proto.on_message(ChatAcknowledgement) diff --git a/openai-agent-sdk/Scholarship-finder/workflow.py b/openai-agent-sdk/Scholarship-finder/workflow.py index 6284e209..c79438bf 100644 --- a/openai-agent-sdk/Scholarship-finder/workflow.py +++ b/openai-agent-sdk/Scholarship-finder/workflow.py @@ -8,9 +8,9 @@ "country": "US", "region": None, "city": None, - "timezone": None + "timezone": None, }, - search_context_size="high" + search_context_size="high", ) scholarship_agent = Agent( @@ -88,7 +88,7 @@ - Never show citations or reference markers - Focus on scholarships with verifiable information""", model="gpt-4o", - tools=[web_search_tool] + tools=[web_search_tool], ) @@ -102,29 +102,24 @@ async def run_workflow(workflow_input: WorkflowInput): conversation_history: list[TResponseInputItem] = [ { "role": "user", - "content": [ - { - "type": "input_text", - "text": workflow["input_as_text"] - } - ] + "content": [{"type": "input_text", "text": workflow["input_as_text"]}], } ] - + agent_result_temp = await Runner.run( scholarship_agent, input=[*conversation_history], run_config=RunConfig( trace_metadata={ "__trace_source__": "agent-builder", - "workflow_id": "scholarship_finder_v1" + "workflow_id": "scholarship_finder_v1", } - ) + ), ) - conversation_history.extend([item.to_input_item() for item in agent_result_temp.new_items]) + conversation_history.extend( + [item.to_input_item() for item in agent_result_temp.new_items] + ) - agent_result = { - "output_text": agent_result_temp.final_output_as(str) - } + agent_result = {"output_text": agent_result_temp.final_output_as(str)} return agent_result diff --git a/pdf-summariser-example/agent.py b/pdf-summariser-example/agent.py index 72dc4389..03ee2032 100644 --- a/pdf-summariser-example/agent.py +++ b/pdf-summariser-example/agent.py @@ -8,4 +8,3 @@ if __name__ == "__main__": agent.run() - diff --git a/pdf-summariser-example/chat_proto.py b/pdf-summariser-example/chat_proto.py index 3f3fd530..e02687cd 100644 --- a/pdf-summariser-example/chat_proto.py +++ b/pdf-summariser-example/chat_proto.py @@ -79,9 +79,7 @@ def download_resource(ctx: Context, item: ResourceContent) -> dict | None: mime_type = "application/pdf" os.makedirs(DOWNLOADS_DIR, exist_ok=True) - filename = ( - f"{DOWNLOADS_DIR}/pdf_{datetime.now(timezone.utc).strftime('%Y%m%d_%H%M%S')}.pdf" - ) + filename = f"{DOWNLOADS_DIR}/pdf_{datetime.now(timezone.utc).strftime('%Y%m%d_%H%M%S')}.pdf" with open(filename, "wb") as pdf_file: pdf_file.write(content_bytes) ctx.logger.info(f"Saved resource locally to {filename}") @@ -142,21 +140,21 @@ async def handle_message(ctx: Context, sender: str, msg: ChatMessage): try: ctx.logger.info("Extracting text from PDF(s)...") extracted_text = get_pdf_text(prompt_content, logger=ctx.logger) - + if not extracted_text or extracted_text == "No PDF content found to extract.": await ctx.send(sender, create_text_chat("No text found in PDF.")) return - + ctx.logger.info("Summarizing extracted text...") summary = summarize_text(extracted_text, logger=ctx.logger) - + if summary: await ctx.send(sender, create_text_chat(summary)) else: # Fallback: send extracted text if summarization fails ctx.logger.warning("Summarization failed, sending extracted text instead") await ctx.send(sender, create_text_chat(extracted_text)) - + except Exception as err: ctx.logger.error(f"PDF extraction failed: {err}") await ctx.send( @@ -172,4 +170,3 @@ async def handle_ack(ctx: Context, sender: str, msg: ChatAcknowledgement): ctx.logger.info( f"Got an acknowledgement from {sender} for {msg.acknowledged_msg_id}" ) - diff --git a/pdf-summariser-example/utils.py b/pdf-summariser-example/utils.py index a26f39df..3346e8a4 100644 --- a/pdf-summariser-example/utils.py +++ b/pdf-summariser-example/utils.py @@ -6,12 +6,14 @@ try: import PyPDF2 + HAS_PYPDF2 = True except ImportError: HAS_PYPDF2 = False try: import pdfplumber + HAS_PDFPLUMBER = True except ImportError: HAS_PDFPLUMBER = False @@ -24,9 +26,9 @@ def extract_text_from_pdf(pdf_bytes: bytes, logger=None) -> str: """ if not pdf_bytes: return "Error: No PDF data provided" - + text_content = [] - + # Try pdfplumber first (better text extraction) if HAS_PDFPLUMBER: try: @@ -42,7 +44,7 @@ def extract_text_from_pdf(pdf_bytes: bytes, logger=None) -> str: except Exception as e: if logger: logger.warning(f"pdfplumber extraction failed: {e}, trying PyPDF2...") - + # Fallback to PyPDF2 if HAS_PYPDF2: try: @@ -50,19 +52,19 @@ def extract_text_from_pdf(pdf_bytes: bytes, logger=None) -> str: logger.info("Extracting text using PyPDF2...") pdf_file = io.BytesIO(pdf_bytes) pdf_reader = PyPDF2.PdfReader(pdf_file) - + for page_num, page in enumerate(pdf_reader.pages, 1): text = page.extract_text() if text: text_content.append(f"--- Page {page_num} ---\n{text}\n") - + if text_content: return "\n".join(text_content) except Exception as e: if logger: logger.error(f"PyPDF2 extraction failed: {e}") return f"Error extracting text from PDF: {e}" - + # If neither library is available return "Error: No PDF extraction library available. Please install pdfplumber or PyPDF2." @@ -81,22 +83,24 @@ def get_pdf_text(content: list[dict[str, Any]], logger=None) -> str | None: mime_type = item.get("mime_type", "") if logger: logger.info(f"Processing resource with mime type: {mime_type}") - + if mime_type == "application/pdf" or mime_type.endswith("/pdf"): if logger: - logger.info(f"Extracting text from PDF - Content length: {len(item.get('contents', ''))} characters") - + logger.info( + f"Extracting text from PDF - Content length: {len(item.get('contents', ''))} characters" + ) + # Decode base64 PDF content try: pdf_base64 = item.get("contents", "") pdf_bytes = base64.b64decode(pdf_base64) if logger: logger.info(f"Decoded PDF: {len(pdf_bytes)} bytes") - + # Extract text from PDF pdf_text = extract_text_from_pdf(pdf_bytes, logger=logger) extracted_texts.append(pdf_text) - + except Exception as e: error_msg = f"Error processing PDF: {e}" if logger: @@ -117,26 +121,26 @@ def summarize_text(text: str, logger=None) -> Optional[str]: """ if not text or not text.strip(): return None - + api_key = os.getenv("ASI_ONE_API_KEY") if not api_key: if logger: logger.error("ASI_ONE_API_KEY not found in environment variables") return None - + url = "https://api.asi1.ai/v1/chat/completions" - + headers = { "Authorization": f"Bearer {api_key}", "x-session-id": "pdf-summarization-session", - "Content-Type": "application/json" + "Content-Type": "application/json", } - + # Create a prompt for summarization # If text is very long, we might need to truncate it max_length = 100000 # Adjust based on model limits text_to_summarize = text[:max_length] if len(text) > max_length else text - + prompt = f"""Please provide a concise summary of the following PDF content. Focus on the main points, key information, and important details. @@ -144,27 +148,29 @@ def summarize_text(text: str, logger=None) -> Optional[str]: {text_to_summarize} Summary:""" - + json_data = { "model": "asi1-mini", - "messages": [{"role": "user", "content": prompt}] + "messages": [{"role": "user", "content": prompt}], } - + try: if logger: logger.info("Sending summarization request to ASI One API...") - + response = requests.post(url, headers=headers, json=json_data, timeout=120) response.raise_for_status() - + result = response.json() summary = result["choices"][0]["message"]["content"] - + if logger: - logger.info(f"Summarization complete. Summary length: {len(summary)} characters") - + logger.info( + f"Summarization complete. Summary length: {len(summary)} characters" + ) + return summary - + except requests.exceptions.RequestException as e: if logger: logger.error(f"Failed to summarize text: {e}") @@ -177,4 +183,3 @@ def summarize_text(text: str, logger=None) -> Optional[str]: if logger: logger.error(f"Unexpected error during summarization: {e}") return None - diff --git a/stripe-horoscope-agent/agent.py b/stripe-horoscope-agent/agent.py index f9dc9546..f172a032 100644 --- a/stripe-horoscope-agent/agent.py +++ b/stripe-horoscope-agent/agent.py @@ -25,4 +25,3 @@ if __name__ == "__main__": agent.run() - diff --git a/stripe-horoscope-agent/chat_proto.py b/stripe-horoscope-agent/chat_proto.py index 57970f34..c46cc44f 100644 --- a/stripe-horoscope-agent/chat_proto.py +++ b/stripe-horoscope-agent/chat_proto.py @@ -33,4 +33,3 @@ async def _on_chat_ack(ctx: Context, sender: str, msg: ChatAcknowledgement): return return proto - diff --git a/stripe-horoscope-agent/config.py b/stripe-horoscope-agent/config.py index 56535820..520168c7 100644 --- a/stripe-horoscope-agent/config.py +++ b/stripe-horoscope-agent/config.py @@ -11,6 +11,9 @@ STRIPE_AMOUNT_CENTS = int(os.getenv("STRIPE_AMOUNT_CENTS", "100")) # $1.00 STRIPE_CURRENCY = (os.getenv("STRIPE_CURRENCY", "usd") or "usd").strip().lower() -STRIPE_PRODUCT_NAME = (os.getenv("STRIPE_PRODUCT_NAME", "Daily horoscope") or "Daily horoscope").strip() -STRIPE_SUCCESS_URL = (os.getenv("STRIPE_SUCCESS_URL", "https://agentverse.ai/payment-success") or "").strip() - +STRIPE_PRODUCT_NAME = ( + os.getenv("STRIPE_PRODUCT_NAME", "Daily horoscope") or "Daily horoscope" +).strip() +STRIPE_SUCCESS_URL = ( + os.getenv("STRIPE_SUCCESS_URL", "https://agentverse.ai/payment-success") or "" +).strip() diff --git a/stripe-horoscope-agent/handlers.py b/stripe-horoscope-agent/handlers.py index 7d146a66..b46cc724 100644 --- a/stripe-horoscope-agent/handlers.py +++ b/stripe-horoscope-agent/handlers.py @@ -22,7 +22,10 @@ save_state, wants_horoscope, ) -from stripe_payments import create_embedded_checkout_session, verify_checkout_session_paid +from stripe_payments import ( + create_embedded_checkout_session, + verify_checkout_session_paid, +) def _looks_like_new_chat(text_l: str) -> bool: @@ -51,9 +54,15 @@ async def on_chat(ctx: Context, sender: str, msg: ChatMessage): awaiting_sign = bool(state.get("awaiting_sign")) was_awaiting_sign = awaiting_sign awaiting_payment = bool(state.get("awaiting_payment")) - pending_stripe = state.get("pending_stripe") if isinstance(state.get("pending_stripe"), dict) else None + pending_stripe = ( + state.get("pending_stripe") + if isinstance(state.get("pending_stripe"), dict) + else None + ) - ctx.logger.info(f"[chat] inbound sender={sender} session={ctx.session} text={text!r} state={state}") + ctx.logger.info( + f"[chat] inbound sender={sender} session={ctx.session} text={text!r} state={state}" + ) wants = wants_horoscope(text_l) @@ -78,7 +87,13 @@ async def on_chat(ctx: Context, sender: str, msg: ChatMessage): ) return req = RequestPayment( - accepted_funds=[Funds(currency="USD", amount=f"{STRIPE_AMOUNT_CENTS / 100:.2f}", payment_method="stripe")], + accepted_funds=[ + Funds( + currency="USD", + amount=f"{STRIPE_AMOUNT_CENTS / 100:.2f}", + payment_method="stripe", + ) + ], recipient=str(ctx.agent.address), deadline_seconds=300, reference=str(ctx.session), @@ -86,7 +101,12 @@ async def on_chat(ctx: Context, sender: str, msg: ChatMessage): metadata={"stripe": pending_stripe, "service": "daily_horoscope"}, ) await ctx.send(sender, req) - await ctx.send(sender, make_chat("Payment is still pending. Please complete the Stripe checkout above.")) + await ctx.send( + sender, + make_chat( + "Payment is still pending. Please complete the Stripe checkout above." + ), + ) return # If we're waiting for the user to provide a sign, treat the next message as the sign input. @@ -109,7 +129,9 @@ async def on_chat(ctx: Context, sender: str, msg: ChatMessage): # Decide whether to reply normally or run the horoscope flow. if not wants: reply = await normal_reply(text) - await ctx.send(sender, make_chat(reply or "Say 'give me my horoscope' to begin.")) + await ctx.send( + sender, make_chat(reply or "Say 'give me my horoscope' to begin.") + ) return # Horoscope flow: require a sign. @@ -140,7 +162,13 @@ async def on_chat(ctx: Context, sender: str, msg: ChatMessage): save_state(ctx, sender, state) req = RequestPayment( - accepted_funds=[Funds(currency="USD", amount=f"{STRIPE_AMOUNT_CENTS / 100:.2f}", payment_method="stripe")], + accepted_funds=[ + Funds( + currency="USD", + amount=f"{STRIPE_AMOUNT_CENTS / 100:.2f}", + payment_method="stripe", + ) + ], recipient=str(ctx.agent.address), deadline_seconds=300, reference=str(ctx.session), @@ -148,17 +176,28 @@ async def on_chat(ctx: Context, sender: str, msg: ChatMessage): metadata={"stripe": checkout, "service": "daily_horoscope"}, ) await ctx.send(sender, req) - await ctx.send(sender, make_chat("Once payment completes, I’ll reply here with your horoscope.")) + await ctx.send( + sender, + make_chat("Once payment completes, I’ll reply here with your horoscope."), + ) async def on_commit(ctx: Context, sender: str, msg: CommitPayment): if msg.funds.payment_method != "stripe" or not msg.transaction_id: - await ctx.send(sender, RejectPayment(reason="Unsupported payment method (expected stripe).")) + await ctx.send( + sender, + RejectPayment(reason="Unsupported payment method (expected stripe)."), + ) return paid = await asyncio.to_thread(verify_checkout_session_paid, msg.transaction_id) if not paid: - await ctx.send(sender, RejectPayment(reason="Stripe payment not completed yet. Please finish checkout.")) + await ctx.send( + sender, + RejectPayment( + reason="Stripe payment not completed yet. Please finish checkout." + ), + ) return state = load_state(ctx, sender) @@ -166,11 +205,18 @@ async def on_commit(ctx: Context, sender: str, msg: CommitPayment): sign = str(state.get("sign") or "").strip() or "unknown" horoscope = await generate_horoscope(sign) - await ctx.send(sender, make_chat(horoscope or "Payment received, but I couldn’t generate your horoscope right now.")) + await ctx.send( + sender, + make_chat( + horoscope + or "Payment received, but I couldn’t generate your horoscope right now." + ), + ) clear_state(ctx, sender) async def on_reject(ctx: Context, sender: str, msg: RejectPayment): clear_state(ctx, sender) - await ctx.send(sender, make_chat(f"Payment was rejected. {msg.reason or ''}".strip())) - + await ctx.send( + sender, make_chat(f"Payment was rejected. {msg.reason or ''}".strip()) + ) diff --git a/stripe-horoscope-agent/llm.py b/stripe-horoscope-agent/llm.py index 5e8946f5..95777fb0 100644 --- a/stripe-horoscope-agent/llm.py +++ b/stripe-horoscope-agent/llm.py @@ -6,6 +6,7 @@ client = OpenAI(api_key=ASI_ONE_API_KEY, base_url="https://api.asi1.ai/v1") + def _normalize_horoscope_output(text: str) -> str: """ Enforce: @@ -22,7 +23,7 @@ def _normalize_horoscope_output(text: str) -> str: # Strip any disclaimer-like lines. cleaned: list[str] = [] for ln in lines: - l = ln.lower() + l = ln.lower() # noqa: E741 if "for entertainment purposes only" in l: continue cleaned.append(ln) @@ -39,13 +40,20 @@ def _normalize_horoscope_output(text: str) -> str: lucky_numbers = ln.split(":", 1)[1].strip() if ":" in ln else None # Handle case where both appear on one line. - if (lucky_color is None or lucky_numbers is None) and ("lucky color:" in joined.lower()): + if (lucky_color is None or lucky_numbers is None) and ( + "lucky color:" in joined.lower() + ): lower = joined.lower() try: lc_idx = lower.rindex("lucky color:") ln_idx = lower.rindex("lucky numbers:") if lc_idx < ln_idx: - lucky_color = joined[lc_idx:].split(":", 1)[1].split("Lucky numbers:", 1)[0].strip() + lucky_color = ( + joined[lc_idx:] + .split(":", 1)[1] + .split("Lucky numbers:", 1)[0] + .strip() + ) lucky_numbers = joined[ln_idx:].split(":", 1)[1].strip() except Exception: pass @@ -53,18 +61,26 @@ def _normalize_horoscope_output(text: str) -> str: # Remove any existing lucky lines from the main body. body_lines = [] for ln in cleaned: - l = ln.lower() + l = ln.lower() # noqa: E741 if "lucky color:" in l or "lucky numbers:" in l: continue body_lines.append(ln) body = "\n".join(body_lines).strip() - lucky_color_line = f"Lucky color: {lucky_color}" if lucky_color else "Lucky color: Blue" + lucky_color_line = ( + f"Lucky color: {lucky_color}" if lucky_color else "Lucky color: Blue" + ) if lucky_numbers: - nums = [n.strip() for n in lucky_numbers.replace(";", ",").split(",") if n.strip()] - lucky_numbers_line = "Lucky numbers: " + ", ".join(nums[:3]) if nums else "Lucky numbers: 7, 19, 42" + nums = [ + n.strip() for n in lucky_numbers.replace(";", ",").split(",") if n.strip() + ] + lucky_numbers_line = ( + "Lucky numbers: " + ", ".join(nums[:3]) + if nums + else "Lucky numbers: 7, 19, 42" + ) else: lucky_numbers_line = "Lucky numbers: 7, 19, 42" @@ -94,7 +110,10 @@ async def generate_horoscope(sign: str) -> str: " Lucky numbers: <3 integers 1-99, comma-separated>" ), }, - {"role": "user", "content": f"My star sign is {sign}. Give me my horoscope of the day."}, + { + "role": "user", + "content": f"My star sign is {sign}. Give me my horoscope of the day.", + }, ], ) return _normalize_horoscope_output((r.choices[0].message.content or "").strip()) @@ -123,4 +142,3 @@ async def normal_reply(user_text: str) -> str: ], ) return (r.choices[0].message.content or "").strip() - diff --git a/stripe-horoscope-agent/payment_proto.py b/stripe-horoscope-agent/payment_proto.py index e1547f29..409b375f 100644 --- a/stripe-horoscope-agent/payment_proto.py +++ b/stripe-horoscope-agent/payment_proto.py @@ -25,4 +25,3 @@ async def _on_reject(ctx: Context, sender: str, msg: RejectPayment): await on_reject(ctx, sender, msg) return proto - diff --git a/stripe-horoscope-agent/state.py b/stripe-horoscope-agent/state.py index c6a9928a..e1f02eed 100644 --- a/stripe-horoscope-agent/state.py +++ b/stripe-horoscope-agent/state.py @@ -82,7 +82,13 @@ def load_state(ctx: Context, sender: str) -> dict: except Exception: return {} - allowed_keys = {"awaiting_sign", "awaiting_payment", "pending_stripe", "sign", "expires_at"} + allowed_keys = { + "awaiting_sign", + "awaiting_payment", + "pending_stripe", + "sign", + "expires_at", + } return {k: state.get(k) for k in allowed_keys if k in state} @@ -94,4 +100,3 @@ def save_state(ctx: Context, sender: str, state: dict) -> None: def clear_state(ctx: Context, sender: str) -> None: ctx.storage.set(state_key(sender), "{}") - diff --git a/stripe-horoscope-agent/stripe_payments.py b/stripe-horoscope-agent/stripe_payments.py index d7ce1e7f..ff43db05 100644 --- a/stripe-horoscope-agent/stripe_payments.py +++ b/stripe-horoscope-agent/stripe_payments.py @@ -25,7 +25,9 @@ def _stripe_expires_at() -> int: return int(time.time()) + expires_in_s -def create_embedded_checkout_session(*, user_address: str, chat_session_id: str, description: str) -> dict: +def create_embedded_checkout_session( + *, user_address: str, chat_session_id: str, description: str +) -> dict: stripe = _get_stripe_sdk() return_url = ( @@ -46,7 +48,10 @@ def create_embedded_checkout_session(*, user_address: str, chat_session_id: str, { "price_data": { "currency": STRIPE_CURRENCY, - "product_data": {"name": STRIPE_PRODUCT_NAME, "description": description}, + "product_data": { + "name": STRIPE_PRODUCT_NAME, + "description": description, + }, "unit_amount": STRIPE_AMOUNT_CENTS, }, "quantity": 1, @@ -76,4 +81,3 @@ def verify_checkout_session_paid(checkout_session_id: str) -> bool: stripe = _get_stripe_sdk() session = stripe.checkout.Session.retrieve(checkout_session_id) return getattr(session, "payment_status", None) == "paid" - diff --git a/stripe-payment-agents/expense-calculator-group/agent.py b/stripe-payment-agents/expense-calculator-group/agent.py index 60ac853e..6073818b 100644 --- a/stripe-payment-agents/expense-calculator-group/agent.py +++ b/stripe-payment-agents/expense-calculator-group/agent.py @@ -4,6 +4,7 @@ - Or add items manually. Then poll (who brought what) and show fair split. - Optional Stripe payment after listing items (same flow as stripe-horoscope-agent). """ + from __future__ import annotations import asyncio @@ -27,6 +28,7 @@ TextContent, chat_protocol_spec, ) + # Payment protocol exists only in uagents-core >= 0.4.0 try: from uagents_core.contrib.protocols.payment import ( @@ -36,6 +38,7 @@ RejectPayment, RequestPayment, ) + _PAYMENT_PROTOCOL_AVAILABLE = True except ModuleNotFoundError: CommitPayment = CompletePayment = Funds = RejectPayment = RequestPayment = None # type: ignore @@ -45,36 +48,51 @@ from dotenv import load_dotenv load_dotenv() # Load .env before config so STRIPE_* keys are available -from config import STRIPE_AMOUNT_CENTS, STRIPE_ENABLED -from expense_logic import ( +from config import STRIPE_AMOUNT_CENTS, STRIPE_ENABLED # noqa: E402 +from expense_logic import ( # noqa: E402 Receipt, ReceiptItem, parse_item_selection, format_split_result, - format_split_summary, format_split_summary_table, format_split_full_table, ) -from receipt_vision import extract_items_from_receipt_image +from receipt_vision import extract_items_from_receipt_image # noqa: E402 if _PAYMENT_PROTOCOL_AVAILABLE: - from stripe_payments import create_embedded_checkout_session, verify_checkout_session_paid + from stripe_payments import ( + create_embedded_checkout_session, + verify_checkout_session_paid, + ) from payment_proto import build_payment_proto else: - create_embedded_checkout_session = verify_checkout_session_paid = build_payment_proto = None # type: ignore + create_embedded_checkout_session = verify_checkout_session_paid = ( + build_payment_proto + ) = None # type: ignore # #region debug log _debug_log_path = "/Users/rutujanemane/Documents/fetchai/cohort 2/.cursor/debug.log" + + def _debug_log(msg: str, data: dict | None = None, hypothesis_id: str = ""): - payload = {"message": msg, "timestamp": __import__("datetime").datetime.now(timezone.utc).isoformat(), "hypothesisId": hypothesis_id} + payload = { + "message": msg, + "timestamp": __import__("datetime").datetime.now(timezone.utc).isoformat(), + "hypothesisId": hypothesis_id, + } if data is not None: - payload["data"] = {k: (v if not isinstance(v, bytes) else f"") for k, v in data.items()} + payload["data"] = { + k: (v if not isinstance(v, bytes) else f"") + for k, v in data.items() + } try: os.makedirs(os.path.dirname(_debug_log_path), exist_ok=True) with open(_debug_log_path, "a") as f: f.write(json.dumps(payload) + "\n") except Exception: pass + + # #endregion # Storage keys @@ -82,9 +100,15 @@ def _debug_log(msg: str, data: dict | None = None, hypothesis_id: str = ""): RECEIPT_ITEMS = "expense_receipt_items" RECEIPT_SELECTIONS = "expense_receipt_selections" RECEIPT_NAMES = "expense_receipt_names" -RECEIPT_NAMES_USER_SET = "expense_receipt_names_user_set" # senders who said "I'm X" (override profile) -RECEIPT_PARTICIPANTS = "expense_receipt_participants" # senders seen in current polling round -RECEIPT_EXPECTED_COUNT = "expense_receipt_expected_count" # optional total participants in group +RECEIPT_NAMES_USER_SET = ( + "expense_receipt_names_user_set" # senders who said "I'm X" (override profile) +) +RECEIPT_PARTICIPANTS = ( + "expense_receipt_participants" # senders seen in current polling round +) +RECEIPT_EXPECTED_COUNT = ( + "expense_receipt_expected_count" # optional total participants in group +) RECEIPT_PAYER_SENDER = "expense_receipt_payer_sender" RECEIPT_PAYER_NAME = "expense_receipt_payer_name" # Per-sender Stripe payment state (awaiting_payment, pending_stripe checkout dict) @@ -117,6 +141,7 @@ def _save_payment_state(ctx: Context, sender: str, data: dict) -> None: def _clear_payment_state(ctx: Context, sender: str) -> None: ctx.storage.set(_payment_state_key(sender), "{}") + agent = Agent( name="ReceiptCalculator", seed=os.getenv("AGENT_SEED", "receipt-calculator-seed"), @@ -238,9 +263,13 @@ def _download_image_bytes( ctx.logger.warning("Downloaded content too small or empty to be a valid image") return None # Basic image check - if content_bytes.startswith(b"\xff\xd8\xff") or content_bytes.startswith(b"\x89PNG"): + if content_bytes.startswith(b"\xff\xd8\xff") or content_bytes.startswith( + b"\x89PNG" + ): return content_bytes - if content_bytes.startswith(b"GIF") or (content_bytes.startswith(b"RIFF") and b"WEBP" in content_bytes[:12]): + if content_bytes.startswith(b"GIF") or ( + content_bytes.startswith(b"RIFF") and b"WEBP" in content_bytes[:12] + ): return content_bytes # Allow unknown as image (e.g. heic or other) return content_bytes @@ -259,16 +288,23 @@ def _load_receipt(ctx: Context) -> tuple[str, Receipt, dict[str, list[int]]]: receipt = Receipt() for d in items_data: receipt.items.append( - ReceiptItem(index=d["index"], name=d["name"], price=Decimal(str(d["price"]))) + ReceiptItem( + index=d["index"], name=d["name"], price=Decimal(str(d["price"])) + ) ) return state, receipt, selections -def _save_receipt(ctx: Context, state: str, receipt: Receipt, selections: dict[str, list[int]]) -> None: +def _save_receipt( + ctx: Context, state: str, receipt: Receipt, selections: dict[str, list[int]] +) -> None: ctx.storage.set(RECEIPT_STATE, state) ctx.storage.set( RECEIPT_ITEMS, - [{"index": i.index, "name": i.name, "price": str(i.price)} for i in receipt.items], + [ + {"index": i.index, "name": i.name, "price": str(i.price)} + for i in receipt.items + ], ) ctx.storage.set(RECEIPT_SELECTIONS, selections) @@ -295,7 +331,9 @@ def _display_name_from_metadata(meta: dict) -> str | None: return None -def _parse_named_selection(raw_text: str, max_idx: int) -> tuple[str, str, list[int], list[int]] | None: +def _parse_named_selection( + raw_text: str, max_idx: int +) -> tuple[str, str, list[int], list[int]] | None: """ Parse user input containing both name and item indices. Supported formats: @@ -368,7 +406,9 @@ def parse_selection(selection_text: str) -> tuple[list[int], list[int]]: return None -def _parse_indices_with_validation(text: str, max_idx: int) -> tuple[list[int], list[int]]: +def _parse_indices_with_validation( + text: str, max_idx: int +) -> tuple[list[int], list[int]]: """Returns (valid_indices, invalid_numbers_out_of_range).""" indices = parse_item_selection(text, max_idx) raw_numbers = sorted({int(x) for x in re.findall(r"\d+", text)}) @@ -421,7 +461,12 @@ def _looks_like_non_selection_command(text: str) -> bool: return True if _extract_group_size_flexible(t) is not None: return True - if _intent_is_pending(t) or _intent_is_start_poll(t) or _intent_is_new_receipt(t) or _intent_is_help(t): + if ( + _intent_is_pending(t) + or _intent_is_start_poll(t) + or _intent_is_new_receipt(t) + or _intent_is_help(t) + ): return True return bool( re.match( @@ -434,27 +479,37 @@ def _looks_like_non_selection_command(text: str) -> bool: def _intent_is_help(text: str) -> bool: t = text.lower() - return any(k in t for k in ("help", "how to", "instructions", "guide", "what can you do")) or t in ("hi", "hello", "hey", "start") + return any( + k in t for k in ("help", "how to", "instructions", "guide", "what can you do") + ) or t in ("hi", "hello", "hey", "start") def _intent_is_new_receipt(text: str) -> bool: t = text.lower().strip() - return t in ("new receipt", "new", "start receipt", "reset") or bool(re.search(r"\b(start|create)\s+(a\s+)?new\s+receipt\b", t)) + return t in ("new receipt", "new", "start receipt", "reset") or bool( + re.search(r"\b(start|create)\s+(a\s+)?new\s+receipt\b", t) + ) def _intent_is_start_poll(text: str) -> bool: t = text.lower() - return t in ("done", "start poll", "poll", "lock") or bool(re.search(r"\b(start|begin)\s+(the\s+)?poll\b", t)) + return t in ("done", "start poll", "poll", "lock") or bool( + re.search(r"\b(start|begin)\s+(the\s+)?poll\b", t) + ) def _intent_is_pending(text: str) -> bool: t = text.lower() - return t in ("pending", "status", "who is left", "who left") or bool(re.search(r"\b(who|what)\s+(is\s+)?left\b", t)) + return t in ("pending", "status", "who is left", "who left") or bool( + re.search(r"\b(who|what)\s+(is\s+)?left\b", t) + ) def _intent_is_show_items(text: str) -> bool: t = text.lower() - return t in ("items", "list", "show items", "receipt") or bool(re.search(r"\b(show|list)\s+(the\s+)?items\b", t)) + return t in ("items", "list", "show items", "receipt") or bool( + re.search(r"\b(show|list)\s+(the\s+)?items\b", t) + ) def _extract_group_size_flexible(text: str) -> int | None: @@ -476,7 +531,13 @@ def _extract_group_size_flexible(text: str) -> int | None: def _extract_calculate_mode(text: str) -> str | None: """Return one of: summary, detailed, table, readable.""" t = text.lower() - if "calculate" not in t and "split" not in t and "summary" not in t and "detailed" not in t and "table" not in t: + if ( + "calculate" not in t + and "split" not in t + and "summary" not in t + and "detailed" not in t + and "table" not in t + ): return None if "summary" in t: return "summary" @@ -549,7 +610,11 @@ def _pending_status_text( def label(sid: str, idx: int) -> str: return names.get(sid) or f"Friend {idx}" - total_expected = expected_count if isinstance(expected_count, int) and expected_count > 0 else len(participants) + total_expected = ( + expected_count + if isinstance(expected_count, int) and expected_count > 0 + else len(participants) + ) lines = ["**Polling status**", ""] lines.append(f"Responded: {len(responded)} / {total_expected}") @@ -565,9 +630,13 @@ def label(sid: str, idx: int) -> str: lines.append("") remaining_unknown = max(total_expected - len(responded), 0) if remaining_unknown > 0: - lines.append(f"Waiting for {remaining_unknown} more participant(s) to reply.") + lines.append( + f"Waiting for {remaining_unknown} more participant(s) to reply." + ) else: - lines.append("Everyone has responded. You can run **calculate summary** or **calculate detailed**.") + lines.append( + "Everyone has responded. You can run **calculate summary** or **calculate detailed**." + ) return "\n".join(lines) @@ -589,9 +658,7 @@ def _build_poll_text(receipt: Receipt) -> str: def _next_step_message(state: str, has_items: bool) -> str: """Return context-aware next-step hint instead of generic 'send a photo'.""" if not has_items: - return ( - "No receipt items yet. You can: (1) **Send a receipt photo** (I’ll extract items from it) or add items manually: **add <name> <price>** (e.g. `add Coffee 3.50`). Type **help** for all commands." - ) + return "No receipt items yet. You can: (1) **Send a receipt photo** (I’ll extract items from it) or add items manually: **add <name> <price>** (e.g. `add Coffee 3.50`). Type **help** for all commands." if state == "draft": return ( "Next: say **done** to start the poll, or add more items with **add <name> <price>**.\n" @@ -645,7 +712,11 @@ async def on_chat(ctx: Context, sender: str, msg: ChatMessage): # If Stripe payment is pending for this sender, re-send RequestPayment unless user wants to cancel/skip/start over. if _PAYMENT_PROTOCOL_AVAILABLE and STRIPE_ENABLED: payment_state = _load_payment_state(ctx, sender) - pending_stripe = payment_state.get("pending_stripe") if isinstance(payment_state.get("pending_stripe"), dict) else None + pending_stripe = ( + payment_state.get("pending_stripe") + if isinstance(payment_state.get("pending_stripe"), dict) + else None + ) if pending_stripe: # Get user text early to allow "cancel" / "new receipt" / "skip" to clear pending and unstick the flow. early_text = "" @@ -653,23 +724,43 @@ async def on_chat(ctx: Context, sender: str, msg: ChatMessage): if isinstance(c, TextContent): early_text = (c.text or "").strip().lower() break - cancel_phrases = ("new receipt", "cancel", "skip payment", "skip", "start over", "help", "no payment", "without payment") + cancel_phrases = ( + "new receipt", + "cancel", + "skip payment", + "skip", + "start over", + "help", + "no payment", + "without payment", + ) if any(p in early_text for p in cancel_phrases): _clear_payment_state(ctx, sender) # Fall through so "new receipt" / "help" / "cancel" are handled below (e.g. new receipt → "✅ New receipt started...") else: req = RequestPayment( - accepted_funds=[Funds(currency="USD", amount=f"{STRIPE_AMOUNT_CENTS / 100:.2f}", payment_method="stripe")], + accepted_funds=[ + Funds( + currency="USD", + amount=f"{STRIPE_AMOUNT_CENTS / 100:.2f}", + payment_method="stripe", + ) + ], recipient=str(ctx.agent.address), deadline_seconds=300, reference=str(ctx.session), description="Pay to unlock receipt split (expense calculator).", - metadata={"stripe": pending_stripe, "service": "expense_calculator"}, + metadata={ + "stripe": pending_stripe, + "service": "expense_calculator", + }, ) await ctx.send(sender, req) await ctx.send( sender, - text_msg("Payment is still pending. Please complete the Stripe checkout above. Once paid, you can say **done** to start the poll."), + text_msg( + "Payment is still pending. Please complete the Stripe checkout above. Once paid, you can say **done** to start the poll." + ), ) return @@ -687,7 +778,9 @@ async def on_chat(ctx: Context, sender: str, msg: ChatMessage): display_name_from_metadata = top_level_name content_list = getattr(msg, "content", None) or [] - session_start_pending = False # Defer so RequestPayment can be first when we have receipt+payment + session_start_pending = ( + False # Defer so RequestPayment can be first when we have receipt+payment + ) for item in content_list: if isinstance(item, MetadataContent): meta = item.metadata or {} @@ -711,10 +804,23 @@ async def on_chat(ctx: Context, sender: str, msg: ChatMessage): # Be flexible and treat any dict that looks like it has a resource # identifier, URI, or inline image data. has_image_keys = any( - key in item for key in ("resource_id", "resourceId", "resource", "uri", "url", "image_url", "data", "contents") + key in item + for key in ( + "resource_id", + "resourceId", + "resource", + "uri", + "url", + "image_url", + "data", + "contents", + ) ) suspected_type = (item.get("type") or "").lower() - if suspected_type in ("resource", "image", "attachment", "file") or has_image_keys: + if ( + suspected_type in ("resource", "image", "attachment", "file") + or has_image_keys + ): saw_attachment = True rid = item.get("resource_id") or item.get("resourceId") res = item.get("resource") @@ -726,7 +832,11 @@ async def on_chat(ctx: Context, sender: str, msg: ChatMessage): ctx.logger.info(f"Received resource (dict): {rid}") data = _download_image_bytes(ctx, str(rid), resources_list) else: - data = _download_image_bytes(ctx, "", resources_list) if resources_list else None + data = ( + _download_image_bytes(ctx, "", resources_list) + if resources_list + else None + ) if data: image_bytes = data if not image_bytes: @@ -775,7 +885,9 @@ async def on_chat(ctx: Context, sender: str, msg: ChatMessage): names = ctx.storage.get(RECEIPT_NAMES) or {} names[sender] = display_name_from_metadata ctx.storage.set(RECEIPT_NAMES, names) - ctx.logger.info(f"Display name from profile for {sender[:12]}…: {display_name_from_metadata[:20]}") + ctx.logger.info( + f"Display name from profile for {sender[:12]}…: {display_name_from_metadata[:20]}" + ) async def _send_session_start_if_pending() -> None: nonlocal session_start_pending @@ -792,7 +904,9 @@ async def _send_session_start_if_pending() -> None: ChatMessage( timestamp=datetime.now(timezone.utc), msg_id=uuid4(), - content=[MetadataContent(type="metadata", metadata={"attachments": "true"})], + content=[ + MetadataContent(type="metadata", metadata={"attachments": "true"}) + ], ), ) await ctx.send(sender, text_msg(WELCOME)) @@ -809,7 +923,9 @@ async def _send_session_start_if_pending() -> None: ctx.logger.exception("Vision extraction failed") await ctx.send( sender, - text_msg(f"Could not read the receipt image. Make sure OPENAI_API_KEY is set. Error: {e}. You can add items manually with **add <name> <price>**."), + text_msg( + f"Could not read the receipt image. Make sure OPENAI_API_KEY is set. Error: {e}. You can add items manually with **add <name> <price>**." + ), ) items_found = [] if items_found: @@ -858,11 +974,21 @@ async def _send_session_start_if_pending() -> None: # If user said done/start poll in same first message, skip to polling immediately start_poll_now = _intent_is_start_poll(text) - _debug_log("receipt from image", {"items_found": len(items_found), "start_poll_now": start_poll_now, "skips_payment_block": start_poll_now}, "H3") + _debug_log( + "receipt from image", + { + "items_found": len(items_found), + "start_poll_now": start_poll_now, + "skips_payment_block": start_poll_now, + }, + "H3", + ) if start_poll_now: _save_receipt(ctx, "polling", receipt, {}) status_line = ", ".join(received_parts) - poll_intro = f"📷 **Read your receipt.**\n\n✅ Received: {status_line}\n\n" + poll_intro = ( + f"📷 **Read your receipt.**\n\n✅ Received: {status_line}\n\n" + ) await ctx.send(sender, text_msg(poll_intro + _build_poll_text(receipt))) await _send_session_start_if_pending() return @@ -891,15 +1017,20 @@ async def _send_session_start_if_pending() -> None: "(for example: `Rutuja paid this bill` or `I paid`)." ) if not group_set: - lines.append("- Optional: set group size for pending accuracy: `group size 3`") - lines.append("- Say **done** to start the poll, or add/edit items if needed") + lines.append( + "- Optional: set group size for pending accuracy: `group size 3`" + ) + lines.append( + "- Say **done** to start the poll, or add/edit items if needed" + ) # Match stripe-horoscope-agent: send RequestPayment *first* so the client shows embedded Stripe UI, then the receipt list. _debug_log( "after receipt list: payment check", { "payment_protocol_available": _PAYMENT_PROTOCOL_AVAILABLE, "stripe_enabled": STRIPE_ENABLED, - "will_request_payment": _PAYMENT_PROTOCOL_AVAILABLE and STRIPE_ENABLED, + "will_request_payment": _PAYMENT_PROTOCOL_AVAILABLE + and STRIPE_ENABLED, "items_count": len(receipt.items), }, "H1_H2_H4", @@ -927,7 +1058,13 @@ async def _send_session_start_if_pending() -> None: ) _save_payment_state(ctx, sender, {"pending_stripe": checkout}) req = RequestPayment( - accepted_funds=[Funds(currency="USD", amount=f"{STRIPE_AMOUNT_CENTS / 100:.2f}", payment_method="stripe")], + accepted_funds=[ + Funds( + currency="USD", + amount=f"{STRIPE_AMOUNT_CENTS / 100:.2f}", + payment_method="stripe", + ) + ], recipient=str(ctx.agent.address), deadline_seconds=300, reference=str(ctx.session), @@ -946,26 +1083,44 @@ async def _send_session_start_if_pending() -> None: ] except Exception: accepted_funds_snapshot = [] - stripe_meta = req.metadata.get("stripe") if isinstance(req.metadata, dict) else None + stripe_meta = ( + req.metadata.get("stripe") + if isinstance(req.metadata, dict) + else None + ) _debug_log( "about_to_send_RequestPayment", { "accepted_funds": accepted_funds_snapshot, - "metadata_keys": sorted(list(req.metadata.keys())) if isinstance(req.metadata, dict) else [], - "stripe_meta_keys": sorted(list(stripe_meta.keys())) if isinstance(stripe_meta, dict) else [], - "service": req.metadata.get("service") if isinstance(req.metadata, dict) else None, + "metadata_keys": sorted(list(req.metadata.keys())) + if isinstance(req.metadata, dict) + else [], + "stripe_meta_keys": sorted(list(stripe_meta.keys())) + if isinstance(stripe_meta, dict) + else [], + "service": req.metadata.get("service") + if isinstance(req.metadata, dict) + else None, }, "H5_FORMAT", ) await ctx.send(sender, req) - await ctx.send(sender, text_msg("\n".join(lines) + "\n\nPlease complete payment above. Once paid, you can say **done** to start the poll.")) + await ctx.send( + sender, + text_msg( + "\n".join(lines) + + "\n\nPlease complete payment above. Once paid, you can say **done** to start the poll." + ), + ) else: await ctx.send(sender, text_msg("\n".join(lines))) await _send_session_start_if_pending() else: await ctx.send( sender, - text_msg("I couldn’t find any line items in that image. Try a clearer photo or add items manually: **add <name> <price>**."), + text_msg( + "I couldn’t find any line items in that image. Try a clearer photo or add items manually: **add <name> <price>**." + ), ) await _send_session_start_if_pending() return @@ -983,7 +1138,9 @@ async def _send_session_start_if_pending() -> None: ) else: state, receipt, _ = _load_receipt(ctx) - await ctx.send(sender, text_msg(_next_step_message(state, bool(receipt.items)))) + await ctx.send( + sender, text_msg(_next_step_message(state, bool(receipt.items))) + ) return state, receipt, selections = _load_receipt(ctx) @@ -998,7 +1155,12 @@ async def _send_session_start_if_pending() -> None: ctx.storage.set(RECEIPT_EXPECTED_COUNT, None) ctx.storage.set(RECEIPT_PAYER_SENDER, None) ctx.storage.set(RECEIPT_PAYER_NAME, None) - await ctx.send(sender, text_msg("✅ New receipt started. Add items with **add <name> <price>** or send a receipt photo.")) + await ctx.send( + sender, + text_msg( + "✅ New receipt started. Add items with **add <name> <price>** or send a receipt photo." + ), + ) return # Payer and/or group size in one message (e.g. "Rutuja paid" or "2 people" or "Rutuja paid 2 people") @@ -1017,7 +1179,12 @@ async def _send_session_start_if_pending() -> None: if (nm or "").strip().lower() == payer_name_from_text.lower(): payer_sender_val = sid break - if not payer_sender_val and names.get(sender) and (names.get(sender) or "").strip().lower() == payer_name_from_text.lower(): + if ( + not payer_sender_val + and names.get(sender) + and (names.get(sender) or "").strip().lower() + == payer_name_from_text.lower() + ): payer_sender_val = sender if expected is not None or payer_name_from_text is not None: @@ -1027,7 +1194,10 @@ async def _send_session_start_if_pending() -> None: await ctx.send(sender, text_msg("Group size must be at least 1.")) return if expected > 50: - await ctx.send(sender, text_msg("Group size is too large. Use a value up to 50.")) + await ctx.send( + sender, + text_msg("Group size is too large. Use a value up to 50."), + ) return ctx.storage.set(RECEIPT_EXPECTED_COUNT, expected) lines.append(f"✅ Expected participants set to **{expected}**.") @@ -1076,15 +1246,27 @@ async def _send_session_start_if_pending() -> None: ), ) except Exception as e: - await ctx.send(sender, text_msg(f"Use: **add ItemName 12.50** (e.g. add Pizza 12). Error: {e}")) + await ctx.send( + sender, + text_msg( + f"Use: **add ItemName 12.50** (e.g. add Pizza 12). Error: {e}" + ), + ) return # Edit/remove item commands - remove_match = re.match(r"(?:remove|delete|del)\s+(?:item\s+)?(\d+)\s*$", text, re.I) + remove_match = re.match( + r"(?:remove|delete|del)\s+(?:item\s+)?(\d+)\s*$", text, re.I + ) if remove_match: idx = int(remove_match.group(1)) if idx < 1 or idx > len(receipt.items): - await ctx.send(sender, text_msg(f"Item {idx} does not exist. Valid range is 1-{len(receipt.items)}.")) + await ctx.send( + sender, + text_msg( + f"Item {idx} does not exist. Valid range is 1-{len(receipt.items)}." + ), + ) return removed = receipt.items.pop(idx - 1) for i, it in enumerate(receipt.items, 1): @@ -1119,7 +1301,12 @@ async def _send_session_start_if_pending() -> None: idx = int(edit_match.group(1)) new_price = Decimal(edit_match.group(2)) if idx < 1 or idx > len(receipt.items): - await ctx.send(sender, text_msg(f"Item {idx} does not exist. Valid range is 1-{len(receipt.items)}.")) + await ctx.send( + sender, + text_msg( + f"Item {idx} does not exist. Valid range is 1-{len(receipt.items)}." + ), + ) return if new_price <= 0: await ctx.send(sender, text_msg("Price must be positive.")) @@ -1204,10 +1391,18 @@ async def _send_session_start_if_pending() -> None: if _intent_is_start_poll(text): if state != "draft": - await ctx.send(sender, text_msg("Poll already started. Reply with your item numbers (e.g. 1,2,3) or **calculate** to see the split.")) + await ctx.send( + sender, + text_msg( + "Poll already started. Reply with your item numbers (e.g. 1,2,3) or **calculate** to see the split." + ), + ) return if not receipt.items: - await ctx.send(sender, text_msg("Add at least one item first, or send a receipt photo.")) + await ctx.send( + sender, + text_msg("Add at least one item first, or send a receipt photo."), + ) return _save_receipt(ctx, "polling", receipt, selections) # Start with empty participant list; add people when they actually submit item selections. @@ -1223,7 +1418,14 @@ async def _send_session_start_if_pending() -> None: # Polling status command if _intent_is_pending(text): - await ctx.send(sender, text_msg(_pending_status_text(names, participants, selections, expected_count))) + await ctx.send( + sender, + text_msg( + _pending_status_text( + names, participants, selections, expected_count + ) + ), + ) return # Do not parse command-like messages as selections. @@ -1310,7 +1512,9 @@ async def _send_session_start_if_pending() -> None: ), ) else: - await ctx.send(sender, text_msg("You do not have a saved selection yet.")) + await ctx.send( + sender, text_msg("You do not have a saved selection yet.") + ) return reset_name_match = re.match(r"reset\s+(.+)$", raw_text, re.I) if reset_name_match and text not in ("reset me", "reset my selection"): @@ -1331,19 +1535,31 @@ async def _send_session_start_if_pending() -> None: ), ) else: - await ctx.send(sender, text_msg("Could not find that name in saved selections.")) + await ctx.send( + sender, text_msg("Could not find that name in saved selections.") + ) return calc_mode = _extract_calculate_mode(text) if calc_mode: if state not in ("polling", "done"): - await ctx.send(sender, text_msg("Add items (or send a receipt photo), say **done**, then have everyone reply with their item numbers. Then say **calculate**.")) + await ctx.send( + sender, + text_msg( + "Add items (or send a receipt photo), say **done**, then have everyone reply with their item numbers. Then say **calculate**." + ), + ) return if not receipt.items: await ctx.send(sender, text_msg("No items on the receipt.")) return if not selections: - await ctx.send(sender, text_msg("No one has replied with their items yet. Ask everyone to reply with numbers (e.g. 1,2,3).")) + await ctx.send( + sender, + text_msg( + "No one has replied with their items yet. Ask everyone to reply with numbers (e.g. 1,2,3)." + ), + ) return expected_count = ctx.storage.get(RECEIPT_EXPECTED_COUNT) if isinstance(expected_count, int) and expected_count > len(selections): @@ -1427,7 +1643,12 @@ async def _send_session_start_if_pending() -> None: if _intent_is_show_items(text): if not receipt.items: - await ctx.send(sender, text_msg("No receipt yet. Send a receipt photo or say **new receipt** then **add <name> <price>**.")) + await ctx.send( + sender, + text_msg( + "No receipt yet. Send a receipt photo or say **new receipt** then **add <name> <price>**." + ), + ) return msg_text = f"**Current receipt ({state}):**\n\n{receipt.format_items()}\n\nTotal: ${receipt.total():.2f}" if state == "polling" and selections: @@ -1444,7 +1665,9 @@ async def _send_session_start_if_pending() -> None: user_set = set(ctx.storage.get(RECEIPT_NAMES_USER_SET) or []) user_set.add(sender) ctx.storage.set(RECEIPT_NAMES_USER_SET, list(user_set)) - await ctx.send(sender, text_msg(f"✅ I'll show you as **{names[sender]}** in the split.")) + await ctx.send( + sender, text_msg(f"✅ I'll show you as **{names[sender]}** in the split.") + ) return if state == "polling": @@ -1463,35 +1686,56 @@ async def on_ack(ctx: Context, sender: str, msg: ChatAcknowledgement): async def on_commit(ctx: Context, sender: str, msg: CommitPayment): if msg.funds.payment_method != "stripe" or not msg.transaction_id: - await ctx.send(sender, RejectPayment(reason="Unsupported payment method (expected stripe).")) + await ctx.send( + sender, + RejectPayment(reason="Unsupported payment method (expected stripe)."), + ) return paid = await asyncio.to_thread(verify_checkout_session_paid, msg.transaction_id) if not paid: - await ctx.send(sender, RejectPayment(reason="Stripe payment not completed yet. Please finish checkout.")) + await ctx.send( + sender, + RejectPayment( + reason="Stripe payment not completed yet. Please finish checkout." + ), + ) return _clear_payment_state(ctx, sender) await ctx.send(sender, CompletePayment(transaction_id=msg.transaction_id)) await ctx.send( sender, - text_msg("Payment received. Say **done** to start the poll, or add/edit items if needed."), + text_msg( + "Payment received. Say **done** to start the poll, or add/edit items if needed." + ), ) async def on_reject(ctx: Context, sender: str, msg: RejectPayment): _clear_payment_state(ctx, sender) - await ctx.send(sender, text_msg(f"Payment was rejected. {msg.reason or ''}".strip())) + await ctx.send( + sender, text_msg(f"Payment was rejected. {msg.reason or ''}".strip()) + ) agent.include(chat_proto, publish_manifest=True) if _PAYMENT_PROTOCOL_AVAILABLE: agent.include(build_payment_proto(on_commit, on_reject), publish_manifest=True) -_debug_log("agent loaded", {"payment_protocol_available": _PAYMENT_PROTOCOL_AVAILABLE, "stripe_enabled": STRIPE_ENABLED}, "H1_H2") +_debug_log( + "agent loaded", + { + "payment_protocol_available": _PAYMENT_PROTOCOL_AVAILABLE, + "stripe_enabled": STRIPE_ENABLED, + }, + "H1_H2", +) if __name__ == "__main__": print("🧾 Receipt / Expense Calculator") - print("Runnable on Agentverse + ASI-One. Send a receipt photo or add items manually.") + print( + "Runnable on Agentverse + ASI-One. Send a receipt photo or add items manually." + ) print("https://asi1.ai | https://agentverse.ai") print("\nPress Ctrl+C to stop\n") agent.run() diff --git a/stripe-payment-agents/expense-calculator-group/config.py b/stripe-payment-agents/expense-calculator-group/config.py index 54be8fa8..ceb1f614 100644 --- a/stripe-payment-agents/expense-calculator-group/config.py +++ b/stripe-payment-agents/expense-calculator-group/config.py @@ -1,4 +1,5 @@ """Environment config. Stripe is optional; receipt flow works without it.""" + import os # Stripe (optional – payment step is skipped if not set) @@ -8,5 +9,10 @@ STRIPE_AMOUNT_CENTS = int(os.getenv("STRIPE_AMOUNT_CENTS", "100")) # $1.00 default STRIPE_CURRENCY = (os.getenv("STRIPE_CURRENCY", "usd") or "usd").strip().lower() -STRIPE_PRODUCT_NAME = (os.getenv("STRIPE_PRODUCT_NAME", "Receipt / expense processing") or "Receipt / expense processing").strip() -STRIPE_SUCCESS_URL = (os.getenv("STRIPE_SUCCESS_URL", "https://agentverse.ai/payment-success") or "").strip() +STRIPE_PRODUCT_NAME = ( + os.getenv("STRIPE_PRODUCT_NAME", "Receipt / expense processing") + or "Receipt / expense processing" +).strip() +STRIPE_SUCCESS_URL = ( + os.getenv("STRIPE_SUCCESS_URL", "https://agentverse.ai/payment-success") or "" +).strip() diff --git a/stripe-payment-agents/expense-calculator-group/expense_logic.py b/stripe-payment-agents/expense-calculator-group/expense_logic.py index c32337a6..436e308d 100644 --- a/stripe-payment-agents/expense-calculator-group/expense_logic.py +++ b/stripe-payment-agents/expense-calculator-group/expense_logic.py @@ -2,6 +2,7 @@ Expense split logic for group receipt calculator. Each item is split only among members who marked they brought that item. """ + from __future__ import annotations from dataclasses import dataclass @@ -12,6 +13,7 @@ @dataclass class ReceiptItem: """Single line item on the receipt.""" + index: int name: str price: Decimal @@ -80,7 +82,9 @@ def compute_splits( for item in receipt.items: item_name_lower = item.name.lower() is_tax_item = any(k in item_name_lower for k in tax_keywords) - who_brought = [sid for sid, indices in selections.items() if item.index in indices] + who_brought = [ + sid for sid, indices in selections.items() if item.index in indices + ] note = None share_group = who_brought @@ -110,7 +114,11 @@ def compute_splits( } for sid in share_group: if sid not in per_person: - per_person[sid] = {"total": Decimal("0"), "breakdown": [], "item_indices": []} + per_person[sid] = { + "total": Decimal("0"), + "breakdown": [], + "item_indices": [], + } per_person[sid]["total"] += share_each per_person[sid]["breakdown"].append((item.name, share_each)) per_person[sid]["item_indices"].append(item.index) @@ -166,7 +174,9 @@ def name(sid: str) -> str: line += f" [{note}]" lines.append(line) else: - lines.append(f"• {item.index}. {item.name} — ${item.price:.2f}\n No one claimed") + lines.append( + f"• {item.index}. {item.name} — ${item.price:.2f}\n No one claimed" + ) lines.append("") lines.append("**Breakdown per person:**") @@ -285,7 +295,9 @@ def name(sid: str) -> str: "|---|---:|---:|", ] for sid, data in sorted(per_person.items(), key=lambda x: -x[1]["total"]): - lines.append(f"| {name(sid)} | ${data['total']:.2f} | {len(data.get('item_indices', []))} |") + lines.append( + f"| {name(sid)} | ${data['total']:.2f} | {len(data.get('item_indices', []))} |" + ) lines.append("") lines.append(f"**Receipt total:** ${receipt.total():.2f}") @@ -341,17 +353,21 @@ def name(sid: str) -> str: "|---|---:|---:|", ] for sid, data in sorted(per_person.items(), key=lambda x: -x[1]["total"]): - lines.append(f"| {name(sid)} | ${data['total']:.2f} | {len(data.get('item_indices', []))} |") + lines.append( + f"| {name(sid)} | ${data['total']:.2f} | {len(data.get('item_indices', []))} |" + ) lines.append("") lines.append(f"**Receipt total:** ${receipt.total():.2f}") lines.append("") - lines.extend([ - "**Per-item split**", - "", - "| # | Item | Price | Shared By | Each | Note |", - "|---:|---|---:|---|---:|---|", - ]) + lines.extend( + [ + "**Per-item split**", + "", + "| # | Item | Price | Shared By | Each | Note |", + "|---:|---|---:|---|---:|---|", + ] + ) for item in receipt.items: info = per_item.get(item.index, {}) shared_by = info.get("shared_by", []) diff --git a/stripe-payment-agents/expense-calculator-group/payment_proto.py b/stripe-payment-agents/expense-calculator-group/payment_proto.py index a4a2f5a2..46bd69e4 100644 --- a/stripe-payment-agents/expense-calculator-group/payment_proto.py +++ b/stripe-payment-agents/expense-calculator-group/payment_proto.py @@ -1,4 +1,5 @@ """Payment protocol (seller role). Same pattern as stripe-horoscope-agent.""" + from __future__ import annotations from typing import Awaitable, Callable diff --git a/stripe-payment-agents/expense-calculator-group/receipt_vision.py b/stripe-payment-agents/expense-calculator-group/receipt_vision.py index 29d61601..7865e45b 100644 --- a/stripe-payment-agents/expense-calculator-group/receipt_vision.py +++ b/stripe-payment-agents/expense-calculator-group/receipt_vision.py @@ -1,6 +1,7 @@ """ Extract line items (name, price) from a receipt image using OpenAI Vision. """ + from __future__ import annotations import base64 @@ -18,6 +19,7 @@ def _get_client(): if _openai_client is None: from openai import OpenAI import os + key = os.getenv("OPENAI_API_KEY") if not key: raise ValueError("OPENAI_API_KEY is required for receipt photo extraction") diff --git a/stripe-payment-agents/expense-calculator-group/stripe_payments.py b/stripe-payment-agents/expense-calculator-group/stripe_payments.py index 80835f09..d56f4446 100644 --- a/stripe-payment-agents/expense-calculator-group/stripe_payments.py +++ b/stripe-payment-agents/expense-calculator-group/stripe_payments.py @@ -1,4 +1,5 @@ """Stripe embedded Checkout: create session and verify payment. Mirrors stripe-horoscope-agent.""" + import os import time @@ -14,6 +15,7 @@ def _get_stripe_sdk(): import stripe # type: ignore + stripe.api_key = STRIPE_SECRET_KEY return stripe @@ -53,7 +55,10 @@ def create_embedded_checkout_session( { "price_data": { "currency": STRIPE_CURRENCY, - "product_data": {"name": STRIPE_PRODUCT_NAME, "description": description}, + "product_data": { + "name": STRIPE_PRODUCT_NAME, + "description": description, + }, "unit_amount": cents, }, "quantity": 1, diff --git a/web3/internet-computer/fetch/agent.py b/web3/internet-computer/fetch/agent.py index 6e1a5dc2..4892f419 100644 --- a/web3/internet-computer/fetch/agent.py +++ b/web3/internet-computer/fetch/agent.py @@ -8,7 +8,7 @@ StartSessionContent, ) from uagents import Agent, Context, Protocol -from datetime import datetime, timezone, timedelta +from datetime import datetime, timezone from uuid import uuid4 # ASI1 API settings @@ -16,16 +16,13 @@ ASI1_BASE_URL = "https://api.asi1.ai/v1" ASI1_HEADERS = { "Authorization": f"Bearer {ASI1_API_KEY}", - "Content-Type": "application/json" + "Content-Type": "application/json", } CANISTER_ID = "uzt4z-lp777-77774-qaabq-cai" BASE_URL = "http://127.0.0.1:4943" -HEADERS = { - "Host": f"{CANISTER_ID}.localhost", - "Content-Type": "application/json" -} +HEADERS = {"Host": f"{CANISTER_ID}.localhost", "Content-Type": "application/json"} # Function definitions for ASI1 function calling tools = [ @@ -38,10 +35,10 @@ "type": "object", "properties": {}, "required": [], - "additionalProperties": False + "additionalProperties": False, }, - "strict": True - } + "strict": True, + }, }, { "type": "function", @@ -53,14 +50,14 @@ "properties": { "address": { "type": "string", - "description": "The Bitcoin address to check." + "description": "The Bitcoin address to check.", } }, "required": ["address"], - "additionalProperties": False + "additionalProperties": False, }, - "strict": True - } + "strict": True, + }, }, { "type": "function", @@ -72,14 +69,14 @@ "properties": { "address": { "type": "string", - "description": "The Bitcoin address to fetch UTXOs for." + "description": "The Bitcoin address to fetch UTXOs for.", } }, "required": ["address"], - "additionalProperties": False + "additionalProperties": False, }, - "strict": True - } + "strict": True, + }, }, { "type": "function", @@ -91,31 +88,36 @@ "properties": { "destinationAddress": { "type": "string", - "description": "The destination Bitcoin address." + "description": "The destination Bitcoin address.", }, "amountInSatoshi": { "type": "number", - "description": "Amount to send in satoshis." - } + "description": "Amount to send in satoshis.", + }, }, "required": ["destinationAddress", "amountInSatoshi"], - "additionalProperties": False + "additionalProperties": False, }, - "strict": True - } - } + "strict": True, + }, + }, ] + async def call_icp_endpoint(func_name: str, args: dict): if func_name == "get_current_fee_percentiles": url = f"{BASE_URL}/get-current-fee-percentiles" response = requests.post(url, headers=HEADERS, json={}, timeout=10) elif func_name == "get_balance": url = f"{BASE_URL}/get-balance" - response = requests.post(url, headers=HEADERS, json={"address": args["address"]}, timeout=10) + response = requests.post( + url, headers=HEADERS, json={"address": args["address"]}, timeout=10 + ) elif func_name == "get_utxos": url = f"{BASE_URL}/get-utxos" - response = requests.post(url, headers=HEADERS, json={"address": args["address"]}, timeout=10) + response = requests.post( + url, headers=HEADERS, json={"address": args["address"]}, timeout=10 + ) elif func_name == "send": url = f"{BASE_URL}/send" response = requests.post(url, headers=HEADERS, json=args, timeout=10) @@ -124,25 +126,23 @@ async def call_icp_endpoint(func_name: str, args: dict): response.raise_for_status() return response.json() + async def process_query(query: str, ctx: Context) -> str: try: # Step 1: Initial call to ASI1 with user query and tools - initial_message = { - "role": "user", - "content": query - } + initial_message = {"role": "user", "content": query} payload = { "model": "asi1-mini", "messages": [initial_message], "tools": tools, "temperature": 0.7, - "max_tokens": 1024 + "max_tokens": 1024, } response = requests.post( f"{ASI1_BASE_URL}/chat/completions", headers=ASI1_HEADERS, json=payload, - timeout=30 + timeout=30, ) response.raise_for_status() response_json = response.json() @@ -168,14 +168,14 @@ async def process_query(query: str, ctx: Context) -> str: except Exception as e: error_content = { "error": f"Tool execution failed: {str(e)}", - "status": "failed" + "status": "failed", } content_to_send = json.dumps(error_content) tool_result_message = { "role": "tool", "tool_call_id": tool_call_id, - "content": content_to_send + "content": content_to_send, } messages_history.append(tool_result_message) @@ -184,13 +184,13 @@ async def process_query(query: str, ctx: Context) -> str: "model": "asi1-mini", "messages": messages_history, "temperature": 0.7, - "max_tokens": 1024 + "max_tokens": 1024, } final_response = requests.post( f"{ASI1_BASE_URL}/chat/completions", headers=ASI1_HEADERS, json=final_payload, - timeout=30 + timeout=30, ) final_response.raise_for_status() final_response_json = final_response.json() @@ -202,19 +202,16 @@ async def process_query(query: str, ctx: Context) -> str: ctx.logger.error(f"Error processing query: {str(e)}") return f"An error occurred while processing your request: {str(e)}" -agent = Agent( - name='test-ICP-agent', - port=8001, - mailbox=True -) + +agent = Agent(name="test-ICP-agent", port=8001, mailbox=True) chat_proto = Protocol(spec=chat_protocol_spec) + @chat_proto.on_message(model=ChatMessage) async def handle_chat_message(ctx: Context, sender: str, msg: ChatMessage): try: ack = ChatAcknowledgement( - timestamp=datetime.now(timezone.utc), - acknowledged_msg_id=msg.msg_id + timestamp=datetime.now(timezone.utc), acknowledged_msg_id=msg.msg_id ) await ctx.send(sender, ack) @@ -229,7 +226,7 @@ async def handle_chat_message(ctx: Context, sender: str, msg: ChatMessage): response = ChatMessage( timestamp=datetime.now(timezone.utc), msg_id=uuid4(), - content=[TextContent(type="text", text=response_text)] + content=[TextContent(type="text", text=response_text)], ) await ctx.send(sender, response) else: @@ -239,16 +236,22 @@ async def handle_chat_message(ctx: Context, sender: str, msg: ChatMessage): error_response = ChatMessage( timestamp=datetime.now(timezone.utc), msg_id=uuid4(), - content=[TextContent(type="text", text=f"An error occurred: {str(e)}")] + content=[TextContent(type="text", text=f"An error occurred: {str(e)}")], ) await ctx.send(sender, error_response) + @chat_proto.on_message(model=ChatAcknowledgement) -async def handle_chat_acknowledgement(ctx: Context, sender: str, msg: ChatAcknowledgement): - ctx.logger.info(f"Received acknowledgement from {sender} for message {msg.acknowledged_msg_id}") +async def handle_chat_acknowledgement( + ctx: Context, sender: str, msg: ChatAcknowledgement +): + ctx.logger.info( + f"Received acknowledgement from {sender} for message {msg.acknowledged_msg_id}" + ) if msg.metadata: ctx.logger.info(f"Metadata: {msg.metadata}") + agent.include(chat_proto) if __name__ == "__main__": @@ -297,4 +300,4 @@ async def handle_chat_acknowledgement(ctx: Context, sender: str, msg: ChatAcknow Can I see a test response? Hit the dummy-test route to make sure it works. -""" \ No newline at end of file +""" diff --git a/web3/singularity-net-metta-fetch-financial-advisor-agent/agent.py b/web3/singularity-net-metta-fetch-financial-advisor-agent/agent.py index cfff3b8b..80d7f7cc 100644 --- a/web3/singularity-net-metta-fetch-financial-advisor-agent/agent.py +++ b/web3/singularity-net-metta-fetch-financial-advisor-agent/agent.py @@ -1,7 +1,5 @@ from datetime import datetime, timezone from uuid import uuid4 -from typing import Any, Dict -import json import os from dotenv import load_dotenv from uagents import Context, Model, Protocol, Agent @@ -22,13 +20,21 @@ load_dotenv() -agent = Agent(name="Financial Investment Advisor", seed="financial-investment-advisor-seed-1", port=8008, mailbox=True, publish_agent_details=True) +agent = Agent( + name="Financial Investment Advisor", + seed="financial-investment-advisor-seed-1", + port=8008, + mailbox=True, + publish_agent_details=True, +) + class InvestmentQuery(Model): query: str intent: str keyword: str + def create_text_chat(text: str, end_session: bool = False) -> ChatMessage: content = [TextContent(type="text", text=text)] if end_session: @@ -39,6 +45,7 @@ def create_text_chat(text: str, end_session: bool = False) -> ChatMessage: content=content, ) + metta = MeTTa() initialize_investment_knowledge(metta) rag = InvestmentRAG(metta) @@ -46,12 +53,15 @@ def create_text_chat(text: str, end_session: bool = False) -> ChatMessage: chat_proto = Protocol(spec=chat_protocol_spec) + @chat_proto.on_message(ChatMessage) async def handle_message(ctx: Context, sender: str, msg: ChatMessage): ctx.storage.set(str(ctx.session), sender) await ctx.send( sender, - ChatAcknowledgement(timestamp=datetime.now(timezone.utc), acknowledged_msg_id=msg.msg_id), + ChatAcknowledgement( + timestamp=datetime.now(timezone.utc), acknowledged_msg_id=msg.msg_id + ), ) for item in msg.content: @@ -61,31 +71,40 @@ async def handle_message(ctx: Context, sender: str, msg: ChatMessage): elif isinstance(item, TextContent): user_query = item.text.strip() ctx.logger.info(f"Got an investment query from {sender}: {user_query}") - + try: response = process_query(user_query, rag, llm) - + if isinstance(response, dict): - answer_text = response.get('humanized_answer', 'I apologize, but I could not process your query.') + answer_text = response.get( + "humanized_answer", + "I apologize, but I could not process your query.", + ) else: answer_text = str(response) - + await ctx.send(sender, create_text_chat(answer_text)) - + except Exception as e: ctx.logger.error(f"Error processing investment query: {e}") await ctx.send( - sender, - create_text_chat("I apologize, but I encountered an error processing your investment query. Please try again.") + sender, + create_text_chat( + "I apologize, but I encountered an error processing your investment query. Please try again." + ), ) else: ctx.logger.info(f"Got unexpected content from {sender}") + @chat_proto.on_message(ChatAcknowledgement) async def handle_ack(ctx: Context, sender: str, msg: ChatAcknowledgement): - ctx.logger.info(f"Got an acknowledgement from {sender} for {msg.acknowledged_msg_id}") + ctx.logger.info( + f"Got an acknowledgement from {sender} for {msg.acknowledged_msg_id}" + ) + agent.include(chat_proto, publish_manifest=True) if __name__ == "__main__": - agent.run() \ No newline at end of file + agent.run() diff --git a/web3/singularity-net-metta-fetch-financial-advisor-agent/metta/investment_rag.py b/web3/singularity-net-metta-fetch-financial-advisor-agent/metta/investment_rag.py index 8b8106ec..72e33832 100644 --- a/web3/singularity-net-metta-fetch-financial-advisor-agent/metta/investment_rag.py +++ b/web3/singularity-net-metta-fetch-financial-advisor-agent/metta/investment_rag.py @@ -1,6 +1,6 @@ -import re from hyperon import MeTTa, E, S, ValueAtom + class InvestmentRAG: def __init__(self, metta_instance: MeTTa): self.metta = metta_instance @@ -8,65 +8,95 @@ def __init__(self, metta_instance: MeTTa): def query_risk_profile(self, risk_profile): """Find investment types suitable for a risk profile.""" risk_profile = risk_profile.strip('"') - query_str = f'!(match &self (risk_profile {risk_profile} $investment) $investment)' + query_str = ( + f"!(match &self (risk_profile {risk_profile} $investment) $investment)" + ) results = self.metta.run(query_str) print(results, query_str) - unique_investments = list(set(str(r[0]) for r in results if r and len(r) > 0)) if results else [] + unique_investments = ( + list(set(str(r[0]) for r in results if r and len(r) > 0)) if results else [] + ) return unique_investments def get_expected_return(self, investment): """Find expected returns for an investment type.""" investment = investment.strip('"') - query_str = f'!(match &self (expected_return {investment} $return) $return)' + query_str = f"!(match &self (expected_return {investment} $return) $return)" results = self.metta.run(query_str) print(results, query_str) - return [r[0].get_object().value for r in results if r and len(r) > 0] if results else [] + return ( + [r[0].get_object().value for r in results if r and len(r) > 0] + if results + else [] + ) def get_risk_level(self, investment): """Find risk level of an investment type.""" investment = investment.strip('"') - query_str = f'!(match &self (risk_level {investment} $risk) $risk)' + query_str = f"!(match &self (risk_level {investment} $risk) $risk)" results = self.metta.run(query_str) print(results, query_str) - return [r[0].get_object().value for r in results if r and len(r) > 0] if results else [] + return ( + [r[0].get_object().value for r in results if r and len(r) > 0] + if results + else [] + ) def get_age_allocation(self, age_group): """Get recommended asset allocation for age group.""" age_group = age_group.strip('"') - query_str = f'!(match &self (age_allocation {age_group} $allocation) $allocation)' + query_str = ( + f"!(match &self (age_allocation {age_group} $allocation) $allocation)" + ) results = self.metta.run(query_str) print(results, query_str) - return [r[0].get_object().value for r in results if r and len(r) > 0] if results else [] + return ( + [r[0].get_object().value for r in results if r and len(r) > 0] + if results + else [] + ) def get_goal_strategy(self, goal): """Get investment strategy for a specific goal.""" goal = goal.strip('"') - query_str = f'!(match &self (goal_strategy {goal} $strategy) $strategy)' + query_str = f"!(match &self (goal_strategy {goal} $strategy) $strategy)" results = self.metta.run(query_str) print(results, query_str) - return [r[0].get_object().value for r in results if r and len(r) > 0] if results else [] + return ( + [r[0].get_object().value for r in results if r and len(r) > 0] + if results + else [] + ) def query_sector_stocks(self, sector): """Get top performing stocks in a sector.""" sector = sector.strip('"') - query_str = f'!(match &self (sector_stocks {sector} $stocks) $stocks)' + query_str = f"!(match &self (sector_stocks {sector} $stocks) $stocks)" results = self.metta.run(query_str) print(results, query_str) - return [r[0].get_object().value for r in results if r and len(r) > 0] if results else [] + return ( + [r[0].get_object().value for r in results if r and len(r) > 0] + if results + else [] + ) def get_mistake_warning(self, mistake): """Get warning about common investment mistakes.""" mistake = mistake.strip('"') - query_str = f'!(match &self (mistake {mistake} $warning) $warning)' + query_str = f"!(match &self (mistake {mistake} $warning) $warning)" results = self.metta.run(query_str) print(results, query_str) - return [r[0].get_object().value for r in results if r and len(r) > 0] if results else [] + return ( + [r[0].get_object().value for r in results if r and len(r) > 0] + if results + else [] + ) def query_faq(self, question): """Retrieve investment FAQ answers.""" @@ -81,4 +111,4 @@ def add_knowledge(self, relation_type, subject, object_value): if isinstance(object_value, str): object_value = ValueAtom(object_value) self.metta.space().add_atom(E(S(relation_type), S(subject), object_value)) - return f"Added {relation_type}: {subject} → {object_value}" \ No newline at end of file + return f"Added {relation_type}: {subject} → {object_value}" diff --git a/web3/singularity-net-metta-fetch-financial-advisor-agent/metta/knowledge.py b/web3/singularity-net-metta-fetch-financial-advisor-agent/metta/knowledge.py index 3ee4e3c1..7566fefa 100644 --- a/web3/singularity-net-metta-fetch-financial-advisor-agent/metta/knowledge.py +++ b/web3/singularity-net-metta-fetch-financial-advisor-agent/metta/knowledge.py @@ -1,69 +1,226 @@ from hyperon import MeTTa, E, S, ValueAtom + def initialize_investment_knowledge(metta: MeTTa): """Initialize the MeTTa knowledge graph with investment, risk, portfolio, and strategy data.""" - + # Risk Profile → Investment Types metta.space().add_atom(E(S("risk_profile"), S("conservative"), S("bonds"))) - metta.space().add_atom(E(S("risk_profile"), S("conservative"), S("dividend_stocks"))) + metta.space().add_atom( + E(S("risk_profile"), S("conservative"), S("dividend_stocks")) + ) metta.space().add_atom(E(S("risk_profile"), S("moderate"), S("index_funds"))) metta.space().add_atom(E(S("risk_profile"), S("moderate"), S("etfs"))) metta.space().add_atom(E(S("risk_profile"), S("aggressive"), S("growth_stocks"))) metta.space().add_atom(E(S("risk_profile"), S("aggressive"), S("cryptocurrency"))) - metta.space().add_atom(E(S("risk_profile"), S("conservative"), S("savings_accounts"))) + metta.space().add_atom( + E(S("risk_profile"), S("conservative"), S("savings_accounts")) + ) metta.space().add_atom(E(S("risk_profile"), S("moderate"), S("real_estate"))) metta.space().add_atom(E(S("risk_profile"), S("aggressive"), S("options"))) - + # Investment Types → Expected Returns - metta.space().add_atom(E(S("expected_return"), S("bonds"), ValueAtom("3-5% annually"))) - metta.space().add_atom(E(S("expected_return"), S("dividend_stocks"), ValueAtom("5-7% annually"))) - metta.space().add_atom(E(S("expected_return"), S("index_funds"), ValueAtom("6-10% annually"))) - metta.space().add_atom(E(S("expected_return"), S("etfs"), ValueAtom("5-12% annually"))) - metta.space().add_atom(E(S("expected_return"), S("growth_stocks"), ValueAtom("8-15% annually"))) - metta.space().add_atom(E(S("expected_return"), S("cryptocurrency"), ValueAtom("highly volatile, -50% to +200%"))) - metta.space().add_atom(E(S("expected_return"), S("savings_accounts"), ValueAtom("1-2% annually"))) - metta.space().add_atom(E(S("expected_return"), S("real_estate"), ValueAtom("4-8% annually"))) - metta.space().add_atom(E(S("expected_return"), S("options"), ValueAtom("high risk, unlimited gains/losses"))) - + metta.space().add_atom( + E(S("expected_return"), S("bonds"), ValueAtom("3-5% annually")) + ) + metta.space().add_atom( + E(S("expected_return"), S("dividend_stocks"), ValueAtom("5-7% annually")) + ) + metta.space().add_atom( + E(S("expected_return"), S("index_funds"), ValueAtom("6-10% annually")) + ) + metta.space().add_atom( + E(S("expected_return"), S("etfs"), ValueAtom("5-12% annually")) + ) + metta.space().add_atom( + E(S("expected_return"), S("growth_stocks"), ValueAtom("8-15% annually")) + ) + metta.space().add_atom( + E( + S("expected_return"), + S("cryptocurrency"), + ValueAtom("highly volatile, -50% to +200%"), + ) + ) + metta.space().add_atom( + E(S("expected_return"), S("savings_accounts"), ValueAtom("1-2% annually")) + ) + metta.space().add_atom( + E(S("expected_return"), S("real_estate"), ValueAtom("4-8% annually")) + ) + metta.space().add_atom( + E( + S("expected_return"), + S("options"), + ValueAtom("high risk, unlimited gains/losses"), + ) + ) + # Investment Types → Risk Levels - metta.space().add_atom(E(S("risk_level"), S("bonds"), ValueAtom("low risk, stable income"))) - metta.space().add_atom(E(S("risk_level"), S("dividend_stocks"), ValueAtom("low-moderate risk, regular dividends"))) - metta.space().add_atom(E(S("risk_level"), S("index_funds"), ValueAtom("moderate risk, diversified"))) - metta.space().add_atom(E(S("risk_level"), S("etfs"), ValueAtom("low-moderate risk, liquid"))) - metta.space().add_atom(E(S("risk_level"), S("growth_stocks"), ValueAtom("high risk, growth potential"))) - metta.space().add_atom(E(S("risk_level"), S("cryptocurrency"), ValueAtom("very high risk, extreme volatility"))) - metta.space().add_atom(E(S("risk_level"), S("savings_accounts"), ValueAtom("no risk, FDIC insured"))) - metta.space().add_atom(E(S("risk_level"), S("real_estate"), ValueAtom("moderate risk, inflation hedge"))) - metta.space().add_atom(E(S("risk_level"), S("options"), ValueAtom("very high risk, leveraged exposure"))) - + metta.space().add_atom( + E(S("risk_level"), S("bonds"), ValueAtom("low risk, stable income")) + ) + metta.space().add_atom( + E( + S("risk_level"), + S("dividend_stocks"), + ValueAtom("low-moderate risk, regular dividends"), + ) + ) + metta.space().add_atom( + E(S("risk_level"), S("index_funds"), ValueAtom("moderate risk, diversified")) + ) + metta.space().add_atom( + E(S("risk_level"), S("etfs"), ValueAtom("low-moderate risk, liquid")) + ) + metta.space().add_atom( + E(S("risk_level"), S("growth_stocks"), ValueAtom("high risk, growth potential")) + ) + metta.space().add_atom( + E( + S("risk_level"), + S("cryptocurrency"), + ValueAtom("very high risk, extreme volatility"), + ) + ) + metta.space().add_atom( + E(S("risk_level"), S("savings_accounts"), ValueAtom("no risk, FDIC insured")) + ) + metta.space().add_atom( + E( + S("risk_level"), + S("real_estate"), + ValueAtom("moderate risk, inflation hedge"), + ) + ) + metta.space().add_atom( + E( + S("risk_level"), + S("options"), + ValueAtom("very high risk, leveraged exposure"), + ) + ) + # Age Group → Recommended Asset Allocation - metta.space().add_atom(E(S("age_allocation"), S("20s"), ValueAtom("80% stocks, 20% bonds"))) - metta.space().add_atom(E(S("age_allocation"), S("30s"), ValueAtom("70% stocks, 30% bonds"))) - metta.space().add_atom(E(S("age_allocation"), S("40s"), ValueAtom("60% stocks, 40% bonds"))) - metta.space().add_atom(E(S("age_allocation"), S("50s"), ValueAtom("50% stocks, 50% bonds"))) - metta.space().add_atom(E(S("age_allocation"), S("60s"), ValueAtom("40% stocks, 60% bonds"))) - + metta.space().add_atom( + E(S("age_allocation"), S("20s"), ValueAtom("80% stocks, 20% bonds")) + ) + metta.space().add_atom( + E(S("age_allocation"), S("30s"), ValueAtom("70% stocks, 30% bonds")) + ) + metta.space().add_atom( + E(S("age_allocation"), S("40s"), ValueAtom("60% stocks, 40% bonds")) + ) + metta.space().add_atom( + E(S("age_allocation"), S("50s"), ValueAtom("50% stocks, 50% bonds")) + ) + metta.space().add_atom( + E(S("age_allocation"), S("60s"), ValueAtom("40% stocks, 60% bonds")) + ) + # Investment Goals → Strategies - metta.space().add_atom(E(S("goal_strategy"), S("retirement"), ValueAtom("diversified index funds, 401k maxing"))) - metta.space().add_atom(E(S("goal_strategy"), S("emergency_fund"), ValueAtom("high-yield savings, money market"))) - metta.space().add_atom(E(S("goal_strategy"), S("house_down_payment"), ValueAtom("CDs, short-term bonds"))) - metta.space().add_atom(E(S("goal_strategy"), S("wealth_building"), ValueAtom("growth stocks, REITs"))) - metta.space().add_atom(E(S("goal_strategy"), S("passive_income"), ValueAtom("dividend stocks, bonds"))) - + metta.space().add_atom( + E( + S("goal_strategy"), + S("retirement"), + ValueAtom("diversified index funds, 401k maxing"), + ) + ) + metta.space().add_atom( + E( + S("goal_strategy"), + S("emergency_fund"), + ValueAtom("high-yield savings, money market"), + ) + ) + metta.space().add_atom( + E( + S("goal_strategy"), + S("house_down_payment"), + ValueAtom("CDs, short-term bonds"), + ) + ) + metta.space().add_atom( + E(S("goal_strategy"), S("wealth_building"), ValueAtom("growth stocks, REITs")) + ) + metta.space().add_atom( + E(S("goal_strategy"), S("passive_income"), ValueAtom("dividend stocks, bonds")) + ) + # Market Sectors → Top Performers - metta.space().add_atom(E(S("sector_stocks"), S("technology"), ValueAtom("Apple, Microsoft, Google"))) - metta.space().add_atom(E(S("sector_stocks"), S("healthcare"), ValueAtom("Johnson & Johnson, Pfizer"))) - metta.space().add_atom(E(S("sector_stocks"), S("finance"), ValueAtom("JPMorgan Chase, Berkshire Hathaway"))) - metta.space().add_atom(E(S("sector_stocks"), S("energy"), ValueAtom("ExxonMobil, Chevron"))) - + metta.space().add_atom( + E(S("sector_stocks"), S("technology"), ValueAtom("Apple, Microsoft, Google")) + ) + metta.space().add_atom( + E(S("sector_stocks"), S("healthcare"), ValueAtom("Johnson & Johnson, Pfizer")) + ) + metta.space().add_atom( + E( + S("sector_stocks"), + S("finance"), + ValueAtom("JPMorgan Chase, Berkshire Hathaway"), + ) + ) + metta.space().add_atom( + E(S("sector_stocks"), S("energy"), ValueAtom("ExxonMobil, Chevron")) + ) + # Common Investment Mistakes → Warnings - metta.space().add_atom(E(S("mistake"), S("timing_market"), ValueAtom("avoid trying to time market peaks and valleys"))) - metta.space().add_atom(E(S("mistake"), S("lack_diversification"), ValueAtom("don't put all money in one stock or sector"))) - metta.space().add_atom(E(S("mistake"), S("emotional_trading"), ValueAtom("avoid panic selling or FOMO buying"))) - metta.space().add_atom(E(S("mistake"), S("high_fees"), ValueAtom("watch out for expensive mutual fund fees"))) - + metta.space().add_atom( + E( + S("mistake"), + S("timing_market"), + ValueAtom("avoid trying to time market peaks and valleys"), + ) + ) + metta.space().add_atom( + E( + S("mistake"), + S("lack_diversification"), + ValueAtom("don't put all money in one stock or sector"), + ) + ) + metta.space().add_atom( + E( + S("mistake"), + S("emotional_trading"), + ValueAtom("avoid panic selling or FOMO buying"), + ) + ) + metta.space().add_atom( + E( + S("mistake"), + S("high_fees"), + ValueAtom("watch out for expensive mutual fund fees"), + ) + ) + # Investment FAQs - metta.space().add_atom(E(S("faq"), S("How much should I invest?"), ValueAtom("Invest 10-20% of income after emergency fund"))) - metta.space().add_atom(E(S("faq"), S("When should I start investing?"), ValueAtom("Start as early as possible for compound growth"))) - metta.space().add_atom(E(S("faq"), S("What is diversification?"), ValueAtom("Spreading investments across different assets to reduce risk"))) - metta.space().add_atom(E(S("faq"), S("Should I pay off debt first?"), ValueAtom("Pay off high-interest debt before investing"))) \ No newline at end of file + metta.space().add_atom( + E( + S("faq"), + S("How much should I invest?"), + ValueAtom("Invest 10-20% of income after emergency fund"), + ) + ) + metta.space().add_atom( + E( + S("faq"), + S("When should I start investing?"), + ValueAtom("Start as early as possible for compound growth"), + ) + ) + metta.space().add_atom( + E( + S("faq"), + S("What is diversification?"), + ValueAtom("Spreading investments across different assets to reduce risk"), + ) + ) + metta.space().add_atom( + E( + S("faq"), + S("Should I pay off debt first?"), + ValueAtom("Pay off high-interest debt before investing"), + ) + ) diff --git a/web3/singularity-net-metta-fetch.ai/agent.py b/web3/singularity-net-metta-fetch.ai/agent.py index 27b3c3ad..7faeb882 100644 --- a/web3/singularity-net-metta-fetch.ai/agent.py +++ b/web3/singularity-net-metta-fetch.ai/agent.py @@ -1,8 +1,5 @@ from datetime import datetime, timezone -import mailbox from uuid import uuid4 -from typing import Any, Dict -import json import os from dotenv import load_dotenv from uagents import Context, Model, Protocol, Agent @@ -26,13 +23,20 @@ load_dotenv() # Initialize agent -agent = Agent(name="Fetch.ai uAgents Assistant", port=8005, mailbox=True, publish_agent_details=True) +agent = Agent( + name="Fetch.ai uAgents Assistant", + port=8005, + mailbox=True, + publish_agent_details=True, +) + class GeneralQuery(Model): query: str intent: str keyword: str + def create_text_chat(text: str, end_session: bool = False) -> ChatMessage: """Create a text chat message.""" content = [TextContent(type="text", text=text)] @@ -44,6 +48,7 @@ def create_text_chat(text: str, end_session: bool = False) -> ChatMessage: content=content, ) + # Initialize global components metta = MeTTa() initialize_knowledge_graph(metta) @@ -53,13 +58,16 @@ def create_text_chat(text: str, end_session: bool = False) -> ChatMessage: # Protocol setup chat_proto = Protocol(spec=chat_protocol_spec) + @chat_proto.on_message(ChatMessage) async def handle_message(ctx: Context, sender: str, msg: ChatMessage): """Handle incoming chat messages and process Fetch.ai/uAgents queries.""" ctx.storage.set(str(ctx.session), sender) await ctx.send( sender, - ChatAcknowledgement(timestamp=datetime.now(timezone.utc), acknowledged_msg_id=msg.msg_id), + ChatAcknowledgement( + timestamp=datetime.now(timezone.utc), acknowledged_msg_id=msg.msg_id + ), ) for item in msg.content: @@ -69,33 +77,39 @@ async def handle_message(ctx: Context, sender: str, msg: ChatMessage): elif isinstance(item, TextContent): user_query = item.text.strip() ctx.logger.info(f"Got a Fetch.ai/uAgents query from {sender}: {user_query}") - + try: # Process the query using the general assistant logic response = process_query(user_query, rag, llm) - + # Format the response if isinstance(response, dict): answer_text = f"**{response.get('selected_question', user_query)}**\n\n{response.get('humanized_answer', 'I apologize, but I could not process your query.')}" else: answer_text = str(response) - + # Send the response back await ctx.send(sender, create_text_chat(answer_text)) - + except Exception as e: ctx.logger.error(f"Error processing Fetch.ai/uAgents query: {e}") await ctx.send( - sender, - create_text_chat("I apologize, but I encountered an error processing your Fetch.ai/uAgents query. Please try again.") + sender, + create_text_chat( + "I apologize, but I encountered an error processing your Fetch.ai/uAgents query. Please try again." + ), ) else: ctx.logger.info(f"Got unexpected content from {sender}") + @chat_proto.on_message(ChatAcknowledgement) async def handle_ack(ctx: Context, sender: str, msg: ChatAcknowledgement): """Handle chat acknowledgements.""" - ctx.logger.info(f"Got an acknowledgement from {sender} for {msg.acknowledged_msg_id}") + ctx.logger.info( + f"Got an acknowledgement from {sender} for {msg.acknowledged_msg_id}" + ) + # Register the protocol agent.include(chat_proto, publish_manifest=True) diff --git a/web3/singularity-net-metta-fetch.ai/metta/knowledge.py b/web3/singularity-net-metta-fetch.ai/metta/knowledge.py index 2b928744..65f46573 100644 --- a/web3/singularity-net-metta-fetch.ai/metta/knowledge.py +++ b/web3/singularity-net-metta-fetch.ai/metta/knowledge.py @@ -1,9 +1,10 @@ # knowledge.py from hyperon import MeTTa, E, S, ValueAtom + def initialize_knowledge_graph(metta: MeTTa): """Initialize the MeTTa knowledge graph with comprehensive Fetch.ai/uAgents knowledge for real-time queries.""" - + # Agent Types → Capabilities metta.space().add_atom(E(S("capability"), S("uAgent"), S("microservice"))) metta.space().add_atom(E(S("capability"), S("uAgent"), S("message handling"))) @@ -14,204 +15,852 @@ def initialize_knowledge_graph(metta: MeTTa): metta.space().add_atom(E(S("capability"), S("Agentverse"), S("agent marketplace"))) metta.space().add_atom(E(S("capability"), S("Agentverse"), S("agent discovery"))) metta.space().add_atom(E(S("capability"), S("Agentverse"), S("agent deployment"))) - + # Communication Patterns → Methods metta.space().add_atom(E(S("communication"), S("ctx.send"), S("asynchronous"))) metta.space().add_atom(E(S("communication"), S("ctx.send"), S("fire and forget"))) - metta.space().add_atom(E(S("communication"), S("ctx.send_and_receive"), S("synchronous"))) - metta.space().add_atom(E(S("communication"), S("ctx.send_and_receive"), S("request-response"))) - metta.space().add_atom(E(S("communication"), S("chat protocol"), S("structured messaging"))) - metta.space().add_atom(E(S("communication"), S("chat protocol"), S("acknowledgments"))) - metta.space().add_atom(E(S("communication"), S("REST endpoints"), S("HTTP integration"))) - + metta.space().add_atom( + E(S("communication"), S("ctx.send_and_receive"), S("synchronous")) + ) + metta.space().add_atom( + E(S("communication"), S("ctx.send_and_receive"), S("request-response")) + ) + metta.space().add_atom( + E(S("communication"), S("chat protocol"), S("structured messaging")) + ) + metta.space().add_atom( + E(S("communication"), S("chat protocol"), S("acknowledgments")) + ) + metta.space().add_atom( + E(S("communication"), S("REST endpoints"), S("HTTP integration")) + ) + # Agent Deployment → Types - metta.space().add_atom(E(S("deployment"), S("hosted agents"), S("Agentverse managed"))) + metta.space().add_atom( + E(S("deployment"), S("hosted agents"), S("Agentverse managed")) + ) metta.space().add_atom(E(S("deployment"), S("local agents"), S("self-hosted"))) - metta.space().add_atom(E(S("deployment"), S("mailbox agents"), S("hybrid deployment"))) + metta.space().add_atom( + E(S("deployment"), S("mailbox agents"), S("hybrid deployment")) + ) metta.space().add_atom(E(S("deployment"), S("hosted agents"), S("always running"))) - metta.space().add_atom(E(S("deployment"), S("local agents"), S("full library access"))) - metta.space().add_atom(E(S("deployment"), S("mailbox agents"), S("Agentverse integration"))) - + metta.space().add_atom( + E(S("deployment"), S("local agents"), S("full library access")) + ) + metta.space().add_atom( + E(S("deployment"), S("mailbox agents"), S("Agentverse integration")) + ) + # Framework Integration → Adapters metta.space().add_atom(E(S("adapter"), S("LangChain"), S("uAgents ecosystem"))) metta.space().add_atom(E(S("adapter"), S("LangGraph"), S("orchestration"))) metta.space().add_atom(E(S("adapter"), S("CrewAI"), S("multi-agent collaboration"))) metta.space().add_atom(E(S("adapter"), S("A2A inbound"), S("external A2A clients"))) - metta.space().add_atom(E(S("adapter"), S("A2A outbound"), S("A2A agent registration"))) - + metta.space().add_atom( + E(S("adapter"), S("A2A outbound"), S("A2A agent registration")) + ) + # Blockchain Features → Benefits - metta.space().add_atom(E(S("blockchain"), S("decentralization"), S("trustless interactions"))) - metta.space().add_atom(E(S("blockchain"), S("transparency"), S("auditable transactions"))) - metta.space().add_atom(E(S("blockchain"), S("smart contracts"), S("automated execution"))) + metta.space().add_atom( + E(S("blockchain"), S("decentralization"), S("trustless interactions")) + ) + metta.space().add_atom( + E(S("blockchain"), S("transparency"), S("auditable transactions")) + ) + metta.space().add_atom( + E(S("blockchain"), S("smart contracts"), S("automated execution")) + ) metta.space().add_atom(E(S("blockchain"), S("Almanac"), S("agent registry"))) metta.space().add_atom(E(S("blockchain"), S("transactions"), S("secure payments"))) - + # Agentverse Features → Capabilities metta.space().add_atom(E(S("capability"), S("Agentverse"), S("continuous uptime"))) metta.space().add_atom(E(S("capability"), S("Agentverse"), S("easy deployment"))) - metta.space().add_atom(E(S("capability"), S("Agentverse"), S("blockchain integration"))) - metta.space().add_atom(E(S("capability"), S("Agentverse"), S("marketplace discovery"))) + metta.space().add_atom( + E(S("capability"), S("Agentverse"), S("blockchain integration")) + ) + metta.space().add_atom( + E(S("capability"), S("Agentverse"), S("marketplace discovery")) + ) metta.space().add_atom(E(S("capability"), S("Agentverse"), S("mailroom service"))) metta.space().add_atom(E(S("capability"), S("Agentverse"), S("integrated IDE"))) metta.space().add_atom(E(S("capability"), S("Agentverse"), S("agent search"))) metta.space().add_atom(E(S("capability"), S("Agentverse"), S("agent registration"))) # ASI:One Specific Models metta.space().add_atom(E(S("specificInstance"), S("ASI:One"), S("asi1-mini"))) - metta.space().add_atom(E(S("specificInstance"),S("ASI:One"), S("asi1-fast"))) + metta.space().add_atom(E(S("specificInstance"), S("ASI:One"), S("asi1-fast"))) - metta.space().add_atom(E(S("specificInstance"),S("ASI:One"), S("asi1-extended"))) + metta.space().add_atom(E(S("specificInstance"), S("ASI:One"), S("asi1-extended"))) - metta.space().add_atom(E(S("specificInstance"),S("ASI:One"), S("asi1-agentic"))) + metta.space().add_atom(E(S("specificInstance"), S("ASI:One"), S("asi1-agentic"))) - metta.space().add_atom(E(S("specificInstance"),S("ASI:One"), S("asi1-graph"))) + metta.space().add_atom(E(S("specificInstance"), S("ASI:One"), S("asi1-graph"))) # ASI:One Models → Features - metta.space().add_atom(E(S("capability"), S("asi1-mini"), S("balanced performance"))) + metta.space().add_atom( + E(S("capability"), S("asi1-mini"), S("balanced performance")) + ) metta.space().add_atom(E(S("capability"), S("asi1-mini"), S("speed optimization"))) metta.space().add_atom(E(S("capability"), S("asi1-fast"), S("quick responses"))) metta.space().add_atom(E(S("capability"), S("asi1-extended"), S("complex tasks"))) - metta.space().add_atom(E(S("capability"), S("asi1-agentic"), S("agent interactions"))) + metta.space().add_atom( + E(S("capability"), S("asi1-agentic"), S("agent interactions")) + ) metta.space().add_atom(E(S("capability"), S("asi1-graph"), S("data analytics"))) metta.space().add_atom(E(S("capability"), S("asi1-graph"), S("graph optimization"))) - + # ASI:One Core Features metta.space().add_atom(E(S("capability"), S("ASI:One"), S("agentic reasoning"))) - metta.space().add_atom(E(S("capability"), S("ASI:One"), S("natural language understanding"))) - metta.space().add_atom(E(S("capability"), S("ASI:One"), S("multi-step task execution"))) + metta.space().add_atom( + E(S("capability"), S("ASI:One"), S("natural language understanding")) + ) + metta.space().add_atom( + E(S("capability"), S("ASI:One"), S("multi-step task execution")) + ) metta.space().add_atom(E(S("capability"), S("ASI:One"), S("contextual memory"))) - metta.space().add_atom(E(S("capability"), S("ASI:One"), S("API-driven integration"))) + metta.space().add_atom( + E(S("capability"), S("ASI:One"), S("API-driven integration")) + ) metta.space().add_atom(E(S("capability"), S("ASI:One"), S("Web3 native"))) metta.space().add_atom(E(S("capability"), S("ASI:One"), S("tool calling"))) metta.space().add_atom(E(S("capability"), S("ASI:One"), S("image generation"))) metta.space().add_atom(E(S("capability"), S("ASI:One"), S("structured data"))) - metta.space().add_atom(E(S("capability"), S("ASI:One"), S("OpenAI compatible"))) - + metta.space().add_atom(E(S("capability"), S("ASI:One"), S("OpenAI compatible"))) + # Comprehensive Implementation → Solutions Mappings - + # Agent Creation Solutions - metta.space().add_atom(E(S("solution"), S("create uAgent"), ValueAtom("pip install uagents, define Agent class, add handlers, run agent"))) - metta.space().add_atom(E(S("solution"), S("hosted agent"), ValueAtom("use Agentverse IDE, write Python code, click Start button"))) - metta.space().add_atom(E(S("solution"), S("local agent"), ValueAtom("define port/endpoint, run locally, manage uptime yourself"))) - metta.space().add_atom(E(S("solution"), S("mailbox agent"), ValueAtom("set mailbox=True, connect via Agentverse, hybrid deployment"))) - + metta.space().add_atom( + E( + S("solution"), + S("create uAgent"), + ValueAtom( + "pip install uagents, define Agent class, add handlers, run agent" + ), + ) + ) + metta.space().add_atom( + E( + S("solution"), + S("hosted agent"), + ValueAtom("use Agentverse IDE, write Python code, click Start button"), + ) + ) + metta.space().add_atom( + E( + S("solution"), + S("local agent"), + ValueAtom("define port/endpoint, run locally, manage uptime yourself"), + ) + ) + metta.space().add_atom( + E( + S("solution"), + S("mailbox agent"), + ValueAtom("set mailbox=True, connect via Agentverse, hybrid deployment"), + ) + ) + # Communication Solutions - metta.space().add_atom(E(S("solution"), S("agent messaging"), ValueAtom("use ctx.send for async, ctx.send_and_receive for sync communication"))) - metta.space().add_atom(E(S("solution"), S("chat protocol"), ValueAtom("import chat protocol components, handle ChatMessage and ChatAcknowledgement"))) - metta.space().add_atom(E(S("solution"), S("REST endpoints"), ValueAtom("use @agent.on_rest_get/@agent.on_rest_post decorators"))) - metta.space().add_atom(E(S("solution"), S("event handling"), ValueAtom("use @agent.on_event('startup'/'shutdown') decorators"))) - + metta.space().add_atom( + E( + S("solution"), + S("agent messaging"), + ValueAtom( + "use ctx.send for async, ctx.send_and_receive for sync communication" + ), + ) + ) + metta.space().add_atom( + E( + S("solution"), + S("chat protocol"), + ValueAtom( + "import chat protocol components, handle ChatMessage and ChatAcknowledgement" + ), + ) + ) + metta.space().add_atom( + E( + S("solution"), + S("REST endpoints"), + ValueAtom("use @agent.on_rest_get/@agent.on_rest_post decorators"), + ) + ) + metta.space().add_atom( + E( + S("solution"), + S("event handling"), + ValueAtom("use @agent.on_event('startup'/'shutdown') decorators"), + ) + ) + # Integration Solutions - metta.space().add_atom(E(S("solution"), S("LangChain integration"), ValueAtom("use LangchainRegisterTool adapter, wrap agent function"))) - metta.space().add_atom(E(S("solution"), S("LangGraph integration"), ValueAtom("use LangchainRegisterTool adapter, wrap orchestration function"))) - metta.space().add_atom(E(S("solution"), S("CrewAI integration"), ValueAtom("use CrewaiRegisterTool adapter, wrap crew handler function"))) - metta.space().add_atom(E(S("solution"), S("A2A integration"), ValueAtom("use A2A inbound/outbound adapters for external protocol support"))) - + metta.space().add_atom( + E( + S("solution"), + S("LangChain integration"), + ValueAtom("use LangchainRegisterTool adapter, wrap agent function"), + ) + ) + metta.space().add_atom( + E( + S("solution"), + S("LangGraph integration"), + ValueAtom("use LangchainRegisterTool adapter, wrap orchestration function"), + ) + ) + metta.space().add_atom( + E( + S("solution"), + S("CrewAI integration"), + ValueAtom("use CrewaiRegisterTool adapter, wrap crew handler function"), + ) + ) + metta.space().add_atom( + E( + S("solution"), + S("A2A integration"), + ValueAtom( + "use A2A inbound/outbound adapters for external protocol support" + ), + ) + ) + # Agentverse Solutions - metta.space().add_atom(E(S("solution"), S("deploy on Agentverse"), ValueAtom("create agent in Agentverse IDE, write code, click Start button"))) - metta.space().add_atom(E(S("solution"), S("search agents"), ValueAtom("use Agentverse Search API with filters for state, category, agent_type, protocol_digest"))) - metta.space().add_atom(E(S("solution"), S("agent discovery"), ValueAtom("register agents with good readme, include tags and domain descriptions"))) - metta.space().add_atom(E(S("solution"), S("mailroom setup"), ValueAtom("enable mailroom service for offline message handling"))) - metta.space().add_atom(E(S("solution"), S("agent registration"), ValueAtom("register with Agentverse API using identity, URL, agent title, and readme"))) - + metta.space().add_atom( + E( + S("solution"), + S("deploy on Agentverse"), + ValueAtom("create agent in Agentverse IDE, write code, click Start button"), + ) + ) + metta.space().add_atom( + E( + S("solution"), + S("search agents"), + ValueAtom( + "use Agentverse Search API with filters for state, category, agent_type, protocol_digest" + ), + ) + ) + metta.space().add_atom( + E( + S("solution"), + S("agent discovery"), + ValueAtom( + "register agents with good readme, include tags and domain descriptions" + ), + ) + ) + metta.space().add_atom( + E( + S("solution"), + S("mailroom setup"), + ValueAtom("enable mailroom service for offline message handling"), + ) + ) + metta.space().add_atom( + E( + S("solution"), + S("agent registration"), + ValueAtom( + "register with Agentverse API using identity, URL, agent title, and readme" + ), + ) + ) + # ASI:One Solutions - metta.space().add_atom(E(S("solution"), S("ASI:One API setup"), ValueAtom("get API key from asi1.ai, use OpenAI-compatible endpoints"))) - metta.space().add_atom(E(S("solution"), S("model selection"), ValueAtom("choose asi1-mini for balance, asi1-fast for speed, asi1-extended for complexity"))) - metta.space().add_atom(E(S("solution"), S("tool calling"), ValueAtom("enable models to use external tools and APIs through function calling"))) - metta.space().add_atom(E(S("solution"), S("structured responses"), ValueAtom("use JSON schema to get structured model responses"))) - metta.space().add_atom(E(S("solution"), S("agentic reasoning"), ValueAtom("use asi1-agentic model for autonomous planning and execution"))) - + metta.space().add_atom( + E( + S("solution"), + S("ASI:One API setup"), + ValueAtom("get API key from asi1.ai, use OpenAI-compatible endpoints"), + ) + ) + metta.space().add_atom( + E( + S("solution"), + S("model selection"), + ValueAtom( + "choose asi1-mini for balance, asi1-fast for speed, asi1-extended for complexity" + ), + ) + ) + metta.space().add_atom( + E( + S("solution"), + S("tool calling"), + ValueAtom( + "enable models to use external tools and APIs through function calling" + ), + ) + ) + metta.space().add_atom( + E( + S("solution"), + S("structured responses"), + ValueAtom("use JSON schema to get structured model responses"), + ) + ) + metta.space().add_atom( + E( + S("solution"), + S("agentic reasoning"), + ValueAtom("use asi1-agentic model for autonomous planning and execution"), + ) + ) + # Comprehensive Implementation → Considerations/Limitations Mappings - + # Hosted Agent Considerations - metta.space().add_atom(E(S("consideration"), S("hosted agents"), ValueAtom("limited library support, always running, managed uptime"))) - metta.space().add_atom(E(S("consideration"), S("local agents"), ValueAtom("full library access, self-managed uptime, scaling responsibility"))) - metta.space().add_atom(E(S("consideration"), S("mailbox agents"), ValueAtom("hybrid approach, Agentverse integration, local control"))) - + metta.space().add_atom( + E( + S("consideration"), + S("hosted agents"), + ValueAtom("limited library support, always running, managed uptime"), + ) + ) + metta.space().add_atom( + E( + S("consideration"), + S("local agents"), + ValueAtom( + "full library access, self-managed uptime, scaling responsibility" + ), + ) + ) + metta.space().add_atom( + E( + S("consideration"), + S("mailbox agents"), + ValueAtom("hybrid approach, Agentverse integration, local control"), + ) + ) + # Communication Considerations - metta.space().add_atom(E(S("consideration"), S("ctx.send"), ValueAtom("fire and forget, no response waiting, suitable for notifications"))) - metta.space().add_atom(E(S("consideration"), S("ctx.send_and_receive"), ValueAtom("synchronous waiting, timeout handling, request-response pattern"))) - metta.space().add_atom(E(S("consideration"), S("chat protocol"), ValueAtom("structured messaging, acknowledgments required, standardized format"))) - + metta.space().add_atom( + E( + S("consideration"), + S("ctx.send"), + ValueAtom( + "fire and forget, no response waiting, suitable for notifications" + ), + ) + ) + metta.space().add_atom( + E( + S("consideration"), + S("ctx.send_and_receive"), + ValueAtom( + "synchronous waiting, timeout handling, request-response pattern" + ), + ) + ) + metta.space().add_atom( + E( + S("consideration"), + S("chat protocol"), + ValueAtom( + "structured messaging, acknowledgments required, standardized format" + ), + ) + ) + # Integration Considerations - metta.space().add_atom(E(S("consideration"), S("LangChain adapter"), ValueAtom("agent wrapping required, function invocation pattern"))) - metta.space().add_atom(E(S("consideration"), S("LangGraph adapter"), ValueAtom("orchestration wrapping, state management considerations"))) - metta.space().add_atom(E(S("consideration"), S("CrewAI adapter"), ValueAtom("team coordination, multi-agent workflow handling"))) - metta.space().add_atom(E(S("consideration"), S("A2A adapters"), ValueAtom("protocol bridging, external client compatibility"))) - + metta.space().add_atom( + E( + S("consideration"), + S("LangChain adapter"), + ValueAtom("agent wrapping required, function invocation pattern"), + ) + ) + metta.space().add_atom( + E( + S("consideration"), + S("LangGraph adapter"), + ValueAtom("orchestration wrapping, state management considerations"), + ) + ) + metta.space().add_atom( + E( + S("consideration"), + S("CrewAI adapter"), + ValueAtom("team coordination, multi-agent workflow handling"), + ) + ) + metta.space().add_atom( + E( + S("consideration"), + S("A2A adapters"), + ValueAtom("protocol bridging, external client compatibility"), + ) + ) + # Agentverse Considerations - metta.space().add_atom(E(S("consideration"), S("Agentverse"), ValueAtom("continuous uptime, easy deployment, but limited to supported libraries"))) - metta.space().add_atom(E(S("consideration"), S("agent search"), ValueAtom("powerful discovery, but requires good readme and proper tagging"))) - metta.space().add_atom(E(S("consideration"), S("mailroom service"), ValueAtom("offline message handling, but adds complexity to message flow"))) - metta.space().add_atom(E(S("consideration"), S("integrated IDE"), ValueAtom("convenient development, but limited compared to full IDE features"))) - + metta.space().add_atom( + E( + S("consideration"), + S("Agentverse"), + ValueAtom( + "continuous uptime, easy deployment, but limited to supported libraries" + ), + ) + ) + metta.space().add_atom( + E( + S("consideration"), + S("agent search"), + ValueAtom( + "powerful discovery, but requires good readme and proper tagging" + ), + ) + ) + metta.space().add_atom( + E( + S("consideration"), + S("mailroom service"), + ValueAtom("offline message handling, but adds complexity to message flow"), + ) + ) + metta.space().add_atom( + E( + S("consideration"), + S("integrated IDE"), + ValueAtom( + "convenient development, but limited compared to full IDE features" + ), + ) + ) + # ASI:One Considerations - metta.space().add_atom(E(S("consideration"), S("ASI:One"), ValueAtom("Web3-native LLM, but API dependency and token costs"))) - metta.space().add_atom(E(S("consideration"), S("asi1-mini"), ValueAtom("balanced performance, but may not handle complex reasoning"))) - metta.space().add_atom(E(S("consideration"), S("asi1-fast"), ValueAtom("quick responses, but limited capabilities"))) - metta.space().add_atom(E(S("consideration"), S("asi1-extended"), ValueAtom("complex tasks, but slower response times"))) - metta.space().add_atom(E(S("consideration"), S("asi1-agentic"), ValueAtom("autonomous reasoning, but requires careful prompt engineering"))) - metta.space().add_atom(E(S("consideration"), S("tool calling"), ValueAtom("external tool integration, but requires proper function definitions"))) - + metta.space().add_atom( + E( + S("consideration"), + S("ASI:One"), + ValueAtom("Web3-native LLM, but API dependency and token costs"), + ) + ) + metta.space().add_atom( + E( + S("consideration"), + S("asi1-mini"), + ValueAtom("balanced performance, but may not handle complex reasoning"), + ) + ) + metta.space().add_atom( + E( + S("consideration"), + S("asi1-fast"), + ValueAtom("quick responses, but limited capabilities"), + ) + ) + metta.space().add_atom( + E( + S("consideration"), + S("asi1-extended"), + ValueAtom("complex tasks, but slower response times"), + ) + ) + metta.space().add_atom( + E( + S("consideration"), + S("asi1-agentic"), + ValueAtom("autonomous reasoning, but requires careful prompt engineering"), + ) + ) + metta.space().add_atom( + E( + S("consideration"), + S("tool calling"), + ValueAtom( + "external tool integration, but requires proper function definitions" + ), + ) + ) + # Blockchain Considerations - metta.space().add_atom(E(S("consideration"), S("decentralization"), ValueAtom("trustless interactions, transparency, but complexity overhead"))) - metta.space().add_atom(E(S("consideration"), S("Almanac registry"), ValueAtom("agent discovery, but registration overhead"))) - metta.space().add_atom(E(S("consideration"), S("smart contracts"), ValueAtom("automated execution, but gas costs and complexity"))) - + metta.space().add_atom( + E( + S("consideration"), + S("decentralization"), + ValueAtom("trustless interactions, transparency, but complexity overhead"), + ) + ) + metta.space().add_atom( + E( + S("consideration"), + S("Almanac registry"), + ValueAtom("agent discovery, but registration overhead"), + ) + ) + metta.space().add_atom( + E( + S("consideration"), + S("smart contracts"), + ValueAtom("automated execution, but gas costs and complexity"), + ) + ) + # Comprehensive FAQ Knowledge Base - + # Greetings and General - metta.space().add_atom(E(S("faq"), S("Hi"), ValueAtom("Hello! I'm your Fetch.ai/uAgents assistant. How can I help you with agent development today?"))) - metta.space().add_atom(E(S("faq"), S("Hello"), ValueAtom("Hi there! I'm here to help with your uAgents and Fetch.ai questions. What can I assist you with?"))) - metta.space().add_atom(E(S("faq"), S("What is Fetch.ai?"), ValueAtom("Fetch.ai is a decentralized AI platform that enables developers to build, connect, and deploy AI agents with blockchain integration."))) - metta.space().add_atom(E(S("faq"), S("What are uAgents?"), ValueAtom("uAgents are lightweight microservices that can represent data, APIs, services, or ML models, designed for seamless agent communication."))) - + metta.space().add_atom( + E( + S("faq"), + S("Hi"), + ValueAtom( + "Hello! I'm your Fetch.ai/uAgents assistant. How can I help you with agent development today?" + ), + ) + ) + metta.space().add_atom( + E( + S("faq"), + S("Hello"), + ValueAtom( + "Hi there! I'm here to help with your uAgents and Fetch.ai questions. What can I assist you with?" + ), + ) + ) + metta.space().add_atom( + E( + S("faq"), + S("What is Fetch.ai?"), + ValueAtom( + "Fetch.ai is a decentralized AI platform that enables developers to build, connect, and deploy AI agents with blockchain integration." + ), + ) + ) + metta.space().add_atom( + E( + S("faq"), + S("What are uAgents?"), + ValueAtom( + "uAgents are lightweight microservices that can represent data, APIs, services, or ML models, designed for seamless agent communication." + ), + ) + ) + # Agent Creation Questions - metta.space().add_atom(E(S("faq"), S("How do I create a uAgent?"), ValueAtom("Install uagents with pip, define an Agent class with name/port/endpoint, add handlers, and run the agent."))) - metta.space().add_atom(E(S("faq"), S("What's the difference between hosted and local agents?"), ValueAtom("Hosted agents run on Agentverse with managed uptime but limited libraries. Local agents run on your machine with full library access but self-managed uptime."))) - metta.space().add_atom(E(S("faq"), S("How do I deploy a mailbox agent?"), ValueAtom("Set mailbox=True in Agent definition, run locally, then connect via Agentverse inspector to create mailbox connection."))) - + metta.space().add_atom( + E( + S("faq"), + S("How do I create a uAgent?"), + ValueAtom( + "Install uagents with pip, define an Agent class with name/port/endpoint, add handlers, and run the agent." + ), + ) + ) + metta.space().add_atom( + E( + S("faq"), + S("What's the difference between hosted and local agents?"), + ValueAtom( + "Hosted agents run on Agentverse with managed uptime but limited libraries. Local agents run on your machine with full library access but self-managed uptime." + ), + ) + ) + metta.space().add_atom( + E( + S("faq"), + S("How do I deploy a mailbox agent?"), + ValueAtom( + "Set mailbox=True in Agent definition, run locally, then connect via Agentverse inspector to create mailbox connection." + ), + ) + ) + # Communication Questions - metta.space().add_atom(E(S("faq"), S("How do agents communicate?"), ValueAtom("Agents use ctx.send for async communication or ctx.send_and_receive for sync communication. Chat protocol provides structured messaging."))) - metta.space().add_atom(E(S("faq"), S("What is the chat protocol?"), ValueAtom("Chat protocol is a standardized communication framework with ChatMessage, ChatAcknowledgement, and various content types for reliable agent messaging."))) - metta.space().add_atom(E(S("faq"), S("How do I add REST endpoints?"), ValueAtom("Use @agent.on_rest_get or @agent.on_rest_post decorators with custom routes, request/response models, and return values."))) - + metta.space().add_atom( + E( + S("faq"), + S("How do agents communicate?"), + ValueAtom( + "Agents use ctx.send for async communication or ctx.send_and_receive for sync communication. Chat protocol provides structured messaging." + ), + ) + ) + metta.space().add_atom( + E( + S("faq"), + S("What is the chat protocol?"), + ValueAtom( + "Chat protocol is a standardized communication framework with ChatMessage, ChatAcknowledgement, and various content types for reliable agent messaging." + ), + ) + ) + metta.space().add_atom( + E( + S("faq"), + S("How do I add REST endpoints?"), + ValueAtom( + "Use @agent.on_rest_get or @agent.on_rest_post decorators with custom routes, request/response models, and return values." + ), + ) + ) + # Integration Questions - metta.space().add_atom(E(S("faq"), S("Can I use LangChain with uAgents?"), ValueAtom("Yes, use the LangchainRegisterTool adapter to wrap LangChain agents and integrate them with the uAgents ecosystem."))) - metta.space().add_atom(E(S("faq"), S("How do I integrate CrewAI?"), ValueAtom("Use the CrewaiRegisterTool adapter to wrap CrewAI crew handlers and expose them as uAgents for multi-agent collaboration."))) - metta.space().add_atom(E(S("faq"), S("What are A2A adapters?"), ValueAtom("A2A adapters bridge uAgents with external Agent-to-Agent protocols - inbound exposes uAgents as A2A endpoints, outbound registers A2A agents as uAgents."))) - + metta.space().add_atom( + E( + S("faq"), + S("Can I use LangChain with uAgents?"), + ValueAtom( + "Yes, use the LangchainRegisterTool adapter to wrap LangChain agents and integrate them with the uAgents ecosystem." + ), + ) + ) + metta.space().add_atom( + E( + S("faq"), + S("How do I integrate CrewAI?"), + ValueAtom( + "Use the CrewaiRegisterTool adapter to wrap CrewAI crew handlers and expose them as uAgents for multi-agent collaboration." + ), + ) + ) + metta.space().add_atom( + E( + S("faq"), + S("What are A2A adapters?"), + ValueAtom( + "A2A adapters bridge uAgents with external Agent-to-Agent protocols - inbound exposes uAgents as A2A endpoints, outbound registers A2A agents as uAgents." + ), + ) + ) + # Development Questions - metta.space().add_atom(E(S("faq"), S("How do I discover other agents?"), ValueAtom("Agents register on the Almanac contract for discovery. Use Agentverse marketplace to find and connect with other agents."))) - metta.space().add_atom(E(S("faq"), S("What is ASI:One?"), ValueAtom("ASI:One is the world's first Web3-native LLM designed for agentic AI, enabling agents to query other agents dynamically."))) - metta.space().add_atom(E(S("faq"), S("How do I handle events?"), ValueAtom("Use @agent.on_event('startup') and @agent.on_event('shutdown') decorators to handle agent lifecycle events."))) - + metta.space().add_atom( + E( + S("faq"), + S("How do I discover other agents?"), + ValueAtom( + "Agents register on the Almanac contract for discovery. Use Agentverse marketplace to find and connect with other agents." + ), + ) + ) + metta.space().add_atom( + E( + S("faq"), + S("What is ASI:One?"), + ValueAtom( + "ASI:One is the world's first Web3-native LLM designed for agentic AI, enabling agents to query other agents dynamically." + ), + ) + ) + metta.space().add_atom( + E( + S("faq"), + S("How do I handle events?"), + ValueAtom( + "Use @agent.on_event('startup') and @agent.on_event('shutdown') decorators to handle agent lifecycle events." + ), + ) + ) + # Blockchain Questions - metta.space().add_atom(E(S("faq"), S("What is the Almanac?"), ValueAtom("Almanac is a blockchain-based registry where agents register for discovery and communication with other agents in the network."))) - metta.space().add_atom(E(S("faq"), S("How does decentralization help?"), ValueAtom("Decentralization provides trustless interactions, transparency, and removes single points of failure in agent communication and transactions."))) - metta.space().add_atom(E(S("faq"), S("Can agents perform transactions?"), ValueAtom("Yes, uAgents can interact with smart contracts, perform blockchain transactions, and maintain transparency through decentralized infrastructure."))) - + metta.space().add_atom( + E( + S("faq"), + S("What is the Almanac?"), + ValueAtom( + "Almanac is a blockchain-based registry where agents register for discovery and communication with other agents in the network." + ), + ) + ) + metta.space().add_atom( + E( + S("faq"), + S("How does decentralization help?"), + ValueAtom( + "Decentralization provides trustless interactions, transparency, and removes single points of failure in agent communication and transactions." + ), + ) + ) + metta.space().add_atom( + E( + S("faq"), + S("Can agents perform transactions?"), + ValueAtom( + "Yes, uAgents can interact with smart contracts, perform blockchain transactions, and maintain transparency through decentralized infrastructure." + ), + ) + ) + # Agentverse Questions - metta.space().add_atom(E(S("faq"), S("What is Agentverse?"), ValueAtom("Agentverse is a cloud-based platform for creating and hosting autonomous agents with continuous uptime, easy deployment, and blockchain integration."))) - metta.space().add_atom(E(S("faq"), S("How do I deploy on Agentverse?"), ValueAtom("Create an agent in the Agentverse IDE, write your Python code, and click the Start button for instant deployment."))) - metta.space().add_atom(E(S("faq"), S("How do I search for agents?"), ValueAtom("Use the Agentverse Search API with filters for state, category, agent_type, and protocol_digest to find specific agents."))) - metta.space().add_atom(E(S("faq"), S("What is mailroom service?"), ValueAtom("Mailroom service allows agents to receive messages even when offline, retrieving them once they come back online."))) - metta.space().add_atom(E(S("faq"), S("How do I make my agent discoverable?"), ValueAtom("Write a good readme with descriptive names, tags, domain descriptions, and input/output models for better discoverability."))) - + metta.space().add_atom( + E( + S("faq"), + S("What is Agentverse?"), + ValueAtom( + "Agentverse is a cloud-based platform for creating and hosting autonomous agents with continuous uptime, easy deployment, and blockchain integration." + ), + ) + ) + metta.space().add_atom( + E( + S("faq"), + S("How do I deploy on Agentverse?"), + ValueAtom( + "Create an agent in the Agentverse IDE, write your Python code, and click the Start button for instant deployment." + ), + ) + ) + metta.space().add_atom( + E( + S("faq"), + S("How do I search for agents?"), + ValueAtom( + "Use the Agentverse Search API with filters for state, category, agent_type, and protocol_digest to find specific agents." + ), + ) + ) + metta.space().add_atom( + E( + S("faq"), + S("What is mailroom service?"), + ValueAtom( + "Mailroom service allows agents to receive messages even when offline, retrieving them once they come back online." + ), + ) + ) + metta.space().add_atom( + E( + S("faq"), + S("How do I make my agent discoverable?"), + ValueAtom( + "Write a good readme with descriptive names, tags, domain descriptions, and input/output models for better discoverability." + ), + ) + ) + # ASI:One Questions - metta.space().add_atom(E(S("faq"), S("What is ASI:One?"), ValueAtom("ASI:One is an intelligent AI platform that excels at finding the right AI agents to help solve tasks involving language, reasoning, analysis, and coding."))) - metta.space().add_atom(E(S("faq"), S("How do I get ASI:One API key?"), ValueAtom("Sign up at asi1.ai, navigate to Developer section, click Create New, and save your API key securely."))) - metta.space().add_atom(E(S("faq"), S("Which ASI:One model should I use?"), ValueAtom("Use asi1-mini for balanced performance, asi1-fast for quick responses, asi1-extended for complex tasks, asi1-agentic for agent interactions."))) - metta.space().add_atom(E(S("faq"), S("What is agentic reasoning?"), ValueAtom("Agentic reasoning allows ASI:One to autonomously plan, execute, and adapt its approach based on evolving inputs and goals."))) - metta.space().add_atom(E(S("faq"), S("How do I use tool calling?"), ValueAtom("Enable models to use external tools and APIs through function calling, defining proper function schemas for tool integration."))) - metta.space().add_atom(E(S("faq"), S("Is ASI:One OpenAI compatible?"), ValueAtom("Yes, ASI:One provides OpenAI-compatible endpoints, making it easy to integrate with existing OpenAI client libraries."))) - + metta.space().add_atom( + E( + S("faq"), + S("What is ASI:One?"), + ValueAtom( + "ASI:One is an intelligent AI platform that excels at finding the right AI agents to help solve tasks involving language, reasoning, analysis, and coding." + ), + ) + ) + metta.space().add_atom( + E( + S("faq"), + S("How do I get ASI:One API key?"), + ValueAtom( + "Sign up at asi1.ai, navigate to Developer section, click Create New, and save your API key securely." + ), + ) + ) + metta.space().add_atom( + E( + S("faq"), + S("Which ASI:One model should I use?"), + ValueAtom( + "Use asi1-mini for balanced performance, asi1-fast for quick responses, asi1-extended for complex tasks, asi1-agentic for agent interactions." + ), + ) + ) + metta.space().add_atom( + E( + S("faq"), + S("What is agentic reasoning?"), + ValueAtom( + "Agentic reasoning allows ASI:One to autonomously plan, execute, and adapt its approach based on evolving inputs and goals." + ), + ) + ) + metta.space().add_atom( + E( + S("faq"), + S("How do I use tool calling?"), + ValueAtom( + "Enable models to use external tools and APIs through function calling, defining proper function schemas for tool integration." + ), + ) + ) + metta.space().add_atom( + E( + S("faq"), + S("Is ASI:One OpenAI compatible?"), + ValueAtom( + "Yes, ASI:One provides OpenAI-compatible endpoints, making it easy to integrate with existing OpenAI client libraries." + ), + ) + ) + # Architecture Questions - metta.space().add_atom(E(S("faq"), S("How do I build end-to-end applications?"), ValueAtom("Use a Prime Agent to orchestrate communication between client applications and specialized agents registered in Agentverse."))) - metta.space().add_atom(E(S("faq"), S("What is agent discovery?"), ValueAtom("Agent discovery allows finding agents by capability through Agentverse search, enabling dynamic agent selection for specific tasks."))) - metta.space().add_atom(E(S("faq"), S("How do agents communicate in production?"), ValueAtom("Agents communicate through Agentverse using standardized protocols, with Prime Agents orchestrating complex multi-agent workflows."))) - + metta.space().add_atom( + E( + S("faq"), + S("How do I build end-to-end applications?"), + ValueAtom( + "Use a Prime Agent to orchestrate communication between client applications and specialized agents registered in Agentverse." + ), + ) + ) + metta.space().add_atom( + E( + S("faq"), + S("What is agent discovery?"), + ValueAtom( + "Agent discovery allows finding agents by capability through Agentverse search, enabling dynamic agent selection for specific tasks." + ), + ) + ) + metta.space().add_atom( + E( + S("faq"), + S("How do agents communicate in production?"), + ValueAtom( + "Agents communicate through Agentverse using standardized protocols, with Prime Agents orchestrating complex multi-agent workflows." + ), + ) + ) + # Troubleshooting Questions - metta.space().add_atom(E(S("faq"), S("My agent won't start"), ValueAtom("Check port availability, verify endpoint configuration, ensure proper imports, and check for syntax errors in your agent code."))) - metta.space().add_atom(E(S("faq"), S("Agents can't communicate"), ValueAtom("Verify agent addresses are correct, check network connectivity, ensure both agents are running, and verify message model compatibility."))) - metta.space().add_atom(E(S("faq"), S("How do I debug agent issues?"), ValueAtom("Use agent inspector URLs, check logs for error messages, verify agent registration on Almanac, and test with simple message exchanges."))) - metta.space().add_atom(E(S("faq"), S("My agent isn't discoverable"), ValueAtom("Ensure your agent has a comprehensive readme with tags, domain descriptions, and clear input/output models for better search visibility."))) - metta.space().add_atom(E(S("faq"), S("ASI:One API not working"), ValueAtom("Verify your API key is correct, check rate limits, ensure proper request format, and validate your authentication headers."))) + metta.space().add_atom( + E( + S("faq"), + S("My agent won't start"), + ValueAtom( + "Check port availability, verify endpoint configuration, ensure proper imports, and check for syntax errors in your agent code." + ), + ) + ) + metta.space().add_atom( + E( + S("faq"), + S("Agents can't communicate"), + ValueAtom( + "Verify agent addresses are correct, check network connectivity, ensure both agents are running, and verify message model compatibility." + ), + ) + ) + metta.space().add_atom( + E( + S("faq"), + S("How do I debug agent issues?"), + ValueAtom( + "Use agent inspector URLs, check logs for error messages, verify agent registration on Almanac, and test with simple message exchanges." + ), + ) + ) + metta.space().add_atom( + E( + S("faq"), + S("My agent isn't discoverable"), + ValueAtom( + "Ensure your agent has a comprehensive readme with tags, domain descriptions, and clear input/output models for better search visibility." + ), + ) + ) + metta.space().add_atom( + E( + S("faq"), + S("ASI:One API not working"), + ValueAtom( + "Verify your API key is correct, check rate limits, ensure proper request format, and validate your authentication headers." + ), + ) + ) diff --git a/web3/singularity-net-metta-fetch.ai/metta/test.py b/web3/singularity-net-metta-fetch.ai/metta/test.py index a0570d69..c91e6504 100644 --- a/web3/singularity-net-metta-fetch.ai/metta/test.py +++ b/web3/singularity-net-metta-fetch.ai/metta/test.py @@ -1,13 +1,11 @@ -from knowledge import * -from generalrag import * +from knowledge import * # noqa: F403 +from generalrag import * # noqa: F403 from hyperon import MeTTa metta = MeTTa() -initialize_knowledge_graph(metta) +initialize_knowledge_graph(metta) # noqa: F405 -rag = GeneralRAG(metta) +rag = GeneralRAG(metta) # noqa: F405 print(rag.get_specific_models("ASI:One")) print(rag.query_all_specific_capabilities("ASI:One")) - - diff --git a/web3/singularity-net-metta/agent.py b/web3/singularity-net-metta/agent.py index 816394b4..548fa2a9 100644 --- a/web3/singularity-net-metta/agent.py +++ b/web3/singularity-net-metta/agent.py @@ -1,9 +1,5 @@ - from datetime import datetime, timezone -import mailbox from uuid import uuid4 -from typing import Any, Dict -import json import os from dotenv import load_dotenv from uagents import Context, Model, Protocol, Agent @@ -27,13 +23,21 @@ load_dotenv() # Initialize agent -agent = Agent(name="Medical MeTTa Agent", seed="medical-metta-agent-seed-1", port=8005, mailbox=True, publish_agent_details=True) +agent = Agent( + name="Medical MeTTa Agent", + seed="medical-metta-agent-seed-1", + port=8005, + mailbox=True, + publish_agent_details=True, +) + class MedicalQuery(Model): query: str intent: str keyword: str + def create_text_chat(text: str, end_session: bool = False) -> ChatMessage: """Create a text chat message.""" content = [TextContent(type="text", text=text)] @@ -45,6 +49,7 @@ def create_text_chat(text: str, end_session: bool = False) -> ChatMessage: content=content, ) + # Initialize global components metta = MeTTa() initialize_knowledge_graph(metta) @@ -54,13 +59,16 @@ def create_text_chat(text: str, end_session: bool = False) -> ChatMessage: # Protocol setup chat_proto = Protocol(spec=chat_protocol_spec) + @chat_proto.on_message(ChatMessage) async def handle_message(ctx: Context, sender: str, msg: ChatMessage): """Handle incoming chat messages and process medical queries.""" ctx.storage.set(str(ctx.session), sender) await ctx.send( sender, - ChatAcknowledgement(timestamp=datetime.now(timezone.utc), acknowledged_msg_id=msg.msg_id), + ChatAcknowledgement( + timestamp=datetime.now(timezone.utc), acknowledged_msg_id=msg.msg_id + ), ) for item in msg.content: @@ -70,36 +78,45 @@ async def handle_message(ctx: Context, sender: str, msg: ChatMessage): elif isinstance(item, TextContent): user_query = item.text.strip() ctx.logger.info(f"Got a medical query from {sender}: {user_query}") - + try: # Process the query using the medical assistant logic response = process_query(user_query, rag, llm) - + # Format the response if isinstance(response, dict): - answer_text = response.get('humanized_answer', 'I apologize, but I could not process your query.') + answer_text = response.get( + "humanized_answer", + "I apologize, but I could not process your query.", + ) else: answer_text = str(response) - + # Send the response back await ctx.send(sender, create_text_chat(answer_text)) - + except Exception as e: ctx.logger.error(f"Error processing medical query: {e}") await ctx.send( - sender, - create_text_chat("I apologize, but I encountered an error processing your medical query. Please try again.") + sender, + create_text_chat( + "I apologize, but I encountered an error processing your medical query. Please try again." + ), ) else: ctx.logger.info(f"Got unexpected content from {sender}") + @chat_proto.on_message(ChatAcknowledgement) async def handle_ack(ctx: Context, sender: str, msg: ChatAcknowledgement): """Handle chat acknowledgements.""" - ctx.logger.info(f"Got an acknowledgement from {sender} for {msg.acknowledged_msg_id}") + ctx.logger.info( + f"Got an acknowledgement from {sender} for {msg.acknowledged_msg_id}" + ) + # Register the protocol agent.include(chat_proto, publish_manifest=True) if __name__ == "__main__": - agent.run() \ No newline at end of file + agent.run() diff --git a/web3/singularity-net-metta/metta/knowledge.py b/web3/singularity-net-metta/metta/knowledge.py index aeae5ac4..a97cef00 100644 --- a/web3/singularity-net-metta/metta/knowledge.py +++ b/web3/singularity-net-metta/metta/knowledge.py @@ -1,6 +1,7 @@ # knowledge.py from hyperon import MeTTa, E, S, ValueAtom + def initialize_knowledge_graph(metta: MeTTa): """Initialize the MeTTa knowledge graph with symptom, treatment, side effect, and FAQ data.""" # Symptoms → Diseases @@ -12,29 +13,87 @@ def initialize_knowledge_graph(metta: MeTTa): metta.space().add_atom(E(S("symptom"), S("dizziness"), S("migraine"))) metta.space().add_atom(E(S("symptom"), S("anxiety"), S("depression"))) metta.space().add_atom(E(S("symptom"), S("stomach upset"), S("flu"))) - metta.space().add_atom(E(S("symptom"), S("insomnia"), S("depression"))) - - # Diseases → Treatments - metta.space().add_atom(E(S("treatment"), S("flu"), ValueAtom("rest, fluids, antiviral drugs"))) - metta.space().add_atom(E(S("treatment"), S("migraine"), ValueAtom("pain relievers, hydration, dark room"))) - metta.space().add_atom(E(S("treatment"), S("depression"), ValueAtom("therapy, antidepressants"))) - metta.space().add_atom(E(S("treatment"), S("anxiety"), ValueAtom("therapy, medications"))) - metta.space().add_atom(E(S("treatment"), S("fatigue"), ValueAtom("rest, hydration, balanced diet"))) - metta.space().add_atom(E(S("treatment"), S("insomnia"), ValueAtom("sleep hygiene, medications"))) - metta.space().add_atom(E(S("treatment"), S("stomach upset"), ValueAtom("dietary changes, medications"))) - metta.space().add_atom(E(S("treatment"), S("dizziness"), ValueAtom("hydration, medications"))) - metta.space().add_atom(E(S("treatment"), S("pain relievers"), ValueAtom("rest, hydration"))) - metta.space().add_atom(E(S("treatment"), S("antiviral drugs"), ValueAtom("rest, hydration"))) - metta.space().add_atom(E(S("treatment"), S("antidepressants"), ValueAtom("therapy, lifestyle changes"))) - metta.space().add_atom(E(S("treatment"), S("antidepressants"), ValueAtom("therapy, medications"))) - + metta.space().add_atom(E(S("symptom"), S("insomnia"), S("depression"))) + + # Diseases → Treatments + metta.space().add_atom( + E(S("treatment"), S("flu"), ValueAtom("rest, fluids, antiviral drugs")) + ) + metta.space().add_atom( + E( + S("treatment"), + S("migraine"), + ValueAtom("pain relievers, hydration, dark room"), + ) + ) + metta.space().add_atom( + E(S("treatment"), S("depression"), ValueAtom("therapy, antidepressants")) + ) + metta.space().add_atom( + E(S("treatment"), S("anxiety"), ValueAtom("therapy, medications")) + ) + metta.space().add_atom( + E(S("treatment"), S("fatigue"), ValueAtom("rest, hydration, balanced diet")) + ) + metta.space().add_atom( + E(S("treatment"), S("insomnia"), ValueAtom("sleep hygiene, medications")) + ) + metta.space().add_atom( + E(S("treatment"), S("stomach upset"), ValueAtom("dietary changes, medications")) + ) + metta.space().add_atom( + E(S("treatment"), S("dizziness"), ValueAtom("hydration, medications")) + ) + metta.space().add_atom( + E(S("treatment"), S("pain relievers"), ValueAtom("rest, hydration")) + ) + metta.space().add_atom( + E(S("treatment"), S("antiviral drugs"), ValueAtom("rest, hydration")) + ) + metta.space().add_atom( + E(S("treatment"), S("antidepressants"), ValueAtom("therapy, lifestyle changes")) + ) + metta.space().add_atom( + E(S("treatment"), S("antidepressants"), ValueAtom("therapy, medications")) + ) + # Treatments → Side Effects - metta.space().add_atom(E(S("side_effect"), S("antiviral drugs"), ValueAtom("nausea, dizziness"))) - metta.space().add_atom(E(S("side_effect"), S("pain relievers"), ValueAtom("stomach upset"))) - metta.space().add_atom(E(S("side_effect"), S("antidepressants"), ValueAtom("weight gain, insomnia"))) - metta.space().add_atom(E(S("side_effect"), S("therapy"), ValueAtom("initial discomfort, emotional release"))) - + metta.space().add_atom( + E(S("side_effect"), S("antiviral drugs"), ValueAtom("nausea, dizziness")) + ) + metta.space().add_atom( + E(S("side_effect"), S("pain relievers"), ValueAtom("stomach upset")) + ) + metta.space().add_atom( + E(S("side_effect"), S("antidepressants"), ValueAtom("weight gain, insomnia")) + ) + metta.space().add_atom( + E( + S("side_effect"), + S("therapy"), + ValueAtom("initial discomfort, emotional release"), + ) + ) + # FAQs - metta.space().add_atom(E(S("faq"), S("Hi"), ValueAtom("Hello! How can I assist you today?"))) - metta.space().add_atom(E(S("faq"), S("What’s wrong with me?"), ValueAtom("I’m not a doctor, but I can help you explore symptoms. What are you feeling?"))) - metta.space().add_atom(E(S("faq"), S("How do I treat a migraine?"), ValueAtom("For migraines, rest in a dark room and stay hydrated. Pain relievers can help."))) \ No newline at end of file + metta.space().add_atom( + E(S("faq"), S("Hi"), ValueAtom("Hello! How can I assist you today?")) + ) + metta.space().add_atom( + E( + S("faq"), + S("What’s wrong with me?"), + ValueAtom( + "I’m not a doctor, but I can help you explore symptoms. What are you feeling?" + ), + ) + ) + metta.space().add_atom( + E( + S("faq"), + S("How do I treat a migraine?"), + ValueAtom( + "For migraines, rest in a dark room and stay hydrated. Pain relievers can help." + ), + ) + ) diff --git a/web3/singularity-net-metta/metta/medicalrag.py b/web3/singularity-net-metta/metta/medicalrag.py index f37a9281..f93c3af0 100644 --- a/web3/singularity-net-metta/metta/medicalrag.py +++ b/web3/singularity-net-metta/metta/medicalrag.py @@ -1,7 +1,7 @@ # medicalrag.py -import re from hyperon import MeTTa, E, S, ValueAtom + class MedicalRAG: def __init__(self, metta_instance: MeTTa): self.metta = metta_instance @@ -9,35 +9,45 @@ def __init__(self, metta_instance: MeTTa): def query_symptom(self, symptom): """Find diseases linked to a symptom.""" symptom = symptom.strip('"') - query_str = f'!(match &self (symptom {symptom} $disease) $disease)' + query_str = f"!(match &self (symptom {symptom} $disease) $disease)" results = self.metta.run(query_str) - print(results,query_str) + print(results, query_str) - unique_diseases = list(set(str(r[0]) for r in results if r and len(r) > 0)) if results else [] + unique_diseases = ( + list(set(str(r[0]) for r in results if r and len(r) > 0)) if results else [] + ) return unique_diseases def get_treatment(self, disease): """Find treatments for a disease.""" disease = disease.strip('"') - query_str = f'!(match &self (treatment {disease} $treatment) $treatment)' + query_str = f"!(match &self (treatment {disease} $treatment) $treatment)" results = self.metta.run(query_str) - print(results,query_str) - return [r[0].get_object().value for r in results if r and len(r) > 0] if results else [] + print(results, query_str) + return ( + [r[0].get_object().value for r in results if r and len(r) > 0] + if results + else [] + ) def get_side_effects(self, treatment): """Find side effects of a treatment.""" treatment = treatment.strip('"') - query_str = f'!(match &self (side_effect {treatment} $effect) $effect)' + query_str = f"!(match &self (side_effect {treatment} $effect) $effect)" results = self.metta.run(query_str) - print(results,query_str) + print(results, query_str) - return [r[0].get_object().value for r in results if r and len(r) > 0] if results else [] + return ( + [r[0].get_object().value for r in results if r and len(r) > 0] + if results + else [] + ) def query_faq(self, question): """Retrieve FAQ answers.""" query_str = f'!(match &self (faq "{question}" $answer) $answer)' results = self.metta.run(query_str) - print(results,query_str) + print(results, query_str) return results[0][0].get_object().value if results and results[0] else None @@ -46,4 +56,4 @@ def add_knowledge(self, relation_type, subject, object_value): if isinstance(object_value, str): object_value = ValueAtom(object_value) self.metta.space().add_atom(E(S(relation_type), S(subject), object_value)) - return f"Added {relation_type}: {subject} → {object_value}" \ No newline at end of file + return f"Added {relation_type}: {subject} → {object_value}"