From 6266b1dc60b9bf122e205358ceeedfe6859c2ea4 Mon Sep 17 00:00:00 2001 From: ssyechuri Date: Tue, 11 Nov 2025 11:28:04 -0600 Subject: [PATCH 1/2] refactored voice_agent agntcy, without agntcy, triage_agent - infermedica - no agntcy components --- .../common/{agntcy => identity}/__init__.py | 0 .../common/identity/tbac.py | 138 +++ .../common/{agntcy => }/observe/README.md | 0 .../common/{agntcy => }/observe/__init__.py | 0 .../{agntcy => }/observe/deploy/.env.template | 0 .../observe/deploy/clickhouse/config.xml | 0 .../observe/deploy/clickhouse/init.sql | 0 .../observe/deploy/clickhouse/users.xml | 0 .../observe/deploy/docker-compose.yml | 0 .../provisioning/dashboards/dashboards.yml | 0 .../dashboards/healthcare-overview.json | 0 .../provisioning/datasources/datasources.yml | 0 .../observe/deploy/nginx/conf.d/default.conf | 0 .../observe/deploy/nginx/nginx.conf | 0 .../observe/deploy/otel/otel-collector.yaml | 0 .../observe/deploy/scripts/backup.sh | 0 .../observe/deploy/scripts/deploy.sh | 0 .../observe/deploy/scripts/update.sh | 0 .../{agntcy => }/observe/observe_config.py | 0 .../requirements.txt | 30 + agentic-healthcare-booking-app/setup.py | 11 + .../infermedica/no_agntcy}/__init__.py | 0 .../infermedica/no_agntcy/client}/__init__.py | 0 .../no_agntcy/client/triage_client.py | 167 ++++ .../infermedica/no_agntcy/config}/__init__.py | 0 .../infermedica/no_agntcy/config/settings.py | 45 + .../infermedica/no_agntcy/main.py | 38 + .../infermedica/no_agntcy/service/__init__.py | 0 .../infermedica/no_agntcy/service/routes.py | 134 +++ .../no_agntcy/service/triage_service.py | 175 ++++ .../infermedica/no_agntcy/utils/__init__.py | 0 .../no_agntcy/utils/task_handlers.py | 210 ++++ .../voice_agent/agntcy/README.md | 306 ------ .../agntcy/agent/healthcare_agent.py | 343 ++++--- .../voice_agent/agntcy/audio/__init__.py | 0 .../audio_service.py => audio/audio.py} | 79 +- .../voice_agent/agntcy/clients/__init__.py | 0 .../{services => clients}/a2a_client.py | 62 +- .../{services => clients}/insurance_client.py | 48 +- .../{services => clients}/llm_client.py | 115 ++- .../voice_agent/agntcy/config/__init__.py | 0 .../voice_agent/agntcy/config/settings.py | 53 + .../voice_agent/agntcy/main.py | 223 ++++- .../voice_agent/agntcy/session/__init__.py | 0 .../agntcy/{models => session}/session.py | 24 +- .../voice_agent/no_agntcy/__init__.py | 0 .../voice_agent/no_agntcy/agent/__init__.py | 0 .../no_agntcy/agent/healthcare_agent.py | 375 +++++++ .../voice_agent/no_agntcy/audio/__init__.py | 0 .../voice_agent/no_agntcy/audio/audio.py | 150 +++ .../voice_agent/no_agntcy/clients/__init__.py | 0 .../no_agntcy/clients/a2a_client.py | 208 ++++ .../no_agntcy/clients/insurance_client.py | 165 ++++ .../no_agntcy/clients/llm_client.py | 147 +++ .../voice_agent/no_agntcy/config/__init__.py | 0 .../voice_agent/no_agntcy/config/settings.py | 121 +++ .../voice_agent/no_agntcy/main.py | 95 ++ .../voice_agent/no_agntcy/session/__init__.py | 151 +++ .../voice_agent/no_agntcy/session/session.py | 92 ++ .../voice_agent/no_agntcy/utils/__init__.py | 0 .../voice_agent/no_agntcy/utils/helpers.py | 151 +++ .../voice_agent/va_a2a_mcp.py | 928 ------------------ 62 files changed, 3204 insertions(+), 1580 deletions(-) rename agentic-healthcare-booking-app/common/{agntcy => identity}/__init__.py (100%) create mode 100644 agentic-healthcare-booking-app/common/identity/tbac.py rename agentic-healthcare-booking-app/common/{agntcy => }/observe/README.md (100%) rename agentic-healthcare-booking-app/common/{agntcy => }/observe/__init__.py (100%) rename agentic-healthcare-booking-app/common/{agntcy => }/observe/deploy/.env.template (100%) rename agentic-healthcare-booking-app/common/{agntcy => }/observe/deploy/clickhouse/config.xml (100%) rename agentic-healthcare-booking-app/common/{agntcy => }/observe/deploy/clickhouse/init.sql (100%) rename agentic-healthcare-booking-app/common/{agntcy => }/observe/deploy/clickhouse/users.xml (100%) rename agentic-healthcare-booking-app/common/{agntcy => }/observe/deploy/docker-compose.yml (100%) rename agentic-healthcare-booking-app/common/{agntcy => }/observe/deploy/grafana/provisioning/dashboards/dashboards.yml (100%) rename agentic-healthcare-booking-app/common/{agntcy => }/observe/deploy/grafana/provisioning/dashboards/healthcare-overview.json (100%) rename agentic-healthcare-booking-app/common/{agntcy => }/observe/deploy/grafana/provisioning/datasources/datasources.yml (100%) rename agentic-healthcare-booking-app/common/{agntcy => }/observe/deploy/nginx/conf.d/default.conf (100%) rename agentic-healthcare-booking-app/common/{agntcy => }/observe/deploy/nginx/nginx.conf (100%) rename agentic-healthcare-booking-app/common/{agntcy => }/observe/deploy/otel/otel-collector.yaml (100%) rename agentic-healthcare-booking-app/common/{agntcy => }/observe/deploy/scripts/backup.sh (100%) rename agentic-healthcare-booking-app/common/{agntcy => }/observe/deploy/scripts/deploy.sh (100%) rename agentic-healthcare-booking-app/common/{agntcy => }/observe/deploy/scripts/update.sh (100%) rename agentic-healthcare-booking-app/common/{agntcy => }/observe/observe_config.py (100%) create mode 100644 agentic-healthcare-booking-app/requirements.txt create mode 100644 agentic-healthcare-booking-app/setup.py rename agentic-healthcare-booking-app/{voice_agent/agntcy => triage_agent/infermedica/no_agntcy}/__init__.py (100%) rename agentic-healthcare-booking-app/{voice_agent/agntcy/models => triage_agent/infermedica/no_agntcy/client}/__init__.py (100%) create mode 100644 agentic-healthcare-booking-app/triage_agent/infermedica/no_agntcy/client/triage_client.py rename agentic-healthcare-booking-app/{voice_agent/agntcy/services => triage_agent/infermedica/no_agntcy/config}/__init__.py (100%) create mode 100644 agentic-healthcare-booking-app/triage_agent/infermedica/no_agntcy/config/settings.py create mode 100644 agentic-healthcare-booking-app/triage_agent/infermedica/no_agntcy/main.py create mode 100644 agentic-healthcare-booking-app/triage_agent/infermedica/no_agntcy/service/__init__.py create mode 100644 agentic-healthcare-booking-app/triage_agent/infermedica/no_agntcy/service/routes.py create mode 100644 agentic-healthcare-booking-app/triage_agent/infermedica/no_agntcy/service/triage_service.py create mode 100644 agentic-healthcare-booking-app/triage_agent/infermedica/no_agntcy/utils/__init__.py create mode 100644 agentic-healthcare-booking-app/triage_agent/infermedica/no_agntcy/utils/task_handlers.py delete mode 100644 agentic-healthcare-booking-app/voice_agent/agntcy/README.md create mode 100644 agentic-healthcare-booking-app/voice_agent/agntcy/audio/__init__.py rename agentic-healthcare-booking-app/voice_agent/agntcy/{services/audio_service.py => audio/audio.py} (61%) create mode 100644 agentic-healthcare-booking-app/voice_agent/agntcy/clients/__init__.py rename agentic-healthcare-booking-app/voice_agent/agntcy/{services => clients}/a2a_client.py (80%) rename agentic-healthcare-booking-app/voice_agent/agntcy/{services => clients}/insurance_client.py (76%) rename agentic-healthcare-booking-app/voice_agent/agntcy/{services => clients}/llm_client.py (76%) create mode 100644 agentic-healthcare-booking-app/voice_agent/agntcy/config/__init__.py create mode 100644 agentic-healthcare-booking-app/voice_agent/agntcy/config/settings.py create mode 100644 agentic-healthcare-booking-app/voice_agent/agntcy/session/__init__.py rename agentic-healthcare-booking-app/voice_agent/agntcy/{models => session}/session.py (76%) create mode 100644 agentic-healthcare-booking-app/voice_agent/no_agntcy/__init__.py create mode 100644 agentic-healthcare-booking-app/voice_agent/no_agntcy/agent/__init__.py create mode 100644 agentic-healthcare-booking-app/voice_agent/no_agntcy/agent/healthcare_agent.py create mode 100644 agentic-healthcare-booking-app/voice_agent/no_agntcy/audio/__init__.py create mode 100644 agentic-healthcare-booking-app/voice_agent/no_agntcy/audio/audio.py create mode 100644 agentic-healthcare-booking-app/voice_agent/no_agntcy/clients/__init__.py create mode 100644 agentic-healthcare-booking-app/voice_agent/no_agntcy/clients/a2a_client.py create mode 100644 agentic-healthcare-booking-app/voice_agent/no_agntcy/clients/insurance_client.py create mode 100644 agentic-healthcare-booking-app/voice_agent/no_agntcy/clients/llm_client.py create mode 100644 agentic-healthcare-booking-app/voice_agent/no_agntcy/config/__init__.py create mode 100644 agentic-healthcare-booking-app/voice_agent/no_agntcy/config/settings.py create mode 100644 agentic-healthcare-booking-app/voice_agent/no_agntcy/main.py create mode 100644 agentic-healthcare-booking-app/voice_agent/no_agntcy/session/__init__.py create mode 100644 agentic-healthcare-booking-app/voice_agent/no_agntcy/session/session.py create mode 100644 agentic-healthcare-booking-app/voice_agent/no_agntcy/utils/__init__.py create mode 100644 agentic-healthcare-booking-app/voice_agent/no_agntcy/utils/helpers.py delete mode 100644 agentic-healthcare-booking-app/voice_agent/va_a2a_mcp.py diff --git a/agentic-healthcare-booking-app/common/agntcy/__init__.py b/agentic-healthcare-booking-app/common/identity/__init__.py similarity index 100% rename from agentic-healthcare-booking-app/common/agntcy/__init__.py rename to agentic-healthcare-booking-app/common/identity/__init__.py diff --git a/agentic-healthcare-booking-app/common/identity/tbac.py b/agentic-healthcare-booking-app/common/identity/tbac.py new file mode 100644 index 0000000..5d56b17 --- /dev/null +++ b/agentic-healthcare-booking-app/common/identity/tbac.py @@ -0,0 +1,138 @@ +""" +TBAC (Task-Based Access Control) Configuration +""" +import os +import logging +from dotenv import load_dotenv + +try: + from identityservice.sdk import IdentityServiceSdk + TBAC_AVAILABLE = True +except ImportError: + TBAC_AVAILABLE = False + logging.warning("identityservice.sdk not available - TBAC disabled") + + +logger = logging.getLogger(__name__) + + +class TBAC: + """TBAC configuration and authorization handler""" + + def __init__(self): + load_dotenv() + + # TBAC credentials + self.client_api_key = os.getenv('CLIENT_AGENT_API_KEY') + self.client_id = os.getenv('CLIENT_AGENT_ID') + self.a2a_api_key = os.getenv('A2A_SERVICE_API_KEY') + self.a2a_id = os.getenv('A2A_SERVICE_ID') + + self.client_sdk = None + self.a2a_sdk = None + self.client_authorized = False + self.a2a_authorized = False + self.client_token = None + self.a2a_token = None + + if TBAC_AVAILABLE: + self._setup() + + def _setup(self): + """Initialize TBAC SDKs""" + if not all([self.client_api_key, self.client_id, self.a2a_api_key, self.a2a_id]): + logger.warning("TBAC Disabled: Missing credentials") + return + + try: + self.client_sdk = IdentityServiceSdk(api_key=self.client_api_key) + self.a2a_sdk = IdentityServiceSdk(api_key=self.a2a_api_key) + logger.info("TBAC SDKs initialized successfully") + except Exception as e: + logger.error(f"TBAC setup failed: {e}") + + def authorize_client_to_a2a(self): + """Authorize client agent to communicate with A2A service""" + if not self.client_sdk or not self.a2a_sdk: + return True # Skip if not configured + + try: + # Get access token for client to access A2A service + self.client_token = self.client_sdk.access_token(self.a2a_id) + + if not self.client_token: + logger.error("TBAC FAILED: Could not get client agent token") + return False + + # Authorize the token with A2A service + self.client_authorized = self.a2a_sdk.authorize(self.client_token) + + if self.client_authorized: + logger.info("TBAC: Client agent authorized to A2A service") + return True + else: + logger.error("TBAC FAILED: Client agent not authorized by A2A service") + return False + + except Exception as e: + logger.error(f"TBAC client-to-a2a authorization failed: {e}") + return False + + def authorize_a2a_to_client(self): + """Authorize A2A service to communicate with client agent""" + if not self.client_sdk or not self.a2a_sdk: + return True # Skip if not configured + + try: + # Get access token for A2A service to access client + self.a2a_token = self.a2a_sdk.access_token(self.client_id) + + if not self.a2a_token: + logger.error("TBAC FAILED: Could not get A2A service token") + return False + + # Authorize the token with client agent + self.a2a_authorized = self.client_sdk.authorize(self.a2a_token) + + if self.a2a_authorized: + logger.info("TBAC: A2A service authorized to client agent") + return True + else: + logger.error("TBAC FAILED: A2A service not authorized by client agent") + return False + + except Exception as e: + logger.error(f"TBAC A2A-to-client authorization failed: {e}") + return False + + def authorize_bidirectional(self): + """Perform bidirectional authorization""" + client_to_a2a = self.authorize_client_to_a2a() + a2a_to_client = self.authorize_a2a_to_client() + return client_to_a2a and a2a_to_client + + def is_client_authorized(self): + """Check if client agent is authorized to communicate with A2A service""" + return self.client_authorized or not all([self.client_api_key, self.a2a_api_key]) + + def is_a2a_authorized(self): + """Check if A2A service is authorized to communicate with client agent""" + return self.a2a_authorized or not all([self.client_api_key, self.a2a_api_key]) + + def is_fully_authorized(self): + """Check if both directions are authorized""" + return self.is_client_authorized() and self.is_a2a_authorized() + + def is_voice_authorized(self): + """Check if voice agent is authorized to call A2A service""" + return self.is_client_authorized() + + def check_voice_authorization(self): + """Block if voice agent not authorized - raises exception""" + if not self.is_voice_authorized(): + raise PermissionError("TBAC: Voice agent not authorized to communicate with A2A service") + + def check_a2a_authorization(self): + """Block if A2A service not authorized - raises exception""" + if not self.is_a2a_authorized(): + raise PermissionError("TBAC: A2A service not authorized to handle messages") \ No newline at end of file diff --git a/agentic-healthcare-booking-app/common/agntcy/observe/README.md b/agentic-healthcare-booking-app/common/observe/README.md similarity index 100% rename from agentic-healthcare-booking-app/common/agntcy/observe/README.md rename to agentic-healthcare-booking-app/common/observe/README.md diff --git a/agentic-healthcare-booking-app/common/agntcy/observe/__init__.py b/agentic-healthcare-booking-app/common/observe/__init__.py similarity index 100% rename from agentic-healthcare-booking-app/common/agntcy/observe/__init__.py rename to agentic-healthcare-booking-app/common/observe/__init__.py diff --git a/agentic-healthcare-booking-app/common/agntcy/observe/deploy/.env.template b/agentic-healthcare-booking-app/common/observe/deploy/.env.template similarity index 100% rename from agentic-healthcare-booking-app/common/agntcy/observe/deploy/.env.template rename to agentic-healthcare-booking-app/common/observe/deploy/.env.template diff --git a/agentic-healthcare-booking-app/common/agntcy/observe/deploy/clickhouse/config.xml b/agentic-healthcare-booking-app/common/observe/deploy/clickhouse/config.xml similarity index 100% rename from agentic-healthcare-booking-app/common/agntcy/observe/deploy/clickhouse/config.xml rename to agentic-healthcare-booking-app/common/observe/deploy/clickhouse/config.xml diff --git a/agentic-healthcare-booking-app/common/agntcy/observe/deploy/clickhouse/init.sql b/agentic-healthcare-booking-app/common/observe/deploy/clickhouse/init.sql similarity index 100% rename from agentic-healthcare-booking-app/common/agntcy/observe/deploy/clickhouse/init.sql rename to agentic-healthcare-booking-app/common/observe/deploy/clickhouse/init.sql diff --git a/agentic-healthcare-booking-app/common/agntcy/observe/deploy/clickhouse/users.xml b/agentic-healthcare-booking-app/common/observe/deploy/clickhouse/users.xml similarity index 100% rename from agentic-healthcare-booking-app/common/agntcy/observe/deploy/clickhouse/users.xml rename to agentic-healthcare-booking-app/common/observe/deploy/clickhouse/users.xml diff --git a/agentic-healthcare-booking-app/common/agntcy/observe/deploy/docker-compose.yml b/agentic-healthcare-booking-app/common/observe/deploy/docker-compose.yml similarity index 100% rename from agentic-healthcare-booking-app/common/agntcy/observe/deploy/docker-compose.yml rename to agentic-healthcare-booking-app/common/observe/deploy/docker-compose.yml diff --git a/agentic-healthcare-booking-app/common/agntcy/observe/deploy/grafana/provisioning/dashboards/dashboards.yml b/agentic-healthcare-booking-app/common/observe/deploy/grafana/provisioning/dashboards/dashboards.yml similarity index 100% rename from agentic-healthcare-booking-app/common/agntcy/observe/deploy/grafana/provisioning/dashboards/dashboards.yml rename to agentic-healthcare-booking-app/common/observe/deploy/grafana/provisioning/dashboards/dashboards.yml diff --git a/agentic-healthcare-booking-app/common/agntcy/observe/deploy/grafana/provisioning/dashboards/healthcare-overview.json b/agentic-healthcare-booking-app/common/observe/deploy/grafana/provisioning/dashboards/healthcare-overview.json similarity index 100% rename from agentic-healthcare-booking-app/common/agntcy/observe/deploy/grafana/provisioning/dashboards/healthcare-overview.json rename to agentic-healthcare-booking-app/common/observe/deploy/grafana/provisioning/dashboards/healthcare-overview.json diff --git a/agentic-healthcare-booking-app/common/agntcy/observe/deploy/grafana/provisioning/datasources/datasources.yml b/agentic-healthcare-booking-app/common/observe/deploy/grafana/provisioning/datasources/datasources.yml similarity index 100% rename from agentic-healthcare-booking-app/common/agntcy/observe/deploy/grafana/provisioning/datasources/datasources.yml rename to agentic-healthcare-booking-app/common/observe/deploy/grafana/provisioning/datasources/datasources.yml diff --git a/agentic-healthcare-booking-app/common/agntcy/observe/deploy/nginx/conf.d/default.conf b/agentic-healthcare-booking-app/common/observe/deploy/nginx/conf.d/default.conf similarity index 100% rename from agentic-healthcare-booking-app/common/agntcy/observe/deploy/nginx/conf.d/default.conf rename to agentic-healthcare-booking-app/common/observe/deploy/nginx/conf.d/default.conf diff --git a/agentic-healthcare-booking-app/common/agntcy/observe/deploy/nginx/nginx.conf b/agentic-healthcare-booking-app/common/observe/deploy/nginx/nginx.conf similarity index 100% rename from agentic-healthcare-booking-app/common/agntcy/observe/deploy/nginx/nginx.conf rename to agentic-healthcare-booking-app/common/observe/deploy/nginx/nginx.conf diff --git a/agentic-healthcare-booking-app/common/agntcy/observe/deploy/otel/otel-collector.yaml b/agentic-healthcare-booking-app/common/observe/deploy/otel/otel-collector.yaml similarity index 100% rename from agentic-healthcare-booking-app/common/agntcy/observe/deploy/otel/otel-collector.yaml rename to agentic-healthcare-booking-app/common/observe/deploy/otel/otel-collector.yaml diff --git a/agentic-healthcare-booking-app/common/agntcy/observe/deploy/scripts/backup.sh b/agentic-healthcare-booking-app/common/observe/deploy/scripts/backup.sh similarity index 100% rename from agentic-healthcare-booking-app/common/agntcy/observe/deploy/scripts/backup.sh rename to agentic-healthcare-booking-app/common/observe/deploy/scripts/backup.sh diff --git a/agentic-healthcare-booking-app/common/agntcy/observe/deploy/scripts/deploy.sh b/agentic-healthcare-booking-app/common/observe/deploy/scripts/deploy.sh similarity index 100% rename from agentic-healthcare-booking-app/common/agntcy/observe/deploy/scripts/deploy.sh rename to agentic-healthcare-booking-app/common/observe/deploy/scripts/deploy.sh diff --git a/agentic-healthcare-booking-app/common/agntcy/observe/deploy/scripts/update.sh b/agentic-healthcare-booking-app/common/observe/deploy/scripts/update.sh similarity index 100% rename from agentic-healthcare-booking-app/common/agntcy/observe/deploy/scripts/update.sh rename to agentic-healthcare-booking-app/common/observe/deploy/scripts/update.sh diff --git a/agentic-healthcare-booking-app/common/agntcy/observe/observe_config.py b/agentic-healthcare-booking-app/common/observe/observe_config.py similarity index 100% rename from agentic-healthcare-booking-app/common/agntcy/observe/observe_config.py rename to agentic-healthcare-booking-app/common/observe/observe_config.py diff --git a/agentic-healthcare-booking-app/requirements.txt b/agentic-healthcare-booking-app/requirements.txt new file mode 100644 index 0000000..2750fae --- /dev/null +++ b/agentic-healthcare-booking-app/requirements.txt @@ -0,0 +1,30 @@ +# Core dependencies +requests>=2.31.0 +python-dotenv>=1.0.0 + +# Audio dependencies (optional but recommended) +SpeechRecognition>=3.10.0 +PyAudio>=0.2.13 +pygame>=2.5.0 +gTTS>=2.4.0 + +# Development dependencies +pytest>=7.4.0 +pytest-asyncio>=0.21.0 +black>=23.0.0 +flake8>=6.0.0 +mypy>=1.5.0 + +# a2a protocol +a2a-sdk + +# agntcy +ioa_observe_sdk +agntcy-identity-service-sdk==0.0.7 + +# a2a service +Flask +Flask-cors +pydantic +pytest-cov +pytest-mock \ No newline at end of file diff --git a/agentic-healthcare-booking-app/setup.py b/agentic-healthcare-booking-app/setup.py new file mode 100644 index 0000000..2846f2b --- /dev/null +++ b/agentic-healthcare-booking-app/setup.py @@ -0,0 +1,11 @@ +from gettext import install +from setuptools import setup, find_packages + +setup( + name="agentic-healthcare-booking-app", + packages=find_packages(), +) + + +# run "pip install -r requirements.txt" for installing dependency libraries +# run "pip install -e ." for clean imports setup \ No newline at end of file diff --git a/agentic-healthcare-booking-app/voice_agent/agntcy/__init__.py b/agentic-healthcare-booking-app/triage_agent/infermedica/no_agntcy/__init__.py similarity index 100% rename from agentic-healthcare-booking-app/voice_agent/agntcy/__init__.py rename to agentic-healthcare-booking-app/triage_agent/infermedica/no_agntcy/__init__.py diff --git a/agentic-healthcare-booking-app/voice_agent/agntcy/models/__init__.py b/agentic-healthcare-booking-app/triage_agent/infermedica/no_agntcy/client/__init__.py similarity index 100% rename from agentic-healthcare-booking-app/voice_agent/agntcy/models/__init__.py rename to agentic-healthcare-booking-app/triage_agent/infermedica/no_agntcy/client/__init__.py diff --git a/agentic-healthcare-booking-app/triage_agent/infermedica/no_agntcy/client/triage_client.py b/agentic-healthcare-booking-app/triage_agent/infermedica/no_agntcy/client/triage_client.py new file mode 100644 index 0000000..daa6795 --- /dev/null +++ b/agentic-healthcare-booking-app/triage_agent/infermedica/no_agntcy/client/triage_client.py @@ -0,0 +1,167 @@ +""" +External triage API client with timing +""" +import base64 +import time +import logging +import requests + +logger = logging.getLogger(__name__) + + +class TriageAPIClient: + """Handles all external triage API communication""" + + def __init__(self, config): + self.triage_app_id = config['triage_app_id'] + self.triage_app_key = config['triage_app_key'] + self.triage_instance_id = config['triage_instance_id'] + self.triage_token_url = config['triage_token_url'] + self.triage_base_url = config['triage_base_url'] + + def timed_external_request(self, method, url, description, **kwargs): + """Make a timed request to external API with detailed logging""" + start_time = time.time() + timestamp = time.strftime("%H:%M:%S", time.localtime(start_time)) + logger.info(f"A2A-SERVICE: [{timestamp}] >>> {method} {description}") + logger.info(f"A2A-SERVICE: URL: {url}") + + try: + if method == 'GET': + response = requests.get(url, **kwargs) + elif method == 'POST': + response = requests.post(url, **kwargs) + else: + raise ValueError(f"Unsupported method: {method}") + + elapsed = time.time() - start_time + end_timestamp = time.strftime("%H:%M:%S", time.localtime()) + elapsed_ms = elapsed * 1000 + + logger.info(f"A2A-SERVICE: [{end_timestamp}] <<< {response.status_code} | {elapsed:.3f}s ({elapsed_ms:.0f}ms)") + + if response.status_code != 200: + logger.error(f"A2A-SERVICE: Error response: {response.text[:300]}") + else: + logger.info(f"A2A-SERVICE: Success - response length: {len(response.text)} chars") + + return response, elapsed + + except Exception as e: + elapsed = time.time() - start_time + end_timestamp = time.strftime("%H:%M:%S", time.localtime()) + elapsed_ms = elapsed * 1000 + logger.error(f"A2A-SERVICE: [{end_timestamp}] <<< ERROR: {e} | {elapsed:.3f}s ({elapsed_ms:.0f}ms)") + raise e + + def get_triage_token(self): + """Get authentication token from external triage API with timing""" + logger.info("Requesting triage API authentication token") + + creds = base64.b64encode(f"{self.triage_app_id}:{self.triage_app_key}".encode()).decode() + headers = { + "Content-Type": "application/json", + "Authorization": f"Basic {creds}", + "instance-id": self.triage_instance_id + } + payload = {"grant_type": "client_credentials"} + + response, elapsed = self.timed_external_request( + 'POST', self.triage_token_url, "Get OAuth Token", + headers=headers, json=payload, timeout=30 + ) + + if response.status_code == 200: + token = response.json()['access_token'] + logger.info(f"Successfully obtained triage API token") + return token + + raise Exception(f"Failed to get token: {response.status_code} - {response.text}") + + def create_triage_survey(self, token, age, sex): + """Create a new triage survey with timing""" + logger.info(f"Creating triage survey - age={age}, sex={sex}") + + headers = { + "Authorization": f"Bearer {token}", + "Content-Type": "application/json" + } + payload = { + "sex": sex.lower(), + "age": {"value": age, "unit": "year"} + } + + response, elapsed = self.timed_external_request( + 'POST', f"{self.triage_base_url}/surveys", "Create Survey", + headers=headers, json=payload, timeout=30 + ) + + if response.status_code == 200: + survey_id = response.json()['survey_id'] + logger.info(f"Successfully created triage survey: {survey_id}") + return survey_id + + raise Exception(f"Failed to create survey: {response.status_code} - {response.text}") + + def send_triage_api_message(self, token, survey_id, message): + """Send message to external triage API with timing""" + logger.info(f"Sending message to triage API: '{message[:50]}...'") + + headers = { + "Authorization": f"Bearer {token}", + "Content-Type": "application/json" + } + payload = {"user_message": message} + + response, elapsed = self.timed_external_request( + 'POST', f"{self.triage_base_url}/surveys/{survey_id}/messages", + "Send Message", + headers=headers, json=payload, timeout=30 + ) + + if response.status_code == 200: + data = response.json() + external_state = data.get('survey_state', 'in_progress') + agent_response = data.get('assistant_message', '') + + logger.info(f"Triage state: {external_state}") + logger.info(f"Triage response length: {len(agent_response)} chars") + + return { + "success": True, + "response": agent_response, + "state": external_state + } + else: + logger.error(f"Triage API error: {response.status_code} - {response.text}") + return { + "success": False, + "response": "I'm having trouble with the medical assessment system." + } + + def get_triage_summary(self, token, survey_id): + """Get triage summary from external API with timing""" + try: + headers = {"Authorization": f"Bearer {token}"} + + response, elapsed = self.timed_external_request( + 'GET', f"{self.triage_base_url}/surveys/{survey_id}/summary", + "Get Triage Summary", + headers=headers, timeout=30 + ) + + if response.status_code == 200: + data = response.json() + logger.info(f"Triage summary retrieved successfully") + return { + 'success': True, + 'urgency_level': data.get('urgency', 'standard'), + 'doctor_type': data.get('doctor_type', 'general practitioner'), + 'notes': data.get('notes', 'Assessment completed') + } + else: + logger.warning(f"Failed to get triage summary: {response.status_code}") + return {'success': False} + except Exception as e: + logger.error(f"Error getting triage summary: {e}", exc_info=True) + return {'success': False} \ No newline at end of file diff --git a/agentic-healthcare-booking-app/voice_agent/agntcy/services/__init__.py b/agentic-healthcare-booking-app/triage_agent/infermedica/no_agntcy/config/__init__.py similarity index 100% rename from agentic-healthcare-booking-app/voice_agent/agntcy/services/__init__.py rename to agentic-healthcare-booking-app/triage_agent/infermedica/no_agntcy/config/__init__.py diff --git a/agentic-healthcare-booking-app/triage_agent/infermedica/no_agntcy/config/settings.py b/agentic-healthcare-booking-app/triage_agent/infermedica/no_agntcy/config/settings.py new file mode 100644 index 0000000..941d79d --- /dev/null +++ b/agentic-healthcare-booking-app/triage_agent/infermedica/no_agntcy/config/settings.py @@ -0,0 +1,45 @@ +""" +Configuration loading for A2A Triage Service +""" +import os +import logging + +logger = logging.getLogger(__name__) + + +def load_env(): + """Load environment variables from .env file""" + try: + from dotenv import load_dotenv + load_dotenv() + logger.info("Environment variables loaded from .env file") + except ImportError: + logger.info("python-dotenv not available, using system environment variables") + except Exception as e: + logger.warning(f"Failed to load .env file: {e}") + + +def get_triage_config(): + """Load and validate triage API configuration""" + required_vars = [ + 'TRIAGE_APP_ID', + 'TRIAGE_APP_KEY', + 'TRIAGE_INSTANCE_ID', + 'TRIAGE_TOKEN_URL', + 'TRIAGE_BASE_URL' + ] + + config = {} + missing_vars = [] + + for var in required_vars: + value = os.getenv(var) + if not value: + missing_vars.append(var) + config[var.lower()] = value + + if missing_vars: + raise ValueError(f"Missing required environment variables: {missing_vars}") + + logger.info("Triage API configuration loaded successfully") + return config \ No newline at end of file diff --git a/agentic-healthcare-booking-app/triage_agent/infermedica/no_agntcy/main.py b/agentic-healthcare-booking-app/triage_agent/infermedica/no_agntcy/main.py new file mode 100644 index 0000000..309bac8 --- /dev/null +++ b/agentic-healthcare-booking-app/triage_agent/infermedica/no_agntcy/main.py @@ -0,0 +1,38 @@ +""" +Main entry point for A2A Medical Triage Service +""" +import argparse +import logging +from service.triage_service import A2ATriageService + +# Setup logging +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger(__name__) + + +def main(): + """Main entry point""" + parser = argparse.ArgumentParser(description='A2A Medical Triage Service') + parser.add_argument('--host', default='0.0.0.0', help='Host to bind to (default: 0.0.0.0)') + parser.add_argument('--port', type=int, default=8887, help='Port to bind to (default: 8887)') + parser.add_argument('--debug', action='store_true', help='Enable debug mode') + + args = parser.parse_args() + + try: + service = A2ATriageService( + host=args.host, + port=args.port, + debug=args.debug + ) + service.run() + except Exception as e: + logger.error(f"Failed to start service: {e}", exc_info=True) + exit(1) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/agentic-healthcare-booking-app/triage_agent/infermedica/no_agntcy/service/__init__.py b/agentic-healthcare-booking-app/triage_agent/infermedica/no_agntcy/service/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/agentic-healthcare-booking-app/triage_agent/infermedica/no_agntcy/service/routes.py b/agentic-healthcare-booking-app/triage_agent/infermedica/no_agntcy/service/routes.py new file mode 100644 index 0000000..0b91327 --- /dev/null +++ b/agentic-healthcare-booking-app/triage_agent/infermedica/no_agntcy/service/routes.py @@ -0,0 +1,134 @@ +""" +Flask route definitions +""" +import logging +from datetime import datetime +from flask import request, jsonify +from a2a.types import JSONRPCErrorResponse, JSONParseError, InvalidRequestError, MethodNotFoundError, InvalidParamsError, InternalError, TaskNotFoundError, TaskNotCancelableError + +logger = logging.getLogger(__name__) + + +def setup_routes(app, service): + """Setup all Flask routes""" + + @app.route('/.well-known/agent-card.json', methods=['GET']) + def agent_card(): + """A2A Agent Discovery Card""" + return jsonify({ + "name": "Medical Triage Agent A2A service", + "description": "A2A service for an AI agent that performs medical symptom triage and assessment using professional medical protocols", + "url": f"http://{request.host}", + "provider": { + "organization": "Outshift", + "url": f"http://{request.host}" + }, + "iconUrl": f"http://{request.host}/icon.png", + "version": "1.0.0", + "documentationUrl": f"http://{request.host}/docs", + "capabilities": { + "streaming": False, + "pushNotifications": False, + "stateTransitionHistory": False, + "extensions": [] + }, + "securitySchemes": { + "noAuth": { + "type": "http", + "scheme": "none" + } + }, + "security": [], + "defaultInputModes": ["text/plain", "application/json"], + "defaultOutputModes": ["text/plain", "application/json"], + "skills": [ + { + "id": "medical-triage", + "name": "Medical Symptom Triage A2A Service", + "description": "Performs comprehensive medical symptom assessment and triage using AI-powered clinical protocols", + "tags": ["healthcare", "triage", "medical", "symptoms", "diagnosis"], + "examples": [ + "I have chest pain and shortness of breath", + "My child has a fever and headache", + "I'm experiencing severe abdominal pain" + ], + "inputModes": ["text/plain", "application/json"], + "outputModes": ["text/plain", "application/json"] + } + ], + "supportsAuthenticatedExtendedCard": False + }) + + @app.route('/health', methods=['GET']) + def health_check(): + """Health check endpoint for load balancers""" + return jsonify({ + "status": "healthy", + "timestamp": datetime.now().isoformat(), + "version": "1.0.0", + "active_tasks": len(service.tasks) + }) + + @app.route('/docs', methods=['GET']) + def documentation(): + """Basic documentation endpoint""" + return jsonify({ + "title": "Medical Triage A2A Service", + "description": "Agent-to-Agent protocol service for medical symptom triage", + "endpoints": { + "/.well-known/agent.json": "Agent discovery card", + "/health": "Health check", + "/docs": "This documentation", + "/": "JSON-RPC 2.0 endpoint for A2A communication" + }, + "supported_methods": [ + "message/send", + "tasks/get", + "tasks/cancel" + ] + }) + + @app.route('/', methods=['POST']) + def handle_jsonrpc(): + """Main JSON-RPC 2.0 endpoint for A2A protocol""" + try: + data = request.get_json() + + if not service.validate_jsonrpc_request(data): + logger.warning(f"Invalid JSON-RPC request: {data}") + return jsonify(service.create_error_response( + data.get('id'), InvalidRequestError, InvalidRequestError.message + )) + + method = data['method'] + params = data.get('params', {}) + request_id = data['id'] + + logger.info(f"Handling {method} request with ID {request_id}") + + if method == 'message/send': + return jsonify(service.handle_message_send(params, request_id)) + elif method == 'tasks/get': + return jsonify(service.handle_tasks_get(params, request_id)) + elif method == 'tasks/cancel': + return jsonify(service.handle_tasks_cancel(params, request_id)) + else: + logger.warning(f"Unknown method: {method}") + return jsonify(service.create_error_response( + request_id, MethodNotFoundError, MethodNotFoundError.message + )) + + except Exception as e: + logger.error(f"Error handling JSON-RPC request: {e}", exc_info=True) + return jsonify(service.create_error_response( + None, InternalError, InternalError.message + )) + + @app.errorhandler(404) + def not_found(error): + return jsonify({"error": "Not found"}), 404 + + @app.errorhandler(500) + def internal_error(error): + logger.error(f"Internal server error: {error}") + return jsonify({"error": "Internal server error"}), 500 \ No newline at end of file diff --git a/agentic-healthcare-booking-app/triage_agent/infermedica/no_agntcy/service/triage_service.py b/agentic-healthcare-booking-app/triage_agent/infermedica/no_agntcy/service/triage_service.py new file mode 100644 index 0000000..64e78c7 --- /dev/null +++ b/agentic-healthcare-booking-app/triage_agent/infermedica/no_agntcy/service/triage_service.py @@ -0,0 +1,175 @@ +""" +Main A2A Triage Service class +""" +import logging +from datetime import datetime +from flask import Flask +from flask_cors import CORS +from config.settings import load_env, get_triage_config + +from a2a.types import TaskState, JSONRPCErrorResponse, JSONParseError, InvalidRequestError, MethodNotFoundError, InvalidParamsError, InternalError, TaskNotFoundError, TaskNotCancelableError +from client.triage_client import TriageAPIClient +from utils.task_handlers import create_new_task, continue_existing_task +from service.routes import setup_routes + +logger = logging.getLogger(__name__) + + +class A2ATriageService: + """ + Standalone A2A Medical Triage Service with Timing Logs + + Provides medical symptom triage through the Agent-to-Agent protocol, + integrating with external medical triage APIs. + """ + + def __init__(self, host='0.0.0.0', port=8887, debug=False): + self.app = Flask(__name__) + CORS(self.app) # Enable CORS for cross-origin requests + + self.host = host + self.port = port + self.debug = debug + + # In-memory storage for tasks and contexts + self.tasks = {} + self.contexts = {} + + # Load triage API configuration + load_env() + triage_config = get_triage_config() + self.triage_client = TriageAPIClient(triage_config) + + # Setup Flask routes + setup_routes(self.app, self) + + logger.info(f"A2A Triage Service initialized - will run on {host}:{port}") + + def validate_jsonrpc_request(self, data): + """Validate JSON-RPC 2.0 request format""" + if not isinstance(data, dict): + return False + if data.get('jsonrpc') != '2.0': + return False + if 'method' not in data: + return False + if 'id' not in data: + return False + return True + + def create_error_response(self, request_id, code, message, data=None): + """Create JSON-RPC 2.0 error response""" + response = { + "jsonrpc": "2.0", + "id": request_id, + "error": { + "code": code, + "message": message + } + } + if data: + response["error"]["data"] = data + return response + + def create_success_response(self, request_id, result): + """Create JSON-RPC 2.0 success response""" + return { + "jsonrpc": "2.0", + "id": request_id, + "result": result + } + + def handle_message_send(self, params, request_id): + """Handle message/send JSON-RPC method""" + try: + message = params.get('message') + if not message: + return self.create_error_response( + request_id, InvalidParamsError, InvalidParamsError.message + ) + + parts = message.get('parts', []) + task_id = message.get('taskId') + context_id = message.get('contextId') + message_id = message.get('messageId') + + # Extract text from message parts + user_text = "" + for part in parts: + if part.get('kind') == 'text': + user_text = part.get('text', '') + break + + logger.info(f"Processing message: '{user_text[:100]}...'") + + if task_id and task_id in self.tasks: + task = continue_existing_task( + self.triage_client, self.tasks, task_id, user_text, request_id, message + ) + if task is None: + return self.create_error_response(request_id, TaskNotCancelableError, TaskNotCancelableError.message) + return self.create_success_response(request_id, task) + else: + task = create_new_task( + self.triage_client, user_text, context_id, request_id, message + ) + self.tasks[task['id']] = task + return self.create_success_response(request_id, task) + + except Exception as e: + logger.error(f"Error in message/send: {e}", exc_info=True) + return self.create_error_response(request_id, InternalError, InternalError.message) + + def handle_tasks_get(self, params, request_id): + """Handle tasks/get JSON-RPC method""" + task_id = params.get('id') + if not task_id or task_id not in self.tasks: + logger.warning(f"Task not found: {task_id}") + return self.create_error_response(request_id, TaskNotFoundError, TaskNotFoundError.message ) + + task = self.tasks[task_id] + history_length = params.get('historyLength', 10) + + # Limit history if requested + if history_length and len(task.get('history', [])) > history_length: + task_copy = task.copy() + task_copy['history'] = task['history'][-history_length:] + return self.create_success_response(request_id, task_copy) + + logger.info(f"Retrieved task {task_id}") + return self.create_success_response(request_id, task) + + def handle_tasks_cancel(self, params, request_id): + """Handle tasks/cancel JSON-RPC method""" + task_id = params.get('id') + if not task_id or task_id not in self.tasks: + logger.warning(f"Task not found for cancellation: {task_id}") + return self.create_error_response(request_id, TaskNotFoundError, TaskNotFoundError.message) + + task = self.tasks[task_id] + + # Check if task can be cancelled + if task['status']['state'] in [TaskState.completed, TaskState.failed, TaskState.canceled]: + logger.warning(f"Task {task_id} cannot be cancelled - in terminal state") + return self.create_error_response(request_id, TaskNotCancelableError, TaskNotCancelableError.message) + + # Cancel the task + task['status']['state'] = TaskState.canceled + task['status']['timestamp'] = datetime.now().isoformat() + + logger.info(f"Task {task_id} cancelled") + return self.create_success_response(request_id, task) + + def run(self): + """Run the Flask application""" + logger.info(f"Starting A2A Triage Service on {self.host}:{self.port}") + logger.info(f"Agent card available at: http://{self.host}:{self.port}/.well-known/agent-card.json") + logger.info(f"Health check available at: http://{self.host}:{self.port}/health") + + # Run Flask app + self.app.run( + host=self.host, + port=self.port, + debug=self.debug, + use_reloader=False + ) \ No newline at end of file diff --git a/agentic-healthcare-booking-app/triage_agent/infermedica/no_agntcy/utils/__init__.py b/agentic-healthcare-booking-app/triage_agent/infermedica/no_agntcy/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/agentic-healthcare-booking-app/triage_agent/infermedica/no_agntcy/utils/task_handlers.py b/agentic-healthcare-booking-app/triage_agent/infermedica/no_agntcy/utils/task_handlers.py new file mode 100644 index 0000000..f52fc56 --- /dev/null +++ b/agentic-healthcare-booking-app/triage_agent/infermedica/no_agntcy/utils/task_handlers.py @@ -0,0 +1,210 @@ +""" +Task creation and continuation handlers +""" +import re +import uuid +import logging +from datetime import datetime +from a2a.types import TaskState + +logger = logging.getLogger(__name__) + + +def extract_demographics(text): + """Extract age and sex from user input text""" + demographics = {} + + # Age extraction patterns + age_patterns = [ + r'\b(\d{1,2})\s*(?:years?\s*old|yo)\b', + r'\bage\s*(?:is\s*)?(\d{1,2})\b', + r'\bi\s*am\s*(\d{1,2})\b' + ] + + for pattern in age_patterns: + match = re.search(pattern, text.lower()) + if match: + age = int(match.group(1)) + if 1 <= age <= 120: + demographics['age'] = age + break + + # Sex extraction + text_lower = text.lower() + if any(word in text_lower for word in ['male', 'man', 'boy', 'he', 'his', 'him']): + demographics['sex'] = 'male' + elif any(word in text_lower for word in ['female', 'woman', 'girl', 'she', 'her']): + demographics['sex'] = 'female' + + logger.info(f"Extracted demographics: {demographics}") + return demographics + + +def start_triage_session(triage_client, age, sex, complaint, task): + """Start a new triage session with external API""" + try: + token = triage_client.get_triage_token() + survey_id = triage_client.create_triage_survey(token, age, sex) + + initial_response = triage_client.send_triage_api_message(token, survey_id, complaint) + + return { + 'success': True, + 'response': initial_response.get('response', 'Medical triage session started. Please describe your symptoms.'), + 'metadata': { + 'triage_token': token, + 'survey_id': survey_id + } + } + except Exception as e: + logger.error(f"Error starting triage session: {e}", exc_info=True) + return {'success': False, 'error': str(e)} + + +def create_new_task(triage_client, user_text, context_id, request_id, original_message): + """Create a new triage task""" + task_id = str(uuid.uuid4()) + if not context_id: + context_id = str(uuid.uuid4()) + + logger.info(f"Creating new triage task {task_id}") + + # Create task structure + task = { + "id": task_id, + "contextId": context_id, + "status": { + "state": TaskState.submitted, + "timestamp": datetime.now().isoformat() + }, + "history": [original_message], + "artifacts": [], + "metadata": { + "triage_token": None, + "survey_id": None, + "triage_state": "starting" + }, + "kind": "task" + } + + # Extract demographics from user input + demographics = extract_demographics(user_text) + age = demographics.get('age', 64) + sex = demographics.get('sex', 'female') + + logger.info(f"Starting triage session with age={age}, sex={sex}") + + # Start external triage session + result = start_triage_session(triage_client, age, sex, user_text, task) + + if result['success']: + task['metadata'].update(result['metadata']) + task['status']['state'] = TaskState.input_required + + # Create agent response message + agent_message = { + "role": "agent", + "parts": [{"kind": "text", "text": result['response']}], + "messageId": str(uuid.uuid4()), + "taskId": task_id, + "contextId": context_id, + "kind": "message" + } + task['history'].append(agent_message) + task['status']['message'] = agent_message + task['metadata']['triage_state'] = 'in_progress' + + logger.info(f"Triage task {task_id} started successfully") + else: + task['status']['state'] = TaskState.failed + logger.error(f"Failed to start triage for task {task_id}: {result.get('error')}") + + return task + + +def continue_existing_task(triage_client, tasks, task_id, user_text, request_id, message): + """Continue an existing triage task""" + task = tasks[task_id] + + logger.info(f"Continuing task {task_id}, current state: {task['status']['state']}") + + # Check if task is in a terminal state + if task['status']['state'] in [TaskState.completed, TaskState.failed, TaskState.canceled]: + logger.warning(f"Task {task_id} is in terminal state: {task['status']['state']}") + return None + + task['history'].append(message) + + # Send message to external triage API + result = triage_client.send_triage_api_message( + task['metadata']['triage_token'], + task['metadata']['survey_id'], + user_text + ) + + if result['success']: + # Create agent response + agent_message = { + "role": "agent", + "parts": [{"kind": "text", "text": result['response']}], + "messageId": str(uuid.uuid4()), + "taskId": task_id, + "contextId": task['contextId'], + "kind": "message" + } + task['history'].append(agent_message) + task['status']['message'] = agent_message + + # Map external triage state to A2A task state + external_state = result.get('state', 'in_progress') + task['metadata']['triage_state'] = external_state + + logger.info(f"External triage state: {external_state}") + + if external_state == 'present_result': + logger.info("Triage completed - transitioning to COMPLETED state") + task['status']['state'] = TaskState.completed + + # Get triage summary and create artifact + summary_result = triage_client.get_triage_summary( + task['metadata']['triage_token'], + task['metadata']['survey_id'] + ) + artifact_data = { + "urgency_level": summary_result.get('urgency_level', 'standard'), + "doctor_type": summary_result.get('doctor_type', 'general practitioner'), + "notes": summary_result.get('notes', 'Triage assessment completed'), + "completed_at": datetime.now().isoformat() + } + + artifact = { + "artifactId": str(uuid.uuid4()), + "name": "Medical Triage Assessment", + "description": "Results from medical triage evaluation", + "parts": [ + { + "kind": "data", + "data": artifact_data + } + ] + } + task['artifacts'] = [artifact] + + logger.info(f"Task {task_id} completed with triage results") + + elif external_state == 'in_progress': + task['status']['state'] = TaskState.input_required + logger.info(f"Task {task_id} waiting for more user input") + + elif external_state == 'post_result': + logger.warning("Received post_result state - task should already be completed") + task['status']['state'] = TaskState.completed + + else: + task['status']['state'] = TaskState.input_required + + else: + task['status']['state'] = TaskState.failed + logger.error(f"Failed to process triage message for task {task_id}: {result.get('error')}") + + return task \ No newline at end of file diff --git a/agentic-healthcare-booking-app/voice_agent/agntcy/README.md b/agentic-healthcare-booking-app/voice_agent/agntcy/README.md deleted file mode 100644 index 52eec94..0000000 --- a/agentic-healthcare-booking-app/voice_agent/agntcy/README.md +++ /dev/null @@ -1,306 +0,0 @@ -# Healthcare Voice + A2A + MCP Agent - Observability README - -This document outlines the observability implementation for the Healthcare Voice + A2A + MCP Agent, focusing on how the `ioa_observe.sdk` is used to instrument and monitor the agent's operations, including interactions with A2A and MCP services. - -## Table of Contents -1. [Introduction](#1-introduction) -2. [Project Structure](#2-project-structure) -3. [Module Descriptions](#3-module-descriptions) -4. [Observability Setup](#4-observability-setup) -5. [Agent, Workflow, Task, and Tool Instrumentation](#5-agent-workflow-task-and-tool-instrumentation) - * [`@agent` Decorator](#agent-decorator) - * [`@workflow` Decorator](#workflow-decorator) - * [`@task` Decorator](#task-decorator) - * [`@tool` Decorator](#tool-decorator) -6. [A2A Protocol Observability](#6-a2a-protocol-observability) -7. [Metrics](#7-metrics) -8. [Session Tracing](#8-session-tracing) -9. [Configuration](#9-configuration) - ---- - -## 1. Introduction - -The Healthcare Voice + A2A + MCP Agent is designed to automate healthcare appointment scheduling, integrating with external services for medical assessment (via A2A) and insurance verification (via MCP). To ensure robust monitoring, debugging, and performance analysis, the agent leverages the `ioa_observe.sdk` for comprehensive observability. This includes tracing, metrics, and structured logging across its various components and external interactions. - ---- - -## 2. Project Structure - -The agent follows a modular architecture with clear separation of concerns: - -``` -voice-agent/agntcy/ -├── main.py # Entry point and initialization -├── models/ -│ ├── __init__.py -│ └── session.py # Session management and persistence -├── services/ -│ ├── __init__.py -│ ├── audio_service.py # Audio I/O (TTS/STT) -│ ├── a2a_client.py # A2A protocol client -│ ├── llm_client.py # LLM processing client -│ └── insurance_client.py # Insurance MCP client -├── agent/ -│ ├── __init__.py -│ └── healthcare_agent.py # Main agent orchestrator - -``` - ---- - -## 3. Module Descriptions - -### Configuration Module (`config/`) -* **`settings.py`**: Contains the `load_env()` function that loads environment variables from a `.env` file using `python-dotenv`. This module has no business logic and is solely responsible for environment setup. - -### Models Module (`models/`) -* **`task_state.py`**: Defines the `TaskState` enum that represents all possible states of an A2A task according to the A2A specification (submitted, working, input-required, completed, canceled, failed, rejected, auth-required, unknown). -* **`session.py`**: Implements the `Session` class that manages conversation state, logs all interactions, and persists complete session data to JSON files in the `sessions/` directory. Each session has a unique ID, tracks triage state, and maintains a complete conversation log with timestamps. - -### Services Module (`services/`) -* **`audio_service.py`**: Implements the `AudioSystem` class that handles speech recognition (using Google Speech API) and text-to-speech (using gTTS and pygame). Automatically falls back to console I/O if audio libraries are unavailable. Methods `listen()` and `speak()` are decorated with `@tool` for observability. -* **`a2a_client.py`**: Implements the `A2AClient` class for A2A protocol communication. Handles agent discovery, message sending, and task tracking. Uses `A2AInstrumentor` for automatic HTTP instrumentation and includes custom `_timed_request()` method for detailed logging. -* **`llm_client.py`**: Implements the `LLMClient` class for natural language processing. Sends prompts to an LLM endpoint and parses JSON responses for intent extraction. The `process()` method is decorated with `@tool`. -* **`insurance_client.py`**: Implements the `InsuranceClient` class for MCP-based insurance operations. Provides `discovery()` for finding insurance information and `eligibility()` for benefits verification. Both methods are decorated with `@tool` and include automatic date/name formatting. - -### Agent Module (`agent/`) -* **`healthcare_agent.py`**: Implements the `HealthcareAgent` class decorated with `@agent`. This is the main orchestrator that coordinates all services, manages the conversation loop, and implements triage workflows decorated with `@workflow`. Handles turn-based interaction, error recovery, and session persistence. - -### Entry Point (`main.py`) -* **`main.py`**: Application entry point that initializes observability, validates configuration, creates the `HealthcareAgent` instance, and starts the conversation loop. Handles graceful shutdown on keyboard interrupt. - ---- - -## 4. Observability Setup - -The core observability initialization happens in the `run_agent` function in `main.py` and within the `A2AClient` constructor in `services/a2a_client.py`: - -* **Global Initialization** (`main.py`): - ```python - from agntcy.observe.observe_config import initialize_observability - # ... - def run_agent(): - # ... - service_name = "Healthcare_Voice_Agent" - initialize_observability(service_name) - ``` - This call sets up the foundational observability configuration for the entire agent, typically configuring OpenTelemetry exporters and resource attributes. - For observe_config refer https://github.com/techm-2025/customer-support/tree/main/agentic-healthcare-booking-app/common/agntcy/observe/observe_config.py - -* **A2A Client Specific Initialization** (`services/a2a_client.py`): - ```python - from ioa_observe.sdk import Observe - from ioa_observe.sdk.instrumentations.a2a import A2AInstrumentor - # ... - class A2AClient: - def __init__(self): - # ... - api_endpoint = os.getenv('OTLP_ENDPOINT', 'http://localhost:4318') - Observe.init("A2A_Client", api_endpoint=api_endpoint) - A2AInstrumentor().instrument() - ``` - The `A2AClient` explicitly initializes `Observe` for its own operations, specifying a service name "A2A_Client" and an OTLP endpoint. Crucially, `A2AInstrumentor().instrument()` is called here to automatically instrument HTTP requests made by the A2A client, ensuring that A2A protocol interactions are captured as spans in traces. - ---- - -## 5. Agent, Workflow, Task, and Tool Instrumentation - -The `ioa_observe.sdk` provides decorators to automatically instrument key components of the agent's logic, turning them into observable units (spans in traces). - -### `@agent` Decorator -The main agent class `HealthcareAgent` in `agent/healthcare_agent.py` is decorated with `@agent`. This marks `HealthcareAgent` as a top-level observable entity, making its lifecycle and high-level operations traceable. - -```python -@agent(name="healthcare_agent", description="healthcare voice agent", version="1.0.0", protocol="A2A") -class HealthcareAgent: - # ... -``` -This decorator captures the instantiation and execution of the agent, providing metadata like its name, description, version, and the protocol it primarily uses. - -### `@workflow` Decorator -Workflows represent a sequence of related operations or a significant business process within the agent. The `_start_integrated_triage` and `_handle_triage_conversation` methods in `agent/healthcare_agent.py` are decorated as workflows: - -```python -@workflow(name="integrated_triage_workflow") -async def _start_integrated_triage(self): - # ... - -@workflow(name="triage_conversational_flow") -async def _handle_triage_conversation(self, user_input): - # ... -``` -These decorators ensure that the entire execution flow of starting a triage or handling a triage conversation is captured as a distinct workflow span, allowing for end-to-end tracing of these complex interactions. - -### `@task` Decorator -The `@task` decorator is conceptually similar to `@workflow` but typically for smaller, more granular units of work within a workflow. The A2A protocol itself defines "tasks," and the `A2AInstrumentor` will likely create spans for these A2A tasks. - -### `@tool` Decorator -Tools represent external capabilities or specific functions the agent can invoke. Several methods across different service modules are decorated as `@tool`: - -* **Audio System Tools** (`services/audio_service.py`): - ```python - @tool(name="listening_tool") - async def listen(self, timeout=5): - # ... - @tool(name="speaking_tool") - async def speak(self, text): - # ... - ``` - These capture the agent's interactions with the audio input/output, showing when the agent is listening or speaking. - -* **A2A Message Tool** (`services/a2a_client.py`): - ```python - @tool(name="a2a_message_tool") - async def send_message(self, message_parts, task_id=None, context_id=None): - # ... - ``` - This instruments the `send_message` method of the `A2AClient`, making each message sent to the A2A service a traceable tool call. - -* **LLM Tool** (`services/llm_client.py`): - ```python - @tool(name="llm_tool") - async def process(self, user_input, session): - # ... - ``` - This instruments calls to the Large Language Model, tracking the prompts sent and responses received. - -* **Insurance Client Tools (MCP Protocol)** (`services/insurance_client.py`): - ```python - @tool(name="insurance_discovery_tool") - async def discovery(self, name, dob, state): - # ... - @tool(name="insurance_eligibility_tool") - async def eligibility(self, name, dob, subscriber_id, payer_name, provider_name): - # ... - ``` - These tools instrument the interactions with the Insurance Client, which uses the MCP (Managed Care Protocol) for discovery and eligibility checks. Each call to these methods will be captured as a tool invocation, providing visibility into the MCP service interactions. - ---- - -## 6. A2A Protocol Observability - -The `A2AClient` in `services/a2a_client.py` is specifically designed for A2A protocol interactions and includes dedicated instrumentation: - -```python -from ioa_observe.sdk.instrumentations.a2a import A2AInstrumentor -# ... -class A2AClient: - def __init__(self): - # ... - A2AInstrumentor().instrument() -``` -The `A2AInstrumentor().instrument()` call is crucial. It automatically instruments HTTP requests made by the `requests` library (used by `A2AClient` for `_timed_request`), enriching them with A2A-specific context. This means that when the `A2AClient` sends messages or performs discovery, the underlying HTTP calls are automatically traced, and their spans are linked to the A2A task and message IDs, providing a clear view of the A2A communication flow within the overall trace. - ---- - -## 7. Metrics - -The agent also records specific metrics related to its availability and activity in `agent/healthcare_agent.py`: - -```python -from ioa_observe.sdk.metrics.agents.availability import agent_availability -# ... -class HealthcareAgent: - async def start(self): - # ... - agent_availability.record_agent_heartbeat("healthcare_voice_agent") - # ... - if turn %5 ==0: - agent_availability.record_agent_heartbeat("healthcare_voice_agent") - # ... - agent_availability.record_agent_activity("healthcare_voice_agent", success=False) - # ... - agent_availability.record_agent_activity("healthcare_voice_agent", success=True) -``` -* `agent_availability.record_agent_heartbeat("healthcare_voice_agent")`: This metric indicates that the agent is alive and operational. It's recorded at startup and periodically during the conversation (every 5 turns). -* `agent_availability.record_agent_activity("healthcare_voice_agent", success=True/False)`: This metric tracks the agent's activity and whether a specific interaction was successful or not. It's used after processing user input to indicate successful processing or failures (e.g., unclear audio). - ---- - -## 8. Session Tracing - -The `session_start()` function is used within the `A2AClient.send_message` method in `services/a2a_client.py` to mark the beginning of a new session or a significant interaction within a trace. - -```python -from ioa_observe.sdk.tracing import session_start -# ... -class A2AClient: - async def send_message(self, message_parts, task_id=None, context_id=None): - # ... - session_start() - # ... -``` -This helps in organizing traces, especially in long-running conversations, by explicitly denoting the start of a new logical session or interaction within the tracing system. - ---- - -## 9. Configuration - -### Environment Variables -The agent requires the following environment variables (loaded via `config/settings.py`): - -```bash -# LLM Configuration -JWT_TOKEN=your_jwt_token -ENDPOINT_URL=your_llm_endpoint_url -PROJECT_ID=your_project_id -CONNECTION_ID=your_connection_id - -# Insurance MCP Configuration -MCP_URL=your_mcp_server_url -X_INF_API_KEY=your_insurance_api_key - -# A2A Protocol Configuration -A2A_SERVICE_URL=http://localhost:8887 -A2A_MESSAGE_URL=http://localhost:8887 -A2A_API_KEY=your_a2a_api_key - -# Observability -OTLP_ENDPOINT=http://localhost:4318 -``` - -### Configuration Validation -The `main.py` entry point validates all required environment variables before starting the agent: -```python -jwt_required = ['JWT_TOKEN', 'ENDPOINT_URL', 'PROJECT_ID', 'CONNECTION_ID'] -insurance_required = ['MCP_URL', 'X_INF_API_KEY'] -a2a_required = ['A2A_SERVICE_URL', 'A2A_MESSAGE_URL', 'A2A_API_KEY'] -``` -If any required variables are missing, the agent will print an error message and exit gracefully. - ---- - -## Installation & Setup - -### Prerequisites -- Python 3.8 or higher -- macOS, Linux, or Windows -- Microphone access (for voice features) - -### Quick Start -```bash -# Clone and navigate to project -cd healthcare-booking_app - -# Create virtual environment -python3 -m venv venv -source venv/bin/activate # On Windows: venv\Scripts\activate - -# Install dependencies -pip install -r requirements.txt - -# Create .env file with your configuration -cp .env.example .env # Edit with your credentials - -# Run the agent -python main.py -``` - ---- - - -## Support - -For issues or questions, please open troubleshooting documentation in the repository -https://github.com/techm-2025/customer-support/tree/main/agentic-healthcare-booking-app/documentation/troubleshooting \ No newline at end of file diff --git a/agentic-healthcare-booking-app/voice_agent/agntcy/agent/healthcare_agent.py b/agentic-healthcare-booking-app/voice_agent/agntcy/agent/healthcare_agent.py index c1f7bbb..1db21b2 100644 --- a/agentic-healthcare-booking-app/voice_agent/agntcy/agent/healthcare_agent.py +++ b/agentic-healthcare-booking-app/voice_agent/agntcy/agent/healthcare_agent.py @@ -1,88 +1,103 @@ -import asyncio -import os +""" +Healthcare Voice Agent +""" import time import random import string -import sys -from pathlib import Path from ioa_observe.sdk.decorators import agent, workflow from ioa_observe.sdk.metrics.agents.availability import agent_availability -from models.session import Session -from services.llm_client import LLMClient -from services.audio_service import AudioSystem -from services.a2a_client import A2AClient -from services.insurance_client import InsuranceClient -from dotenv import load_dotenv - +from config.settings import Settings +from clients.a2a_client import A2AClient +from clients.llm_client import LLMClient +from clients.insurance_client import InsuranceClient +from audio.audio import AudioSystem +from session.session import Session from a2a.types import TaskState -load_dotenv() + @agent(name="healthcare_agent", description="healthcare voice agent", version="1.0.0", protocol="A2A") class HealthcareAgent: + """Main healthcare appointment scheduling agent""" + def __init__(self): - self.session = Session() + settings = Settings() + + self.session = Session(session_dir=settings.session_dir) self.audio = AudioSystem() # Initialize LLM client - jwt_token = os.getenv('JWT_TOKEN') - endpoint_url = os.getenv('ENDPOINT_URL') - project_id = os.getenv('PROJECT_ID') - connection_id = os.getenv('CONNECTION_ID') - - if not all([jwt_token, endpoint_url, project_id, connection_id]): - raise Exception("Missing JWT config") - - self.llm = LLMClient(jwt_token, endpoint_url, project_id, connection_id) + self.llm = LLMClient( + settings.jwt_token, + settings.endpoint_url, + settings.project_id, + settings.connection_id + ) # Initialize insurance client - mcp_url = os.getenv('MCP_URL') - insurance_key = os.getenv('X_INF_API_KEY') - if not mcp_url or not insurance_key: - raise Exception("Missing insurance config") - - self.insurance = InsuranceClient(mcp_url, insurance_key) + self.insurance = InsuranceClient(settings.mcp_url, settings.insurance_api_key) # Initialize A2A client self.a2a_client = None try: - self.a2a_client = A2AClient() - except: - print("A2A client not available") + self.a2a_client = A2AClient( + settings.a2a_service_url, + settings.a2a_message_url, + settings.a2a_api_key, + settings.otlp_endpoint + ) + except Exception as e: + print(f"A2A client initialization failed: {e}") + + self.max_turns = settings.max_turns + self.max_errors = settings.max_errors async def start(self): - print(f"Healthcare Agent starting - Session {self.session.id}") - from ioa_observe.sdk.metrics.agents.availability import agent_availability + """Start the agent conversation""" + print(f"\n{'='*60}") + print(f"Healthcare Agent Starting - Session {self.session.id}") + print(f"{'='*60}\n") + + # Record agent heartbeat start = time.time() agent_availability.record_agent_heartbeat("healthcare_voice_agent") observe_latency = time.time() - start - print(f"Observe heartbeat latency: {observe_latency:.2f} seconds") - print(f"Healthcare Agent starting - Session {self.session.id}") + print(f"✓ Observe heartbeat latency: {observe_latency:.2f}s\n") + + # Discover A2A agent if available if self.a2a_client: await self.a2a_client.discover_agent() + # Initial greeting initial_message = "Hello! I'm your healthcare appointment assistant. Let's start by getting your basic information. What's your full name?" await self.audio.speak(initial_message) self.session.add_interaction("assistant", initial_message) + # Main conversation loop turn = 0 errors = 0 - while turn < 50 and errors < 3: + while turn < self.max_turns and errors < self.max_errors: turn += 1 - print(f"--- Turn {turn} ---") - if turn %5 ==0: + print(f"\n--- Turn {turn} ---") + + # Periodic heartbeat + if turn % 5 == 0: agent_availability.record_agent_heartbeat("healthcare_voice_agent") + # Listen for user input user_input = await self.audio.listen(timeout=5) + # Handle audio errors if user_input in ["UNCLEAR", "TIMEOUT", "ERROR"]: errors += 1 agent_availability.record_agent_activity("healthcare_voice_agent", success=False) + if user_input == "TIMEOUT": await self.audio.speak("I'm still here. What would you like me to help you with?") else: await self.audio.speak("I didn't catch that clearly. Could you please repeat?") continue + agent_availability.record_agent_activity("healthcare_voice_agent", success=True) if not user_input: @@ -92,90 +107,114 @@ async def start(self): print(f"USER: {user_input}") self.session.add_interaction("user", user_input) + # Check for exit commands if any(phrase in user_input.lower() for phrase in ['bye', 'goodbye', 'end', 'quit']): await self.audio.speak("Thank you for calling. Have a great day!") self.session.add_interaction("assistant", "Thank you for calling. Have a great day!") break + # Handle conversation based on mode if self.session.in_triage_mode: await self._handle_triage_conversation(user_input) else: - result = await self.llm.process(user_input, self.session) - - if result.get("extract"): - for key, value in result["extract"].items(): - if value: - self.session.data[key] = value - print(f"SESSION-UPDATE: Set {key} = {value}") - - if (result.get("need_triage") and not self.session.triage_complete and - self.session.triage_attempts < 1 and self.a2a_client): - - print("TRIAGE: Starting integrated triage conversation") - await self._start_integrated_triage() - continue - - if result.get("call_discovery"): - required = ['name', 'date_of_birth', 'state'] - if all(k in self.session.data and self.session.data[k] for k in required): - print("INSURANCE-DISCOVERY: Calling API...") - discovery = await self.insurance.discovery( - self.session.data['name'], - self.session.data['date_of_birth'], - self.session.data['state'] - ) - if discovery["success"]: - self.session.data['payer'] = discovery['payer'] - self.session.data['member_id'] = discovery['member_id'] - - insurance_message = f"Great! I found your insurance: {discovery['payer']}, Policy ID: {discovery['member_id']}." - await self.audio.speak(insurance_message) - self.session.add_interaction("assistant", insurance_message) - else: - fallback_msg = "I had some trouble finding your insurance, but we can proceed." - await self.audio.speak(fallback_msg) - self.session.add_interaction("assistant", fallback_msg) - - if result.get("call_eligibility"): - required = ['name', 'date_of_birth', 'member_id', 'payer', 'provider_name'] - if all(k in self.session.data and self.session.data[k] for k in required): - print("INSURANCE-ELIGIBILITY: Calling API...") - eligibility = await self.insurance.eligibility( - self.session.data['name'], - self.session.data['date_of_birth'], - self.session.data['member_id'], - self.session.data['payer'], - self.session.data['provider_name'] - ) - if eligibility["success"] and eligibility.get('copay'): - eligibility_message = f"Perfect! Your insurance is verified. Payer: {self.session.data['payer']}, Policy ID: {self.session.data['member_id']}, Your copay will be ${eligibility['copay']}." - await self.audio.speak(eligibility_message) - self.session.add_interaction("assistant", eligibility_message) - else: - fallback_message = f"Your insurance {self.session.data['payer']} with Policy ID {self.session.data['member_id']} is on file. We can proceed with scheduling." - await self.audio.speak(fallback_message) - self.session.add_interaction("assistant", fallback_message) - - response = result.get("response", "") - if response: - await self.audio.speak(response) - self.session.add_interaction("assistant", response) - - if result.get("done"): - confirmation = ''.join(random.choices(string.ascii_uppercase + string.digits, k=6)) - final_message = f"Excellent! Your appointment is confirmed. Confirmation number: {confirmation}. You'll receive an email confirmation shortly. Thank you for calling!" - await self.audio.speak(final_message) - self.session.add_interaction("assistant", final_message) - break + await self._handle_appointment_flow(user_input) + # End of conversation + print(f"\n{'='*60}") print(f"Conversation ended. Final data: {self.session.data}") + print(f"{'='*60}\n") + # Save session saved_file = self.session.save_to_file() if saved_file: - print(f"Session saved to: {saved_file}") + print(f"✓ Session saved to: {saved_file}") + + async def _handle_appointment_flow(self, user_input): + """Handle normal appointment scheduling flow""" + result = await self.llm.process(user_input, self.session) + + # Extract data fields + if result.get("extract"): + for key, value in result["extract"].items(): + if value: + self.session.data[key] = value + print(f"SESSION-UPDATE: Set {key} = {value}") + + # Start triage if needed + if (result.get("need_triage") and not self.session.triage_complete and + self.session.triage_attempts < 1 and self.a2a_client): + + print("TRIAGE: Starting integrated triage conversation") + await self._start_integrated_triage() + return + + # Call insurance discovery + if result.get("call_discovery"): + await self._handle_insurance_discovery() + + # Call insurance eligibility + if result.get("call_eligibility"): + await self._handle_insurance_eligibility() + + # Speak response + response = result.get("response", "") + if response: + await self.audio.speak(response) + self.session.add_interaction("assistant", response) + + # Complete appointment + if result.get("done"): + confirmation = ''.join(random.choices(string.ascii_uppercase + string.digits, k=6)) + final_message = f"Excellent! Your appointment is confirmed. Confirmation number: {confirmation}. You'll receive an email confirmation shortly. Thank you for calling!" + await self.audio.speak(final_message) + self.session.add_interaction("assistant", final_message) + + async def _handle_insurance_discovery(self): + """Handle insurance discovery API call""" + required = ['name', 'date_of_birth', 'state'] + if all(k in self.session.data and self.session.data[k] for k in required): + print("INSURANCE-DISCOVERY: Calling API...") + discovery = await self.insurance.discovery( + self.session.data['name'], + self.session.data['date_of_birth'], + self.session.data['state'] + ) + if discovery["success"]: + self.session.data['payer'] = discovery['payer'] + self.session.data['member_id'] = discovery['member_id'] + + insurance_message = f"Great! I found your insurance: {discovery['payer']}, Policy ID: {discovery['member_id']}." + await self.audio.speak(insurance_message) + self.session.add_interaction("assistant", insurance_message) + else: + fallback_msg = "I had some trouble finding your insurance, but we can proceed." + await self.audio.speak(fallback_msg) + self.session.add_interaction("assistant", fallback_msg) + + async def _handle_insurance_eligibility(self): + """Handle insurance eligibility API call""" + required = ['name', 'date_of_birth', 'member_id', 'payer', 'provider_name'] + if all(k in self.session.data and self.session.data[k] for k in required): + print("INSURANCE-ELIGIBILITY: Calling API...") + eligibility = await self.insurance.eligibility( + self.session.data['name'], + self.session.data['date_of_birth'], + self.session.data['member_id'], + self.session.data['payer'], + self.session.data['provider_name'] + ) + if eligibility["success"] and eligibility.get('copay'): + eligibility_message = f"Perfect! Your insurance is verified. Payer: {self.session.data['payer']}, Policy ID: {self.session.data['member_id']}, Your copay will be ${eligibility['copay']}." + await self.audio.speak(eligibility_message) + self.session.add_interaction("assistant", eligibility_message) + else: + fallback_message = f"Your insurance {self.session.data['payer']} with Policy ID {self.session.data['member_id']} is on file. We can proceed with scheduling." + await self.audio.speak(fallback_message) + self.session.add_interaction("assistant", fallback_message) @workflow(name="integrated_triage_workflow") async def _start_integrated_triage(self): + """Start integrated triage conversation with A2A service""" self.session.triage_attempts += 1 self.session.in_triage_mode = True @@ -187,6 +226,7 @@ async def _start_integrated_triage(self): await self.audio.speak(triage_intro) self.session.add_interaction("assistant", triage_intro) + # Default demographics for triage age = 33 sex = "female" complaint = self.session.data.get('reason', 'general health concern') @@ -197,12 +237,7 @@ async def _start_integrated_triage(self): if not result: print("TRIAGE: Failed to start - falling back to normal flow") await self._end_triage_mode("I'll help you schedule your appointment without the assessment.") - return { - "goto":"__end__", - "error":True, - "success":False, - "reason":"triage_start_failed" - } + return if result.get('kind') == 'task': self.session.triage_task_id = result['id'] @@ -215,55 +250,38 @@ async def _start_integrated_triage(self): if triage_question: await self.audio.speak(triage_question) self.session.add_interaction("assistant", triage_question) - return { - "goto": "triage_service_agent", - "success": True, - "task_id" : result['id'], - "context_id":result['contextId'], - "action":"triage_started" - } except Exception as e: print(f"TRIAGE: Error starting: {e}") await self._end_triage_mode("Let me help you schedule your appointment.") - return{ - "goto":"__end__", - "error":True, - "success":False, - "error_message":str(e) - } @workflow(name="triage_conversational_flow") async def _handle_triage_conversation(self, user_input): + """Handle multi-turn triage conversation""" print(f"TRIAGE: User response: {user_input}") try: message_parts = [{"kind": "text", "text": user_input}] result = await self.a2a_client.send_message( - message_parts, - task_id=self.session.triage_task_id, + message_parts, + task_id=self.session.triage_task_id, context_id=self.session.triage_context_id ) if not result: print("TRIAGE: Failed to continue - ending triage") await self._end_triage_mode("Let me help you continue with scheduling your appointment.") - return{ - "goto":"__end__", - "error":True, - "success":False, - "reason":"triage_communication_failed" - } + return - task_data = result.get('a2a_response', result) task_state = result['status']['state'] print(f"TRIAGE: A2A task state: {task_state}") - if task_state == TaskState.COMPLETED: - print("TRIAGE: Assessment COMPLETED - exiting A2A mode") + if task_state == TaskState.completed: + print("TRIAGE: Assessment COMPLETED") - if task_data.get('artifacts'): - artifact = task_data['artifacts'][0] + # Extract triage results + if result.get('artifacts'): + artifact = result['artifacts'][0] triage_data = self._extract_triage_results(artifact) if triage_data: self.session.triage_results.update(triage_data) @@ -275,76 +293,42 @@ async def _handle_triage_conversation(self, user_input): completion_message = f"Thank you for the assessment. Based on your responses, I recommend seeing a {doctor_type}. Priority level: {urgency}. Now let's get you scheduled. I'll need your date of birth for insurance verification." await self._end_triage_mode() - await self.audio.speak(completion_message) self.session.add_interaction("assistant", completion_message) - return { - "goto":"__end__", - "success":True, - "triage_complete":True, - "urgency_level":urgency, - "doctor_type":doctor_type - } - - elif task_state == TaskState.INPUT_REQUIRED: + elif task_state == TaskState.input_required: if result['status'].get('message'): next_question = self._extract_text_from_message(result['status']['message']) if next_question: await self.audio.speak(next_question) self.session.add_interaction("assistant", next_question) - return { - "goto":"triage_service_agent", - "success":True, - "action":"continue_triage", - "state":"awaiting_user_input" - } else: print("TRIAGE: No message in input-required state - ending triage") await self._end_triage_mode("Let me help you continue with scheduling your appointment.") - return { - "goto":"__end__", - "error":True, - "success":False, - "reason":"no_triage_message" - } - - elif task_state in [TaskState.FAILED, TaskState.CANCELED]: + + elif task_state in [TaskState.failed, TaskState.canceled]: print(f"TRIAGE: Task ended with state: {task_state}") await self._end_triage_mode("Let me help you continue with scheduling your appointment.") - return { - "goto":"__end__", - "error":True, - "success":False, - "task_state":task_state - } - + except Exception as e: print(f"TRIAGE: Error in conversation: {e}") await self._end_triage_mode("Let me help you continue with scheduling your appointment.") - return { - "goto":"__end__", - "error":True, - "success":False, - "error_message":str(e) - } async def _end_triage_mode(self, message=None): - print("TRIAGE: Ending triage mode - cleaning up A2A connection") + """End triage mode and return to normal flow""" + print("TRIAGE: Ending triage mode") self.session.in_triage_mode = False self.session.triage_complete = True - self.session.triage_task_id = None self.session.triage_context_id = None - print("TRIAGE: Mode ended - returning to normal appointment flow") - if message: await self.audio.speak(message) self.session.add_interaction("assistant", message) def _extract_text_from_message(self, message): + """Extract text content from A2A message""" if not message or not message.get('parts'): return None @@ -355,6 +339,7 @@ def _extract_text_from_message(self, message): return None def _extract_triage_results(self, artifact): + """Extract triage results from artifact""" if not artifact or not artifact.get('parts'): return {} diff --git a/agentic-healthcare-booking-app/voice_agent/agntcy/audio/__init__.py b/agentic-healthcare-booking-app/voice_agent/agntcy/audio/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/agentic-healthcare-booking-app/voice_agent/agntcy/services/audio_service.py b/agentic-healthcare-booking-app/voice_agent/agntcy/audio/audio.py similarity index 61% rename from agentic-healthcare-booking-app/voice_agent/agntcy/services/audio_service.py rename to agentic-healthcare-booking-app/voice_agent/agntcy/audio/audio.py index c06c5bd..d06c1c5 100644 --- a/agentic-healthcare-booking-app/voice_agent/agntcy/services/audio_service.py +++ b/agentic-healthcare-booking-app/voice_agent/agntcy/audio/audio.py @@ -1,3 +1,6 @@ +""" +Audio System for Speech Recognition and Text-to-Speech +""" import asyncio import os import tempfile @@ -10,59 +13,68 @@ AUDIO_AVAILABLE = True except ImportError: AUDIO_AVAILABLE = False - print("Audio libraries not found. Audio functionalities will be disabled.") + class AudioSystem: + """Handles speech recognition and text-to-speech""" + def __init__(self): self.enabled = AUDIO_AVAILABLE self.tts_enabled = False self.speech_enabled = False if self.enabled: + self._initialize_audio() + + def _initialize_audio(self): + """Initialize audio components""" + try: + print("Initializing audio...") + + # Initialize speech recognition try: - print("Initializing audio...") - - try: - self.recognizer = sr.Recognizer() - self.microphone = sr.Microphone() - with self.microphone as source: - self.recognizer.adjust_for_ambient_noise(source, duration=1) - - self.recognizer.energy_threshold = 300 - self.recognizer.dynamic_energy_threshold = True - self.recognizer.pause_threshold = 0.8 - self.speech_enabled = True - print("Speech recognition ready") - except Exception as e: - print(f"Speech recognition failed: {e}") - self.speech_enabled = False + self.recognizer = sr.Recognizer() + self.microphone = sr.Microphone() + with self.microphone as source: + self.recognizer.adjust_for_ambient_noise(source, duration=1) - try: - pygame.mixer.pre_init(frequency=22050, size=-16, channels=2, buffer=1024) - pygame.mixer.init() - self.tts_enabled = True - print("TTS system ready") - except Exception as e: - print(f"TTS init failed: {e}") - self.tts_enabled = False - + self.recognizer.energy_threshold = 300 + self.recognizer.dynamic_energy_threshold = True + self.recognizer.pause_threshold = 0.8 + self.speech_enabled = True + print("✓ Speech recognition ready") except Exception as e: - print(f"Audio init failed: {e}") - self.enabled = False + print(f"✗ Speech recognition failed: {e}") + self.speech_enabled = False + + # Initialize TTS + try: + pygame.mixer.pre_init(frequency=22050, size=-16, channels=2, buffer=1024) + pygame.mixer.init() + self.tts_enabled = True + print("✓ TTS system ready") + except Exception as e: + print(f"✗ TTS init failed: {e}") + self.tts_enabled = False + + except Exception as e: + print(f"✗ Audio init failed: {e}") + self.enabled = False @tool(name="listening_tool") async def listen(self, timeout=5): + """Listen for user input via microphone or console""" if not self.speech_enabled: return input("You: ").strip() - print("Listening...") + print("🎤 Listening...") def _listen(): try: with self.microphone as source: audio = self.recognizer.listen(source, timeout=timeout, phrase_time_limit=6) result = self.recognizer.recognize_google(audio, language='en-US') - print(f"Recognized: '{result}'") + print(f"✓ Recognized: '{result}'") return result.strip() except sr.UnknownValueError: return "UNCLEAR" @@ -76,10 +88,10 @@ def _listen(): @tool(name="speaking_tool") async def speak(self, text): - print(f"Agent: {text}") + """Speak text using TTS or print to console""" + print(f"🤖 Agent: {text}") if not self.tts_enabled: - print("TTS: Not enabled, skipping audio") return def _speak(): @@ -94,6 +106,7 @@ def _speak(): pygame.mixer.music.load(temp_file) pygame.mixer.music.play() + # Wait for playback to complete (max 30 seconds) max_wait = 30 wait_count = 0 while pygame.mixer.music.get_busy() and wait_count < max_wait * 20: @@ -123,4 +136,4 @@ def _speak(): timeout=35 ) except Exception as e: - print(f"TTS: Error: {e}") + print(f"TTS: Error: {e}") \ No newline at end of file diff --git a/agentic-healthcare-booking-app/voice_agent/agntcy/clients/__init__.py b/agentic-healthcare-booking-app/voice_agent/agntcy/clients/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/agentic-healthcare-booking-app/voice_agent/agntcy/services/a2a_client.py b/agentic-healthcare-booking-app/voice_agent/agntcy/clients/a2a_client.py similarity index 80% rename from agentic-healthcare-booking-app/voice_agent/agntcy/services/a2a_client.py rename to agentic-healthcare-booking-app/voice_agent/agntcy/clients/a2a_client.py index 6474376..3efd1ff 100644 --- a/agentic-healthcare-booking-app/voice_agent/agntcy/services/a2a_client.py +++ b/agentic-healthcare-booking-app/voice_agent/agntcy/clients/a2a_client.py @@ -1,24 +1,28 @@ +""" +A2A (Agent-to-Agent) Client for Hosted Service Communication +""" import asyncio -import time,os -import requests +import time import uuid -from http import HTTPStatus +import requests from ioa_observe.sdk import Observe +from ioa_observe.sdk.instrumentations.a2a import A2AInstrumentor from ioa_observe.sdk.decorators import tool from ioa_observe.sdk.tracing import session_start -from ioa_observe.sdk.instrumentations.a2a import A2AInstrumentor -from dotenv import load_dotenv -load_dotenv() + class A2AClient: - def __init__(self): - self.base_url = os.getenv('A2A_SERVICE_URL', 'http://localhost:8887') - self.message_url = os.getenv('A2A_MESSAGE_URL', self.base_url) - self.api_key = os.getenv('A2A_API_KEY') + """Client for communicating with A2A hosted services""" + + def __init__(self, base_url, message_url, api_key, otlp_endpoint): + self.base_url = base_url + self.message_url = message_url + self.api_key = api_key self.agent_id = f"client_{uuid.uuid4().hex[:8]}" self.agent_card = None - api_endpoint = os.getenv('OTLP_ENDPOINT', 'http://localhost:4318') - Observe.init("A2A_Client", api_endpoint=api_endpoint) + + # Initialize observability + Observe.init("A2A_Client", api_endpoint=otlp_endpoint) A2AInstrumentor().instrument() print(f"A2A-CLIENT: Initialized as {self.agent_id}") @@ -27,6 +31,7 @@ def __init__(self): print(f"A2A-CLIENT: API Key: {'Set' if self.api_key else 'Not set'}") def _timed_request(self, method, url, description, **kwargs): + """Execute a timed HTTP request with detailed logging""" start_time = time.time() timestamp = time.strftime("%H:%M:%S", time.localtime(start_time)) print(f"A2A-CLIENT: [{timestamp}] >>> {method} {description}") @@ -43,7 +48,7 @@ def _timed_request(self, method, url, description, **kwargs): elapsed_ms = elapsed * 1000 print(f"A2A-CLIENT: [{end_timestamp}] <<< {response.status_code} | {elapsed:.3f}s ({elapsed_ms:.0f}ms)") - if response.status_code != HTTPStatus.OK: + if response.status_code != 200: print(f"A2A-CLIENT: Error response: {response.text[:200]}") else: print(f"A2A-CLIENT: Success - response length: {len(response.text)} chars") @@ -57,17 +62,22 @@ def _timed_request(self, method, url, description, **kwargs): return None, elapsed async def discover_agent(self): + """Discover the A2A agent's capabilities""" try: def _request(): - return self._timed_request('GET', f"{self.base_url}/.well-known/agent-card.json", - "Agent Discovery", timeout=30) + return self._timed_request( + 'GET', + f"{self.base_url}/.well-known/agent-card.json", + "Agent Discovery", + timeout=30 + ) loop = asyncio.get_event_loop() response, elapsed = await loop.run_in_executor(None, _request) - if response and response.status_code == HTTPStatus.OK: + if response and response.status_code == 200: self.agent_card = response.json() - print(f"A2A-CLIENT: Discovered agent: {self.agent_card['name']}") + print(f"A2A-CLIENT: Discovered agent: {self.agent_card.get('name', 'Unknown')}") return True else: if response: @@ -79,6 +89,7 @@ def _request(): @tool(name="a2a_message_tool") async def send_message(self, message_parts, task_id=None, context_id=None): + """Send a message to the A2A service""" message = { "role": "user", "parts": message_parts, @@ -90,7 +101,10 @@ async def send_message(self, message_parts, task_id=None, context_id=None): message["taskId"] = task_id if context_id: message["contextId"] = context_id + + # Start observability session session_start() + payload = { "jsonrpc": "2.0", "id": str(uuid.uuid4()), @@ -115,6 +129,8 @@ async def send_message(self, message_parts, task_id=None, context_id=None): try: def _request(): headers = {"Content-Type": "application/json"} + + # Add API key if available if self.api_key: headers['X-Shared-Key'] = self.api_key @@ -122,13 +138,19 @@ def _request(): if task_id: description += f" (Task: {task_id})" - return self._timed_request('POST', self.message_url, description, - json=payload, headers=headers, timeout=60) + return self._timed_request( + 'POST', + self.message_url, + description, + json=payload, + headers=headers, + timeout=60 + ) loop = asyncio.get_event_loop() response, elapsed = await loop.run_in_executor(None, _request) - if response and response.status_code == HTTPStatus.OK: + if response and response.status_code == 200: data = response.json() if 'result' in data: result = data['result'] diff --git a/agentic-healthcare-booking-app/voice_agent/agntcy/services/insurance_client.py b/agentic-healthcare-booking-app/voice_agent/agntcy/clients/insurance_client.py similarity index 76% rename from agentic-healthcare-booking-app/voice_agent/agntcy/services/insurance_client.py rename to agentic-healthcare-booking-app/voice_agent/agntcy/clients/insurance_client.py index 3f7c3ea..e42dfa2 100644 --- a/agentic-healthcare-booking-app/voice_agent/agntcy/services/insurance_client.py +++ b/agentic-healthcare-booking-app/voice_agent/agntcy/clients/insurance_client.py @@ -1,17 +1,23 @@ +""" +Insurance/MCP Client for discovery and eligibility checks +""" import asyncio import re import requests -from http import HTTPStatus -from ioa_observe.sdk.decorators import tool from datetime import datetime +from ioa_observe.sdk.decorators import tool + class InsuranceClient: + """Client for insurance discovery and eligibility verification via MCP""" + def __init__(self, mcp_url, api_key): self.mcp_url = mcp_url self.headers = {"Content-Type": "application/json", "X-INF-API-KEY": api_key} print("INSURANCE: Client initialized") def _split_name(self, name): + """Split full name into first and last""" parts = name.strip().split() if len(parts) == 1: return parts[0], "" @@ -21,14 +27,16 @@ def _split_name(self, name): return parts[0], " ".join(parts[1:]) def _format_dob(self, dob): + """Format date of birth to YYYY-MM-DD""" if not dob: return "" + # Handle MM/DD/YYYY format if re.match(r'^\d{1,2}/\d{1,2}/\d{4}$', dob): month, day, year = dob.split('/') - formatted = f"{year}-{month.zfill(2)}-{day.zfill(2)}" - return formatted + return f"{year}-{month.zfill(2)}-{day.zfill(2)}" + # Already in YYYY-MM-DD format if re.match(r'^\d{4}-\d{1,2}-\d{1,2}$', dob): return dob @@ -36,7 +44,9 @@ def _format_dob(self, dob): @tool(name="insurance_discovery_tool") async def discovery(self, name, dob, state): + """Discover insurance information for a patient""" print(f"INSURANCE: Discovery - {name}, {dob}, {state}") + first, last = self._split_name(name) formatted_dob = self._format_dob(dob) formatted_state = state.strip().title() if state else "" @@ -62,37 +72,44 @@ def _request(): loop = asyncio.get_event_loop() response = await loop.run_in_executor(None, _request) - if response.status_code == HTTPStatus.OK: + if response.status_code == 200: data = response.json() if "result" in data: result_text = str(data["result"]) + # Extract payer name payer = "" - member_id = "" - for pattern in [r'payer[:\s]*([^\n,;]+)', r'insurance[:\s]*([^\n,;]+)', r'plan[:\s]*([^\n,;]+)']: match = re.search(pattern, result_text.lower()) if match: payer = match.group(1).strip().title() break - for pattern in [r'member\s*id[:\s]*([a-za-z0-9\-]+)', r'subscriber\s*id[:\s]*([a-za-z0-9\-]+)', r'policy\s*id[:\s]*([a-za-z0-9\-]+)', r'policy[:\s]*([a-za-z0-9\-]+)']: + # Extract member ID + member_id = "" + for pattern in [r'member\s*id[:\s]*([a-za-z0-9\-]+)', r'subscriber\s*id[:\s]*([a-za-z0-9\-]+)', + r'policy\s*id[:\s]*([a-za-z0-9\-]+)', r'policy[:\s]*([a-za-z0-9\-]+)']: match = re.search(pattern, result_text.lower()) if match: member_id = match.group(1).strip().upper() break + print(f"INSURANCE: Discovery success - Payer: {payer}, Member ID: {member_id}") return {"success": True, "payer": payer, "member_id": member_id} + print("INSURANCE: Discovery failed") return {"success": False} @tool(name="insurance_eligibility_tool") async def eligibility(self, name, dob, subscriber_id, payer_name, provider_name): - print(f"INSURANCE: Eligibility check") + """Check insurance eligibility and benefits""" + print(f"INSURANCE: Eligibility check for {name}") + first, last = self._split_name(name) formatted_dob = self._format_dob(dob) + # Clean provider name provider_clean = re.sub(r'\b(Dr\.?|MD|DO)\b', '', provider_name, flags=re.IGNORECASE).strip() provider_first, provider_last = self._split_name(provider_clean) @@ -110,7 +127,7 @@ async def eligibility(self, name, dob, subscriber_id, payer_name, provider_name) "payerName": payer_name, "providerFirstName": provider_first, "providerLastName": provider_last, - "providerNpi": "1234567890" + "providerNpi": "1234567890" # Default NPI } } } @@ -121,14 +138,19 @@ def _request(): loop = asyncio.get_event_loop() response = await loop.run_in_executor(None, _request) - if response.status_code == HTTPStatus.OK: + if response.status_code == 200: data = response.json() if "result" in data: result_text = str(data["result"]) + # Extract copay amount copay = "" - copay_patterns = [r'co-?pay[:\s]*\$?([0-9,]+)', r'copayment[:\s]*\$?([0-9,]+)', r'patient\s+responsibility[:\s]*\$?([0-9,]+)'] + copay_patterns = [ + r'co-?pay[:\s]*\$?([0-9,]+)', + r'copayment[:\s]*\$?([0-9,]+)', + r'patient\s+responsibility[:\s]*\$?([0-9,]+)' + ] for pattern in copay_patterns: copay_match = re.search(pattern, result_text.lower()) @@ -136,6 +158,8 @@ def _request(): copay = copay_match.group(1) break + print(f"INSURANCE: Eligibility success - Copay: ${copay}") return {"success": True, "copay": copay} + print("INSURANCE: Eligibility check failed") return {"success": False} \ No newline at end of file diff --git a/agentic-healthcare-booking-app/voice_agent/agntcy/services/llm_client.py b/agentic-healthcare-booking-app/voice_agent/agntcy/clients/llm_client.py similarity index 76% rename from agentic-healthcare-booking-app/voice_agent/agntcy/services/llm_client.py rename to agentic-healthcare-booking-app/voice_agent/agntcy/clients/llm_client.py index cc785ff..1563168 100644 --- a/agentic-healthcare-booking-app/voice_agent/agntcy/services/llm_client.py +++ b/agentic-healthcare-booking-app/voice_agent/agntcy/clients/llm_client.py @@ -1,11 +1,15 @@ +""" +LLM Client for processing user inputs +""" import asyncio -import json,os +import json import requests -from http import HTTPStatus from ioa_observe.sdk.decorators import tool -from models.session import Session + class LLMClient: + """Client for LLM-based conversation processing""" + def __init__(self, jwt_token, endpoint_url, project_id, connection_id): self.headers = { 'Content-Type': 'application/json', @@ -18,10 +22,62 @@ def __init__(self, jwt_token, endpoint_url, project_id, connection_id): @tool(name="llm_tool") async def process(self, user_input, session): + """Process user input and return structured response""" print(f"LLM: Processing: '{user_input[:50]}...'") if session.in_triage_mode: - prompt = f"""You are in TRIAGE MODE. The user is answering medical assessment questions. + prompt = self._build_triage_prompt(user_input, session) + else: + prompt = self._build_appointment_prompt(user_input, session) + + payload = { + "messages": [ + {"role": "system", "content": prompt}, + {"role": "user", "content": user_input} + ], + "project_id": self.project_id, + "connection_id": self.connection_id, + "max_tokens": 400, + "temperature": 0.2 + } + + def _request(): + return requests.post(self.endpoint_url, headers=self.headers, json=payload, timeout=30) + + loop = asyncio.get_event_loop() + response = await loop.run_in_executor(None, _request) + + if response.status_code == 200: + data = response.json() + if 'choices' in data and data['choices']: + content = data['choices'][0]['message']['content'] + + try: + # Clean JSON response + if content.startswith('```json'): + content = content[7:] + if content.endswith('```'): + content = content[:-3] + + result = json.loads(content.strip()) + print("LLM: Response parsed successfully") + return result + except Exception as e: + print(f"LLM: Failed to parse response: {e}") + + # Return default response on failure + return { + "response": "I understand. Please continue.", + "extract": {}, + "need_triage": False, + "call_discovery": False, + "call_eligibility": False, + "done": False + } + + def _build_triage_prompt(self, user_input, session): + """Build prompt for triage mode""" + return f"""You are in TRIAGE MODE. The user is answering medical assessment questions. Current triage task: {session.triage_task_id} User response to triage question: "{user_input}" @@ -36,8 +92,10 @@ async def process(self, user_input, session): "done": false, "continue_triage": true }}""" - else: - prompt = f"""You are a healthcare appointment scheduler with this specific flow: + + def _build_appointment_prompt(self, user_input, session): + """Build prompt for appointment scheduling mode""" + return f"""You are a healthcare appointment scheduler with this specific flow: 1. Ask name, phone 2. Ask reason for visit @@ -68,47 +126,4 @@ async def process(self, user_input, session): "call_discovery": true/false, "call_eligibility": true/false, "done": true/false -}}""" - - payload = { - "messages": [ - {"role": "system", "content": prompt}, - {"role": "user", "content": user_input} - ], - "project_id": self.project_id, - "connection_id": self.connection_id, - "max_tokens": 400, - "temperature": 0.2 - } - - def _request(): - return requests.post(self.endpoint_url, headers=self.headers, json=payload, timeout=30) - - loop = asyncio.get_event_loop() - response = await loop.run_in_executor(None, _request) - - if response.status_code == HTTPStatus.OK: - data = response.json() - if 'choices' in data and data['choices']: - content = data['choices'][0]['message']['content'] - - try: - if content.startswith('```json'): - content = content[7:] - if content.endswith('```'): - content = content[:-3] - - result = json.loads(content.strip()) - print("LLM: Response parsed") - return result - except: - pass - - return { - "response": "I understand. Please continue.", - "extract": {}, - "need_triage": False, - "call_discovery": False, - "call_eligibility": False, - "done": False - } \ No newline at end of file +}}""" \ No newline at end of file diff --git a/agentic-healthcare-booking-app/voice_agent/agntcy/config/__init__.py b/agentic-healthcare-booking-app/voice_agent/agntcy/config/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/agentic-healthcare-booking-app/voice_agent/agntcy/config/settings.py b/agentic-healthcare-booking-app/voice_agent/agntcy/config/settings.py new file mode 100644 index 0000000..7540e36 --- /dev/null +++ b/agentic-healthcare-booking-app/voice_agent/agntcy/config/settings.py @@ -0,0 +1,53 @@ +""" +Configuration Settings +""" +import os +from dotenv import load_dotenv + +# Try to import audio dependencies +try: + import speech_recognition as sr + import pygame + from gtts import gTTS + AUDIO_AVAILABLE = True +except ImportError: + AUDIO_AVAILABLE = False + + +class Settings: + """Application settings from environment variables""" + + def __init__(self): + load_dotenv() + + # LLM Configuration + self.jwt_token = os.getenv('JWT_TOKEN') + self.endpoint_url = os.getenv('ENDPOINT_URL') + self.project_id = os.getenv('PROJECT_ID') + self.connection_id = os.getenv('CONNECTION_ID') + + # Insurance/MCP Configuration + self.mcp_url = os.getenv('MCP_URL') + self.insurance_api_key = os.getenv('X_INF_API_KEY') + + # A2A Configuration + self.a2a_service_url = os.getenv('A2A_SERVICE_URL', 'http://localhost:8887') + self.a2a_message_url = os.getenv('A2A_MESSAGE_URL', self.a2a_service_url) + self.a2a_api_key = os.getenv('A2A_API_KEY') + + # TBAC Configuration + self.client_agent_api_key = os.getenv('CLIENT_AGENT_API_KEY') + self.client_agent_id = os.getenv('CLIENT_AGENT_ID') + self.a2a_service_api_key = os.getenv('A2A_SERVICE_API_KEY') + self.a2a_service_id = os.getenv('A2A_SERVICE_ID') + + # Observability Configuration + self.otlp_endpoint = os.getenv('OTLP_ENDPOINT', 'http://localhost:4318') + + # Audio Configuration + self.audio_available = AUDIO_AVAILABLE + + # Session Configuration + self.session_dir = os.getenv('SESSION_DIR', 'sessions') + self.max_turns = int(os.getenv('MAX_TURNS', '50')) + self.max_errors = int(os.getenv('MAX_ERRORS', '3')) \ No newline at end of file diff --git a/agentic-healthcare-booking-app/voice_agent/agntcy/main.py b/agentic-healthcare-booking-app/voice_agent/agntcy/main.py index b7b0b60..0415047 100644 --- a/agentic-healthcare-booking-app/voice_agent/agntcy/main.py +++ b/agentic-healthcare-booking-app/voice_agent/agntcy/main.py @@ -1,68 +1,199 @@ +""" +Healthcare Voice Agent - Main Entry Point with TBAC Patching +""" import asyncio import os import sys -from pathlib import Path -from agent.healthcare_agent import HealthcareAgent -from services.audio_service import AUDIO_AVAILABLE -from dotenv import load_dotenv - -app_dir = Path(__file__).resolve().parent.parent.parent -common_dir = app_dir/ 'common' -sys.path.insert(0, str(common_dir)) -from agntcy.observe.observe_config import initialize_observability - -load_dotenv() - -def get_missing_env_vars(required_vars): - """ - Generator function to yield missing environment variables from a given list. - """ - return (var for var in required_vars if not os.getenv(var)) - -def run_agent(): - print("=" * 50) +from config.settings import Settings +from common.identity.tbac import TBAC +from common.observe.observe_config import initialize_observability + + +# Global TBAC instance +tbac = TBAC() + + +def display_startup_banner(): + """Display startup information""" + print("=" * 60) print("HEALTHCARE VOICE + A2A + MCP AGENT") - print("=" * 50) + print("=" * 60) - service_name = "Healthcare_Voice_Agent" - initialize_observability(service_name) - - jwt_required = ['JWT_TOKEN', 'ENDPOINT_URL', 'PROJECT_ID', 'CONNECTION_ID'] - insurance_required = ['MCP_URL', 'X_INF_API_KEY'] - a2a_required = ['A2A_SERVICE_URL', 'A2A_MESSAGE_URL', 'A2A_API_KEY'] + +def check_configuration(): + """Validate required environment variables""" + settings = Settings() missing = [] - missing.extend(get_missing_env_vars(jwt_required)) - missing.extend(get_missing_env_vars(insurance_required)) - missing.extend(get_missing_env_vars(a2a_required)) + + # Check JWT/LLM config + if not all([settings.jwt_token, settings.endpoint_url, + settings.project_id, settings.connection_id]): + missing.extend(['JWT_TOKEN', 'ENDPOINT_URL', 'PROJECT_ID', 'CONNECTION_ID']) + + # Check Insurance/MCP config + if not all([settings.mcp_url, settings.insurance_api_key]): + missing.extend(['MCP_URL', 'X_INF_API_KEY']) + + # Check A2A config + if not all([settings.a2a_service_url, settings.a2a_message_url, settings.a2a_api_key]): + missing.extend(['A2A_SERVICE_URL', 'A2A_MESSAGE_URL', 'A2A_API_KEY']) if missing: - print(f"ERROR: Missing config: {missing}") + print(f"❌ ERROR: Missing configuration: {', '.join(set(missing))}") + return False + + print("✓ Configuration validated") + return True + + +def check_tbac_status(): + """Initialize and check TBAC authorization""" + print("\n" + "=" * 60) + print("TBAC (Task-Based Access Control) Status") + print("=" * 60) + + # Display TBAC configuration + if not all([tbac.client_api_key, tbac.client_id, tbac.a2a_api_key, tbac.a2a_id]): + print("⚠️ TBAC: DISABLED (missing credentials)") + print(" Agent will run without token-based authorization") return - print("Configuration validated") + print("✓ TBAC: ENABLED") + print(f" Client Agent ID: {tbac.client_id}") + print(f" A2A Service ID: {tbac.a2a_id}") - if AUDIO_AVAILABLE: - print("Audio system available - Triage conversation integrated") + # Perform bidirectional authorization + print("\nPerforming bidirectional authorization...") + + if tbac.authorize_bidirectional(): + print("✓ TBAC: FULLY AUTHORIZED") + print(f" ✓ Voice Agent → A2A Service: AUTHORIZED") + print(f" ✓ A2A Service → Voice Agent: AUTHORIZED") else: - print("Console mode only") + print("❌ TBAC: AUTHORIZATION FAILED") + if not tbac.is_client_authorized(): + print(" ✗ Voice Agent → A2A Service: UNAUTHORIZED") + if not tbac.is_a2a_authorized(): + print(" ✗ A2A Service → Voice Agent: UNAUTHORIZED") + + print("\n⚠️ WARNING: Agent will be blocked from A2A communication") + + +def patch_with_tbac(): + """Patch agent components with TBAC authorization checks""" + print("\n" + "=" * 60) + print("TBAC Patching") + print("=" * 60) - async def start(): - try: - agent = HealthcareAgent() - await agent.start() - except KeyboardInterrupt: - print("\nAgent stopped by user") - except Exception as e: - print(f"Agent error: {e}") + # Skip patching if TBAC not configured + if not all([tbac.client_api_key, tbac.client_id, tbac.a2a_api_key, tbac.a2a_id]): + print("⚠️ TBAC patching skipped (not configured)") + return try: - asyncio.run(start()) + # Import modules to patch + from clients import a2a_client + from agent import healthcare_agent + + # Patch A2AClient.send_message + if hasattr(a2a_client, 'A2AClient'): + original_send = a2a_client.A2AClient.send_message + + async def patched_send(self, message_parts, task_id=None, context_id=None): + """Patched send_message with TBAC authorization check""" + if not tbac.is_voice_authorized(): + print("❌ TBAC: Voice agent NOT AUTHORIZED to send message to A2A service") + return None + + print("✓ TBAC: Voice agent authorized - allowing A2A message") + return await original_send(self, message_parts, task_id, context_id) + + a2a_client.A2AClient.send_message = patched_send + print("✓ Patched: A2AClient.send_message with authorization check") + + # Patch HealthcareAgent._start_integrated_triage + if hasattr(healthcare_agent, 'HealthcareAgent'): + original_triage = healthcare_agent.HealthcareAgent._start_integrated_triage + + async def patched_triage(self): + """Patched triage start with TBAC authorization check""" + if not tbac.is_voice_authorized(): + print("❌ TBAC: Medical triage BLOCKED - Voice agent not authorized") + await self.audio.speak("I apologize, but medical triage is not available at this time.") + self.session.add_interaction("assistant", "Medical triage unavailable due to authorization.") + return + + print("✓ TBAC: Triage authorized - proceeding") + return await original_triage(self) + + healthcare_agent.HealthcareAgent._start_integrated_triage = patched_triage + print("✓ Patched: HealthcareAgent._start_integrated_triage with authorization check") + + print("\n✓ TBAC patching complete - authorization checks active") + + except ImportError as e: + print(f"❌ TBAC patching failed: {e}") + print(" Agent modules not found - ensure correct import paths") + sys.exit(1) + except Exception as e: + print(f"❌ TBAC patching error: {e}") + sys.exit(1) + + +def display_system_info(settings): + """Display system configuration""" + print("\n" + "=" * 60) + print("System Configuration") + print("=" * 60) + print(f"A2A Service URL: {settings.a2a_service_url}") + print(f"A2A Message URL: {settings.a2a_message_url}") + print(f"MCP URL: {settings.mcp_url}") + print(f"Audio System: {'ENABLED' if settings.audio_available else 'DISABLED (console mode)'}") + print("=" * 60 + "\n") + + +async def run_agent(): + """Run the healthcare agent""" + try: + from agent.healthcare_agent import HealthcareAgent + agent = HealthcareAgent() + await agent.start() except KeyboardInterrupt: - print("\nShutting down...") + print("\n\n✓ Agent stopped by user") + except Exception as e: + print(f"\n❌ Agent error: {e}") + import traceback + traceback.print_exc() + def main(): - run_agent() + """Main entry point""" + display_startup_banner() + + # Initialize observability + initialize_observability("Healthcare_Voice_Agent") + + # Check configuration + if not check_configuration(): + return + + # Check and display TBAC status + check_tbac_status() + + # Patch components with TBAC authorization + patch_with_tbac() + + # Display system info + settings = Settings() + display_system_info(settings) + + # Run the agent + try: + asyncio.run(run_agent()) + except KeyboardInterrupt: + print("\n✓ Shutting down gracefully...") + if __name__ == "__main__": main() \ No newline at end of file diff --git a/agentic-healthcare-booking-app/voice_agent/agntcy/session/__init__.py b/agentic-healthcare-booking-app/voice_agent/agntcy/session/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/agentic-healthcare-booking-app/voice_agent/agntcy/models/session.py b/agentic-healthcare-booking-app/voice_agent/agntcy/session/session.py similarity index 76% rename from agentic-healthcare-booking-app/voice_agent/agntcy/models/session.py rename to agentic-healthcare-booking-app/voice_agent/agntcy/session/session.py index 0a983b9..8ec8db4 100644 --- a/agentic-healthcare-booking-app/voice_agent/agntcy/models/session.py +++ b/agentic-healthcare-booking-app/voice_agent/agntcy/session/session.py @@ -1,11 +1,18 @@ +""" +Session Management +""" +import json import os import uuid -import json from datetime import datetime + class Session: - def __init__(self): + """Manages session state and conversation history""" + + def __init__(self, session_dir='sessions'): self.id = str(uuid.uuid4())[:8] + self.session_dir = session_dir self.data = {} self.triage_complete = False self.triage_attempts = 0 @@ -15,8 +22,11 @@ def __init__(self): self.triage_context_id = None self.triage_results = {} self.in_triage_mode = False + + print(f"SESSION: Created session {self.id}") def add_interaction(self, role, message, extra_data=None): + """Log a conversation interaction""" interaction = { "timestamp": datetime.now().isoformat(), "role": role, @@ -25,13 +35,15 @@ def add_interaction(self, role, message, extra_data=None): } if extra_data: interaction["extra_data"] = extra_data + self.conversation_log.append(interaction) print(f"SESSION-LOG: {role.upper()} - {message[:100]}...") def save_to_file(self): + """Save session data to file""" try: - os.makedirs("sessions", exist_ok=True) - filename = f"sessions/session_{datetime.now().strftime('%Y%m%d_%H%M%S')}_{self.id}.json" + os.makedirs(self.session_dir, exist_ok=True) + filename = f"{self.session_dir}/session_{datetime.now().strftime('%Y%m%d_%H%M%S')}_{self.id}.json" session_data = { "session_id": self.id, @@ -41,6 +53,7 @@ def save_to_file(self): "final_data": self.data, "triage_complete": self.triage_complete, "triage_attempts": self.triage_attempts, + "triage_results": self.triage_results, "conversation_log": self.conversation_log, "data_fields_collected": list(self.data.keys()), "total_interactions": len(self.conversation_log) @@ -53,5 +66,4 @@ def save_to_file(self): return filename except Exception as e: print(f"SESSION: Save failed: {e}") - return None - \ No newline at end of file + return None \ No newline at end of file diff --git a/agentic-healthcare-booking-app/voice_agent/no_agntcy/__init__.py b/agentic-healthcare-booking-app/voice_agent/no_agntcy/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/agentic-healthcare-booking-app/voice_agent/no_agntcy/agent/__init__.py b/agentic-healthcare-booking-app/voice_agent/no_agntcy/agent/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/agentic-healthcare-booking-app/voice_agent/no_agntcy/agent/healthcare_agent.py b/agentic-healthcare-booking-app/voice_agent/no_agntcy/agent/healthcare_agent.py new file mode 100644 index 0000000..75ff90f --- /dev/null +++ b/agentic-healthcare-booking-app/voice_agent/no_agntcy/agent/healthcare_agent.py @@ -0,0 +1,375 @@ +""" +Main healthcare agent orchestration +""" +import random +import string +from typing import Optional + +from audio.audio import AudioSystem +from clients.a2a_client import A2AClient +from clients.llm_client import LLMClient +from clients.insurance_client import InsuranceClient +from config.settings import Settings +from session.session import Session +from a2a.types import TaskState + + +class HealthcareAgent: + """Main healthcare appointment agent with voice, triage, and insurance""" + + def __init__(self, settings: Settings): + self.session = Session() + self.audio = AudioSystem() + + # Initialize clients + self.llm = LLMClient(settings.llm) + self.insurance = InsuranceClient(settings.insurance) + + # Initialize A2A client + self.a2a_client: Optional[A2AClient] = None + try: + self.a2a_client = A2AClient(settings.a2a) + except Exception as e: + print(f"A2A client initialization failed: {e}") + + async def start(self): + """Start the healthcare agent conversation""" + print(f"Healthcare Agent starting - Session {self.session.id}") + + # Discover A2A agent if available + if self.a2a_client: + await self.a2a_client.discover_agent() + + # Initial greeting + initial_message = ( + "Hello! I'm your healthcare appointment assistant. " + "Let's start by getting your basic information. " + "What's your full name?" + ) + await self.audio.speak(initial_message) + self.session.add_interaction("assistant", initial_message) + + # Main conversation loop + turn = 0 + errors = 0 + max_turns = 50 + max_errors = 3 + + while turn < max_turns and errors < max_errors: + turn += 1 + print(f"--- Turn {turn} ---") + + # Listen for user input + user_input = await self.audio.listen(timeout=5) + + # Handle audio errors + if user_input in ["UNCLEAR", "TIMEOUT", "ERROR"]: + errors += 1 + if user_input == "TIMEOUT": + await self.audio.speak( + "I'm still here. What would you like me to help you with?" + ) + else: + await self.audio.speak( + "I didn't catch that clearly. Could you please repeat?" + ) + continue + + if not user_input: + continue + + errors = 0 + print(f"USER: {user_input}") + self.session.add_interaction("user", user_input) + + # Check for exit commands + if any(phrase in user_input.lower() for phrase in ['bye', 'goodbye', 'end', 'quit']): + await self.audio.speak("Thank you for calling. Have a great day!") + self.session.add_interaction("assistant", "Thank you for calling. Have a great day!") + break + + # Handle triage or regular conversation + if self.session.in_triage_mode: + await self._handle_triage_conversation(user_input) + else: + await self._handle_regular_conversation(user_input) + + # End of conversation + print(f"Conversation ended. Final data: {self.session.data}") + + saved_file = self.session.save_to_file() + if saved_file: + print(f"Session saved to: {saved_file}") + + async def _handle_regular_conversation(self, user_input: str): + """Handle regular appointment scheduling conversation""" + # Process through LLM + result = await self.llm.process(user_input, self.session) + + # Extract and update session data + if result.get("extract"): + self.session.update_multiple(result["extract"]) + + # Check if triage is needed + if (result.get("need_triage") and + not self.session.triage_complete and + self.session.triage_attempts < 1 and + self.a2a_client): + + print("TRIAGE: Starting integrated triage conversation") + await self._start_integrated_triage() + return + + # Call insurance discovery if needed + if result.get("call_discovery"): + await self._handle_insurance_discovery() + + # Call eligibility check if needed + if result.get("call_eligibility"): + await self._handle_eligibility_check() + + # Speak response + response = result.get("response", "") + if response: + await self.audio.speak(response) + self.session.add_interaction("assistant", response) + + # Check if done + if result.get("done"): + confirmation = ''.join(random.choices(string.ascii_uppercase + string.digits, k=6)) + final_message = ( + f"Excellent! Your appointment is confirmed. " + f"Confirmation number: {confirmation}. " + f"You'll receive an email confirmation shortly. " + f"Thank you for calling!" + ) + await self.audio.speak(final_message) + self.session.add_interaction("assistant", final_message) + + async def _handle_insurance_discovery(self): + """Call insurance discovery API""" + required = ['name', 'date_of_birth', 'state'] + if not self.session.has_required_fields(required): + return + + print("INSURANCE-DISCOVERY: Calling API...") + discovery = await self.insurance.discovery( + self.session.data['name'], + self.session.data['date_of_birth'], + self.session.data['state'] + ) + + if discovery["success"]: + self.session.update_data('payer', discovery['payer']) + self.session.update_data('member_id', discovery['member_id']) + + message = ( + f"Great! I found your insurance: {discovery['payer']}, " + f"Policy ID: {discovery['member_id']}." + ) + await self.audio.speak(message) + self.session.add_interaction("assistant", message) + else: + fallback_msg = "I had some trouble finding your insurance, but we can proceed." + await self.audio.speak(fallback_msg) + self.session.add_interaction("assistant", fallback_msg) + + async def _handle_eligibility_check(self): + """Call insurance eligibility API""" + required = ['name', 'date_of_birth', 'member_id', 'payer', 'provider_name'] + if not self.session.has_required_fields(required): + return + + print("INSURANCE-ELIGIBILITY: Calling API...") + eligibility = await self.insurance.eligibility( + self.session.data['name'], + self.session.data['date_of_birth'], + self.session.data['member_id'], + self.session.data['payer'], + self.session.data['provider_name'] + ) + + if eligibility["success"] and eligibility.get('copay'): + message = ( + f"Perfect! Your insurance is verified. " + f"Payer: {self.session.data['payer']}, " + f"Policy ID: {self.session.data['member_id']}, " + f"Your copay will be ${eligibility['copay']}." + ) + await self.audio.speak(message) + self.session.add_interaction("assistant", message) + else: + fallback_message = ( + f"Your insurance {self.session.data['payer']} " + f"with Policy ID {self.session.data['member_id']} is on file. " + f"We can proceed with scheduling." + ) + await self.audio.speak(fallback_message) + self.session.add_interaction("assistant", fallback_message) + + async def _start_integrated_triage(self): + """Start integrated triage conversation via A2A""" + self.session.triage_attempts += 1 + self.session.in_triage_mode = True + + print("TRIAGE: Starting integrated triage conversation") + + triage_intro = ( + "I need to do a quick medical assessment to better assist you. " + "Let me ask you a few health-related questions." + ) + + try: + await self.audio.speak(triage_intro) + self.session.add_interaction("assistant", triage_intro) + + # Use default demographics for triage + age = 33 + sex = "female" + complaint = self.session.data.get('reason', 'general health concern') + + message_parts = [{ + "kind": "text", + "text": f"I am {age} years old, {sex}. {complaint}" + }] + + result = await self.a2a_client.send_message(message_parts) + + if not result: + print("TRIAGE: Failed to start - falling back to normal flow") + await self._end_triage_mode( + "I'll help you schedule your appointment without the assessment." + ) + return + + if result.get('kind') == 'task': + self.session.triage_task_id = result['id'] + self.session.triage_context_id = result['contextId'] + + print(f"TRIAGE: Started task {self.session.triage_task_id}") + + # Speak first triage question + if result['status'].get('message'): + triage_question = self._extract_text_from_message( + result['status']['message'] + ) + if triage_question: + await self.audio.speak(triage_question) + self.session.add_interaction("assistant", triage_question) + + except Exception as e: + print(f"TRIAGE: Error starting: {e}") + await self._end_triage_mode("Let me help you schedule your appointment.") + + async def _handle_triage_conversation(self, user_input: str): + """Handle ongoing triage conversation""" + print(f"TRIAGE: User response: {user_input}") + + try: + message_parts = [{"kind": "text", "text": user_input}] + result = await self.a2a_client.send_message( + message_parts, + task_id=self.session.triage_task_id, + context_id=self.session.triage_context_id + ) + + if not result: + print("TRIAGE: Failed to continue - ending triage") + await self._end_triage_mode( + "Let me help you continue with scheduling your appointment." + ) + return + + task_state = result['status']['state'] + print(f"TRIAGE: A2A task state: {task_state}") + + if task_state == TaskState.completed: + print("TRIAGE: Assessment COMPLETED - exiting A2A mode") + + # Extract triage results + if result.get('artifacts'): + artifact = result['artifacts'][0] + triage_data = self._extract_triage_results(artifact) + if triage_data: + self.session.triage_results.update(triage_data) + print(f"TRIAGE: Results extracted: {triage_data}") + + # Build completion message + urgency = self.session.triage_results.get('urgency_level', 'standard') + doctor_type = self.session.triage_results.get('doctor_type', 'general practitioner') + + completion_message = ( + f"Thank you for the assessment. " + f"Based on your responses, I recommend seeing a {doctor_type}. " + f"Priority level: {urgency}. " + f"Now let's get you scheduled. " + f"I'll need your date of birth for insurance verification." + ) + + await self._end_triage_mode() + await self.audio.speak(completion_message) + self.session.add_interaction("assistant", completion_message) + + elif task_state == TaskState.input_required: + # Ask next question + if result['status'].get('message'): + next_question = self._extract_text_from_message( + result['status']['message'] + ) + if next_question: + await self.audio.speak(next_question) + self.session.add_interaction("assistant", next_question) + else: + print("TRIAGE: No message in input-required state - ending triage") + await self._end_triage_mode( + "Let me help you continue with scheduling your appointment." + ) + + elif task_state in [TaskState.failed, TaskState.canceled]: + print(f"TRIAGE: Task ended with state: {task_state}") + await self._end_triage_mode( + "Let me help you continue with scheduling your appointment." + ) + + except Exception as e: + print(f"TRIAGE: Error in conversation: {e}") + await self._end_triage_mode( + "Let me help you continue with scheduling your appointment." + ) + + async def _end_triage_mode(self, message: Optional[str] = None): + """End triage mode and return to normal flow""" + print("TRIAGE: Ending triage mode - cleaning up A2A connection") + + self.session.in_triage_mode = False + self.session.triage_complete = True + self.session.triage_task_id = None + self.session.triage_context_id = None + + print("TRIAGE: Mode ended - returning to normal appointment flow") + + if message: + await self.audio.speak(message) + self.session.add_interaction("assistant", message) + + def _extract_text_from_message(self, message: dict) -> Optional[str]: + """Extract text content from A2A message""" + if not message or not message.get('parts'): + return None + + for part in message['parts']: + if part.get('kind') == 'text': + return part.get('text', '') + + return None + + def _extract_triage_results(self, artifact: dict) -> dict: + """Extract triage results from artifact""" + if not artifact or not artifact.get('parts'): + return {} + + for part in artifact['parts']: + if part.get('kind') == 'data' and part.get('data'): + return part['data'] + + return {} \ No newline at end of file diff --git a/agentic-healthcare-booking-app/voice_agent/no_agntcy/audio/__init__.py b/agentic-healthcare-booking-app/voice_agent/no_agntcy/audio/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/agentic-healthcare-booking-app/voice_agent/no_agntcy/audio/audio.py b/agentic-healthcare-booking-app/voice_agent/no_agntcy/audio/audio.py new file mode 100644 index 0000000..3f54150 --- /dev/null +++ b/agentic-healthcare-booking-app/voice_agent/no_agntcy/audio/audio.py @@ -0,0 +1,150 @@ +""" +Audio system for speech recognition and text-to-speech +""" +import asyncio +import os +import tempfile + +# Audio imports with fallback +try: + import speech_recognition as sr + import pygame + from gtts import gTTS + AUDIO_AVAILABLE = True +except ImportError: + AUDIO_AVAILABLE = False + + +class AudioSystem: + """Handles speech recognition and text-to-speech functionality""" + + def __init__(self): + self.enabled = AUDIO_AVAILABLE + self.tts_enabled = False + self.speech_enabled = False + + if self.enabled: + self._initialize_audio() + + def _initialize_audio(self): + """Initialize audio components""" + try: + print("Initializing audio...") + + # Initialize speech recognition + try: + self.recognizer = sr.Recognizer() + self.microphone = sr.Microphone() + with self.microphone as source: + self.recognizer.adjust_for_ambient_noise(source, duration=1) + + self.recognizer.energy_threshold = 300 + self.recognizer.dynamic_energy_threshold = True + self.recognizer.pause_threshold = 0.8 + self.speech_enabled = True + print("Speech recognition ready") + except Exception as e: + print(f"Speech recognition failed: {e}") + self.speech_enabled = False + + # Initialize text-to-speech + try: + pygame.mixer.pre_init(frequency=22050, size=-16, channels=2, buffer=1024) + pygame.mixer.init() + self.tts_enabled = True + print("TTS system ready") + except Exception as e: + print(f"TTS init failed: {e}") + self.tts_enabled = False + + except Exception as e: + print(f"Audio init failed: {e}") + self.enabled = False + + async def listen(self, timeout: int = 5) -> str: + """ + Listen for user speech input + + Args: + timeout: Maximum seconds to wait for input + + Returns: + Recognized text or status string (UNCLEAR, TIMEOUT, ERROR) + """ + if not self.speech_enabled: + return input("You: ").strip() + + print("Listening...") + + def _listen(): + try: + with self.microphone as source: + audio = self.recognizer.listen(source, timeout=timeout, phrase_time_limit=6) + result = self.recognizer.recognize_google(audio, language='en-US') + print(f"Recognized: '{result}'") + return result.strip() + except sr.UnknownValueError: + return "UNCLEAR" + except sr.WaitTimeoutError: + return "TIMEOUT" + except Exception: + return "ERROR" + + loop = asyncio.get_event_loop() + return await loop.run_in_executor(None, _listen) + + async def speak(self, text: str): + """ + Speak text using text-to-speech + + Args: + text: Text to speak + """ + print(f"Agent: {text}") + + if not self.tts_enabled: + print("TTS: Not enabled, skipping audio") + return + + def _speak(): + try: + tts = gTTS(text=text, lang='en', slow=False) + + with tempfile.NamedTemporaryFile(suffix='.mp3', delete=False) as tmp: + temp_file = tmp.name + + try: + tts.save(temp_file) + pygame.mixer.music.load(temp_file) + pygame.mixer.music.play() + + max_wait = 30 + wait_count = 0 + while pygame.mixer.music.get_busy() and wait_count < max_wait * 20: + pygame.time.wait(50) + wait_count += 1 + + if pygame.mixer.music.get_busy(): + pygame.mixer.music.stop() + + finally: + try: + os.unlink(temp_file) + except Exception: + pass + + return True + + except Exception as e: + print(f"TTS error: {e}") + return False + + if self.tts_enabled: + try: + loop = asyncio.get_event_loop() + await asyncio.wait_for( + loop.run_in_executor(None, _speak), + timeout=35 + ) + except Exception as e: + print(f"TTS: Error: {e}") \ No newline at end of file diff --git a/agentic-healthcare-booking-app/voice_agent/no_agntcy/clients/__init__.py b/agentic-healthcare-booking-app/voice_agent/no_agntcy/clients/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/agentic-healthcare-booking-app/voice_agent/no_agntcy/clients/a2a_client.py b/agentic-healthcare-booking-app/voice_agent/no_agntcy/clients/a2a_client.py new file mode 100644 index 0000000..a4a7952 --- /dev/null +++ b/agentic-healthcare-booking-app/voice_agent/no_agntcy/clients/a2a_client.py @@ -0,0 +1,208 @@ +""" +A2A client for hosted A2A service communication +""" +import asyncio +import time +import uuid +from typing import Dict, List, Any, Optional + +import requests + +from config.settings import A2AConfig + + +class A2AClient: + """Client for A2A protocol communication with hosted service""" + + def __init__(self, config: A2AConfig): + self.config = config + self.agent_id = f"client_{uuid.uuid4().hex[:8]}" + self.agent_card: Optional[Dict] = None + + print(f"A2A-CLIENT: Initialized as {self.agent_id}") + print(f"A2A-CLIENT: Discovery URL: {config.service_url}") + print(f"A2A-CLIENT: Message URL: {config.message_url}") + print(f"A2A-CLIENT: API Key: {'Set' if config.api_key else 'Not set'}") + + def _timed_request( + self, + method: str, + url: str, + description: str, + **kwargs + ) -> tuple: + """Execute HTTP request with timing""" + start_time = time.time() + timestamp = time.strftime("%H:%M:%S", time.localtime(start_time)) + print(f"A2A-CLIENT: [{timestamp}] >>> {method} {description}") + print(f"A2A-CLIENT: URL: {url}") + + try: + if method == 'GET': + response = requests.get(url, **kwargs) + else: + response = requests.post(url, **kwargs) + + elapsed = time.time() - start_time + end_timestamp = time.strftime("%H:%M:%S", time.localtime()) + elapsed_ms = elapsed * 1000 + print(f"A2A-CLIENT: [{end_timestamp}] <<< {response.status_code} | {elapsed:.3f}s ({elapsed_ms:.0f}ms)") + + if response.status_code != 200: + print(f"A2A-CLIENT: Error response: {response.text[:200]}") + else: + print(f"A2A-CLIENT: Success - response length: {len(response.text)} chars") + + return response, elapsed + except Exception as e: + elapsed = time.time() - start_time + end_timestamp = time.strftime("%H:%M:%S", time.localtime()) + elapsed_ms = elapsed * 1000 + print(f"A2A-CLIENT: [{end_timestamp}] <<< ERROR: {e} | {elapsed:.3f}s ({elapsed_ms:.0f}ms)") + return None, elapsed + + async def discover_agent(self) -> bool: + """ + Discover agent capabilities via agent card + + Returns: + True if discovery successful, False otherwise + """ + try: + def _request(): + return self._timed_request( + 'GET', + f"{self.config.service_url}/.well-known/agent-card.json", + "Agent Discovery", + timeout=30 + ) + + loop = asyncio.get_event_loop() + response, elapsed = await loop.run_in_executor(None, _request) + + if response and response.status_code == 200: + self.agent_card = response.json() + print(f"A2A-CLIENT: Discovered agent: {self.agent_card['name']}") + return True + else: + if response: + print(f"A2A-CLIENT: Discovery failed: {response.text[:200]}") + return False + except Exception as e: + print(f"A2A-CLIENT: Discovery error: {e}") + return False + + async def send_message( + self, + message_parts: List[Dict[str, Any]], + task_id: Optional[str] = None, + context_id: Optional[str] = None + ) -> Optional[Dict[str, Any]]: + """ + Send message to A2A service + + Args: + message_parts: List of message parts (text, data, etc.) + task_id: Optional task ID for continuing conversation + context_id: Optional context ID + + Returns: + Task result dictionary or None on failure + """ + message = { + "role": "user", + "parts": message_parts, + "messageId": str(uuid.uuid4()), + "kind": "message" + } + + if task_id: + message["taskId"] = task_id + if context_id: + message["contextId"] = context_id + + payload = { + "jsonrpc": "2.0", + "id": str(uuid.uuid4()), + "method": "message/send", + "params": { + "message": message, + "configuration": { + "acceptedOutputModes": ["text/plain", "application/json"], + "blocking": True + } + } + } + + # Log the message being sent + message_text = self._extract_text_from_parts(message_parts) + print(f"A2A-CLIENT: Sending message: '{message_text[:100]}...'") + + try: + def _request(): + headers = {"Content-Type": "application/json"} + if self.config.api_key: + headers['X-Shared-Key'] = self.config.api_key + + description = f"Send Message" + if task_id: + description += f" (Task: {task_id})" + + return self._timed_request( + 'POST', + self.config.message_url, + description, + json=payload, + headers=headers, + timeout=60 + ) + + loop = asyncio.get_event_loop() + response, elapsed = await loop.run_in_executor(None, _request) + + if response and response.status_code == 200: + data = response.json() + if 'result' in data: + result = data['result'] + state = result['status']['state'] + task_id = result.get('id', task_id) + + print(f"A2A-CLIENT: Task {task_id} state: {state}") + + # Log agent response if present + if result['status'].get('message'): + agent_response = self._extract_text_from_message( + result['status']['message'] + ) + if agent_response: + print(f"A2A-CLIENT: Agent response: '{agent_response[:100]}...'") + + # Log artifacts if present + if result.get('artifacts'): + print(f"A2A-CLIENT: Task completed with {len(result['artifacts'])} artifact(s)") + + return result + elif 'error' in data: + print(f"A2A-CLIENT: Server error: {data['error']}") + return None + else: + if response: + print(f"A2A-CLIENT: HTTP error {response.status_code}: {response.text[:200]}") + return None + + except Exception as e: + print(f"A2A-CLIENT: Request failed: {e}") + return None + + def _extract_text_from_parts(self, parts: List[Dict]) -> str: + """Extract text content from message parts""" + for part in parts: + if part.get('kind') == 'text': + return part.get('text', '') + return "" + + def _extract_text_from_message(self, message: Dict) -> str: + """Extract text content from message object""" + if not message or not message.get('parts'): + return "" + return self._extract_text_from_parts(message['parts']) \ No newline at end of file diff --git a/agentic-healthcare-booking-app/voice_agent/no_agntcy/clients/insurance_client.py b/agentic-healthcare-booking-app/voice_agent/no_agntcy/clients/insurance_client.py new file mode 100644 index 0000000..b78cf92 --- /dev/null +++ b/agentic-healthcare-booking-app/voice_agent/no_agntcy/clients/insurance_client.py @@ -0,0 +1,165 @@ +""" +Insurance MCP client for discovery and eligibility checks +""" +import asyncio +from datetime import datetime +from typing import Dict, Any + +import requests + +from config.settings import InsuranceConfig +from utils.helpers import ( + split_name, + format_date_of_birth, + format_state, + clean_provider_name, + extract_payer, + extract_member_id, + extract_copay +) + + +class InsuranceClient: + """Client for insurance MCP service interactions""" + + def __init__(self, config: InsuranceConfig): + self.config = config + self.headers = { + "Content-Type": "application/json", + "X-INF-API-KEY": config.api_key + } + print("INSURANCE: Client initialized") + + async def discovery(self, name: str, dob: str, state: str) -> Dict[str, Any]: + """ + Perform insurance discovery + + Args: + name: Patient full name + dob: Date of birth + state: US state + + Returns: + Dictionary with success status, payer, and member_id + """ + print(f"INSURANCE: Discovery - {name}, {dob}, {state}") + + first, last = split_name(name) + formatted_dob = format_date_of_birth(dob) + formatted_state = format_state(state) + + payload = { + "jsonrpc": "2.0", + "id": f"discovery_{datetime.now().strftime('%Y%m%d_%H%M%S')}", + "method": "tools/call", + "params": { + "name": "insurance_discovery", + "arguments": { + "patientDateOfBirth": formatted_dob, + "patientFirstName": first, + "patientLastName": last, + "patientState": formatted_state + } + } + } + + def _request(): + return requests.post( + self.config.mcp_url, + headers=self.headers, + json=payload, + timeout=45 + ) + + loop = asyncio.get_event_loop() + response = await loop.run_in_executor(None, _request) + + if response.status_code == 200: + data = response.json() + + if "result" in data: + result_text = str(data["result"]) + + payer = extract_payer(result_text) + member_id = extract_member_id(result_text) + + return { + "success": True, + "payer": payer, + "member_id": member_id + } + + return {"success": False} + + async def eligibility( + self, + name: str, + dob: str, + subscriber_id: str, + payer_name: str, + provider_name: str + ) -> Dict[str, Any]: + """ + Check insurance eligibility + + Args: + name: Patient full name + dob: Date of birth + subscriber_id: Insurance member ID + payer_name: Insurance payer name + provider_name: Healthcare provider name + + Returns: + Dictionary with success status and copay + """ + print(f"INSURANCE: Eligibility check") + + first, last = split_name(name) + formatted_dob = format_date_of_birth(dob) + + provider_clean = clean_provider_name(provider_name) + provider_first, provider_last = split_name(provider_clean) + + payload = { + "jsonrpc": "2.0", + "id": f"eligibility_{datetime.now().strftime('%Y%m%d_%H%M%S')}", + "method": "tools/call", + "params": { + "name": "benefits_eligibility", + "arguments": { + "patientFirstName": first, + "patientLastName": last, + "patientDateOfBirth": formatted_dob, + "subscriberId": subscriber_id, + "payerName": payer_name, + "providerFirstName": provider_first, + "providerLastName": provider_last, + "providerNpi": "1234567890" + } + } + } + + def _request(): + return requests.post( + self.config.mcp_url, + headers=self.headers, + json=payload, + timeout=45 + ) + + loop = asyncio.get_event_loop() + response = await loop.run_in_executor(None, _request) + + if response.status_code == 200: + data = response.json() + + if "result" in data: + result_text = str(data["result"]) + copay = extract_copay(result_text) + + return { + "success": True, + "copay": copay + } + + return {"success": False} \ No newline at end of file diff --git a/agentic-healthcare-booking-app/voice_agent/no_agntcy/clients/llm_client.py b/agentic-healthcare-booking-app/voice_agent/no_agntcy/clients/llm_client.py new file mode 100644 index 0000000..8b0150a --- /dev/null +++ b/agentic-healthcare-booking-app/voice_agent/no_agntcy/clients/llm_client.py @@ -0,0 +1,147 @@ +""" +LLM client for conversation processing +""" +import asyncio +import json +from typing import Dict, Any + +import requests + +from config.settings import LLMConfig + + +class LLMClient: + """Client for LLM API interactions""" + + def __init__(self, config: LLMConfig): + self.config = config + self.headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {config.jwt_token}' + } + print("LLM: Initialized with JWT endpoint") + + def _build_prompt(self, user_input: str, session) -> str: + """Build appropriate prompt based on session state""" + if session.in_triage_mode: + return self._build_triage_prompt(user_input, session) + else: + return self._build_appointment_prompt(user_input, session) + + def _build_triage_prompt(self, user_input: str, session) -> str: + """Build prompt for triage mode""" + return f"""You are in TRIAGE MODE. The user is answering medical assessment questions. + +Current triage task: {session.triage_task_id} +User response to triage question: "{user_input}" + +Respond with: +{{ + "response": "I understand your answer. Let me continue the medical assessment.", + "extract": {{}}, + "need_triage": false, + "call_discovery": false, + "call_eligibility": false, + "done": false, + "continue_triage": true +}}""" + + def _build_appointment_prompt(self, user_input: str, session) -> str: + """Build prompt for appointment scheduling""" + return f"""You are a healthcare appointment scheduler with this specific flow: + +1. Ask name, phone +2. Ask reason for visit +3. If medical symptoms → start triage (use default demographics) +4. After triage → collect DOB (for insurance), state → call discovery → announce insurance found +5. Collect provider → call eligibility → announce payer, policy ID, copay +6. Schedule appointment → confirmation code → end + +Current session data: {json.dumps(session.data)} +Triage complete: {session.triage_complete} +Triage results: {json.dumps(session.triage_results)} +User input: "{user_input}" + +EXTRACTION RULES: +- Extract name as "name" +- Extract phone as "phone" +- Extract medical reason as "reason" +- Extract date of birth as "date_of_birth" (MM/DD/YYYY format) +- Extract US state as "state" +- Extract provider name as "provider_name" +- Extract appointment date as "preferred_date" + +JSON response: +{{ + "response": "what to say to user", + "extract": {{"field": "value"}}, + "need_triage": true/false, + "call_discovery": true/false, + "call_eligibility": true/false, + "done": true/false +}}""" + + async def process(self, user_input: str, session) -> Dict[str, Any]: + """ + Process user input through LLM + + Args: + user_input: User's message + session: Current session object + + Returns: + Parsed LLM response as dictionary + """ + print(f"LLM: Processing: '{user_input[:50]}...'") + + prompt = self._build_prompt(user_input, session) + + payload = { + "messages": [ + {"role": "system", "content": prompt}, + {"role": "user", "content": user_input} + ], + "project_id": self.config.project_id, + "connection_id": self.config.connection_id, + "max_tokens": 400, + "temperature": 0.2 + } + + def _request(): + return requests.post( + self.config.endpoint_url, + headers=self.headers, + json=payload, + timeout=30 + ) + + loop = asyncio.get_event_loop() + response = await loop.run_in_executor(None, _request) + + if response.status_code == 200: + data = response.json() + if 'choices' in data and data['choices']: + content = data['choices'][0]['message']['content'] + + try: + # Clean JSON formatting + if content.startswith('```json'): + content = content[7:] + if content.endswith('```'): + content = content[:-3] + + result = json.loads(content.strip()) + print("LLM: Response parsed") + return result + except Exception as e: + print(f"LLM: Parse error: {e}") + + # Fallback response + return { + "response": "I understand. Please continue.", + "extract": {}, + "need_triage": False, + "call_discovery": False, + "call_eligibility": False, + "done": False + } \ No newline at end of file diff --git a/agentic-healthcare-booking-app/voice_agent/no_agntcy/config/__init__.py b/agentic-healthcare-booking-app/voice_agent/no_agntcy/config/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/agentic-healthcare-booking-app/voice_agent/no_agntcy/config/settings.py b/agentic-healthcare-booking-app/voice_agent/no_agntcy/config/settings.py new file mode 100644 index 0000000..948dd68 --- /dev/null +++ b/agentic-healthcare-booking-app/voice_agent/no_agntcy/config/settings.py @@ -0,0 +1,121 @@ +""" +Configuration and environment settings management +""" +import os +from dataclasses import dataclass +from typing import Optional + + +def load_env(): + """Load environment variables from .env file if available""" + try: + from dotenv import load_dotenv + load_dotenv() + except ImportError: + pass + + +@dataclass +class LLMConfig: + """LLM service configuration""" + jwt_token: str + endpoint_url: str + project_id: str + connection_id: str + + @classmethod + def from_env(cls): + return cls( + jwt_token=os.getenv('JWT_TOKEN', ''), + endpoint_url=os.getenv('ENDPOINT_URL', ''), + project_id=os.getenv('PROJECT_ID', ''), + connection_id=os.getenv('CONNECTION_ID', '') + ) + + def validate(self): + """Validate all required fields are present""" + missing = [] + if not self.jwt_token: + missing.append('JWT_TOKEN') + if not self.endpoint_url: + missing.append('ENDPOINT_URL') + if not self.project_id: + missing.append('PROJECT_ID') + if not self.connection_id: + missing.append('CONNECTION_ID') + return missing + + +@dataclass +class InsuranceConfig: + """Insurance MCP service configuration""" + mcp_url: str + api_key: str + + @classmethod + def from_env(cls): + return cls( + mcp_url=os.getenv('MCP_URL', ''), + api_key=os.getenv('X_INF_API_KEY', '') + ) + + def validate(self): + """Validate all required fields are present""" + missing = [] + if not self.mcp_url: + missing.append('MCP_URL') + if not self.api_key: + missing.append('X_INF_API_KEY') + return missing + + +@dataclass +class A2AConfig: + """A2A service configuration""" + service_url: str + message_url: str + api_key: Optional[str] + + @classmethod + def from_env(cls): + base_url = os.getenv('A2A_SERVICE_URL', 'http://localhost:8887') + return cls( + service_url=base_url, + message_url=os.getenv('A2A_MESSAGE_URL', base_url), + api_key=os.getenv('A2A_API_KEY') + ) + + def validate(self): + """Validate all required fields are present""" + missing = [] + if not self.service_url: + missing.append('A2A_SERVICE_URL') + if not self.message_url: + missing.append('A2A_MESSAGE_URL') + return missing + + +class Settings: + """Application settings""" + + def __init__(self): + load_env() + self.llm = LLMConfig.from_env() + self.insurance = InsuranceConfig.from_env() + self.a2a = A2AConfig.from_env() + + def validate_all(self): + """Validate all configuration and return list of missing variables""" + missing = [] + missing.extend(self.llm.validate()) + missing.extend(self.insurance.validate()) + missing.extend(self.a2a.validate()) + return missing + + def print_summary(self): + """Print configuration summary""" + print("Configuration Summary:") + print(f" LLM Endpoint: {self.llm.endpoint_url}") + print(f" Insurance MCP: {self.insurance.mcp_url}") + print(f" A2A Service: {self.a2a.service_url}") + print(f" A2A Message: {self.a2a.message_url}") \ No newline at end of file diff --git a/agentic-healthcare-booking-app/voice_agent/no_agntcy/main.py b/agentic-healthcare-booking-app/voice_agent/no_agntcy/main.py new file mode 100644 index 0000000..676c8f3 --- /dev/null +++ b/agentic-healthcare-booking-app/voice_agent/no_agntcy/main.py @@ -0,0 +1,95 @@ +""" +Healthcare Voice + A2A + MCP Agent +Main entry point +""" +import asyncio + +from agent.healthcare_agent import HealthcareAgent +from config.settings import Settings + +# Check audio availability +try: + import speech_recognition + import pygame + from gtts import gTTS + AUDIO_AVAILABLE = True +except ImportError: + AUDIO_AVAILABLE = False + + +def print_banner(): + """Print application banner""" + print("=" * 50) + print("HEALTHCARE VOICE + A2A + MCP AGENT") + print("=" * 50) + + +def validate_configuration(settings: Settings) -> bool: + """ + Validate all required configuration + + Args: + settings: Application settings + + Returns: + True if valid, False otherwise + """ + missing = settings.validate_all() + + if missing: + print(f"ERROR: Missing configuration variables:") + for var in missing: + print(f" - {var}") + return False + + return True + + +async def run_agent(): + """Main agent execution""" + print_banner() + + # Load and validate settings + settings = Settings() + + if not validate_configuration(settings): + print("\nPlease check your .env file and ensure all required variables are set.") + return + + print("Configuration validated") + #settings.print_summary() + + # Check audio availability + if AUDIO_AVAILABLE: + print("\nAudio system available - Voice interaction enabled") + else: + print("\nAudio libraries not available - Console mode only") + print("Install audio dependencies: pip install SpeechRecognition pyaudio pygame gtts") + + # Start agent + try: + print("\nStarting healthcare agent...") + agent = HealthcareAgent(settings) + await agent.start() + except KeyboardInterrupt: + print("\n\nAgent stopped by user") + except Exception as e: + print(f"\nAgent error: {e}") + import traceback + traceback.print_exc() + + +def main(): + """Application entry point""" + try: + asyncio.run(run_agent()) + except KeyboardInterrupt: + print("\n\nShutting down...") + except Exception as e: + print(f"\nFatal error: {e}") + import traceback + traceback.print_exc() + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/agentic-healthcare-booking-app/voice_agent/no_agntcy/session/__init__.py b/agentic-healthcare-booking-app/voice_agent/no_agntcy/session/__init__.py new file mode 100644 index 0000000..07a4749 --- /dev/null +++ b/agentic-healthcare-booking-app/voice_agent/no_agntcy/session/__init__.py @@ -0,0 +1,151 @@ +""" +Utility functions for data formatting and parsing +""" +import re +from typing import Tuple + + +def split_name(name: str) -> Tuple[str, str]: + """ + Split full name into first and last name + + Args: + name: Full name string + + Returns: + Tuple of (first_name, last_name) + """ + parts = name.strip().split() + if len(parts) == 1: + return parts[0], "" + elif len(parts) == 2: + return parts[0], parts[1] + else: + return parts[0], " ".join(parts[1:]) + + +def format_date_of_birth(dob: str) -> str: + """ + Format date of birth to YYYY-MM-DD format + + Args: + dob: Date string in various formats (MM/DD/YYYY or YYYY-MM-DD) + + Returns: + Formatted date string in YYYY-MM-DD format + """ + if not dob: + return "" + + # Handle MM/DD/YYYY format + if re.match(r'^\d{1,2}/\d{1,2}/\d{4}$', dob): + month, day, year = dob.split('/') + return f"{year}-{month.zfill(2)}-{day.zfill(2)}" + + # Already in YYYY-MM-DD format + if re.match(r'^\d{4}-\d{1,2}-\d{1,2}$', dob): + return dob + + # Return as-is if format unknown + return dob + + +def format_state(state: str) -> str: + """ + Format state name to title case + + Args: + state: State name string + + Returns: + Title-cased state name + """ + return state.strip().title() if state else "" + + +def clean_provider_name(provider_name: str) -> str: + """ + Remove titles from provider name (Dr., MD, DO, etc.) + + Args: + provider_name: Full provider name with potential titles + + Returns: + Cleaned provider name + """ + return re.sub(r'\b(Dr\.?|MD|DO)\b', '', provider_name, flags=re.IGNORECASE).strip() + + +def extract_pattern(text: str, patterns: list) -> str: + """ + Extract text matching any of the provided regex patterns + + Args: + text: Text to search + patterns: List of regex patterns to try + + Returns: + Matched text or empty string + """ + for pattern in patterns: + match = re.search(pattern, text.lower()) + if match: + return match.group(1).strip() + return "" + + +def extract_copay(text: str) -> str: + """ + Extract copay amount from text + + Args: + text: Text containing copay information + + Returns: + Copay amount as string + """ + patterns = [ + r'co-?pay[:\s]*\$?([0-9,]+)', + r'copayment[:\s]*\$?([0-9,]+)', + r'patient\s+responsibility[:\s]*\$?([0-9,]+)' + ] + return extract_pattern(text, patterns) + + +def extract_payer(text: str) -> str: + """ + Extract payer/insurance name from text + + Args: + text: Text containing payer information + + Returns: + Payer name in title case + """ + patterns = [ + r'payer[:\s]*([^\n,;]+)', + r'insurance[:\s]*([^\n,;]+)', + r'plan[:\s]*([^\n,;]+)' + ] + result = extract_pattern(text, patterns) + return result.title() if result else "" + + +def extract_member_id(text: str) -> str: + """ + Extract member/policy ID from text + + Args: + text: Text containing member ID + + Returns: + Member ID in uppercase + """ + patterns = [ + r'member\s*id[:\s]*([a-za-z0-9\-]+)', + r'subscriber\s*id[:\s]*([a-za-z0-9\-]+)', + r'policy\s*id[:\s]*([a-za-z0-9\-]+)', + r'policy[:\s]*([a-za-z0-9\-]+)' + ] + result = extract_pattern(text, patterns) + return result.upper() if result else "" \ No newline at end of file diff --git a/agentic-healthcare-booking-app/voice_agent/no_agntcy/session/session.py b/agentic-healthcare-booking-app/voice_agent/no_agntcy/session/session.py new file mode 100644 index 0000000..4bc282a --- /dev/null +++ b/agentic-healthcare-booking-app/voice_agent/no_agntcy/session/session.py @@ -0,0 +1,92 @@ +""" +Session management and data persistence +""" +import json +import os +import uuid +from datetime import datetime +from typing import Dict, List, Any, Optional + + +class Session: + """Manages conversation session data and persistence""" + + def __init__(self): + self.id = str(uuid.uuid4())[:8] + self.data: Dict[str, Any] = {} + self.triage_complete = False + self.triage_attempts = 0 + self.conversation_log: List[Dict] = [] + self.start_time = datetime.now() + self.triage_task_id: Optional[str] = None + self.triage_context_id: Optional[str] = None + self.triage_results: Dict[str, Any] = {} + self.in_triage_mode = False + + def add_interaction(self, role: str, message: str, extra_data: Optional[Dict] = None): + """Log a conversation interaction""" + interaction = { + "timestamp": datetime.now().isoformat(), + "role": role, + "message": message, + "session_data_snapshot": self.data.copy() + } + if extra_data: + interaction["extra_data"] = extra_data + + self.conversation_log.append(interaction) + print(f"SESSION-LOG: {role.upper()} - {message[:100]}...") + + def update_data(self, key: str, value: Any): + """Update session data with logging""" + self.data[key] = value + print(f"SESSION-UPDATE: Set {key} = {value}") + + def update_multiple(self, data: Dict[str, Any]): + """Update multiple session data fields""" + for key, value in data.items(): + if value: + self.update_data(key, value) + + def has_required_fields(self, fields: List[str]) -> bool: + """Check if all required fields are present""" + return all(k in self.data and self.data[k] for k in fields) + + def save_to_file(self) -> Optional[str]: + """Save session data to JSON file""" + try: + os.makedirs("sessions", exist_ok=True) + filename = f"sessions/session_{datetime.now().strftime('%Y%m%d_%H%M%S')}_{self.id}.json" + + session_data = { + "session_id": self.id, + "start_time": self.start_time.isoformat(), + "end_time": datetime.now().isoformat(), + "duration_minutes": (datetime.now() - self.start_time).total_seconds() / 60, + "final_data": self.data, + "triage_complete": self.triage_complete, + "triage_attempts": self.triage_attempts, + "triage_results": self.triage_results, + "conversation_log": self.conversation_log, + "data_fields_collected": list(self.data.keys()), + "total_interactions": len(self.conversation_log) + } + + with open(filename, 'w') as f: + json.dump(session_data, f, indent=2, default=str) + + print(f"SESSION: Saved complete session to {filename}") + return filename + except Exception as e: + print(f"SESSION: Save failed: {e}") + return None + + def get_summary(self) -> Dict[str, Any]: + """Get session summary""" + return { + "session_id": self.id, + "duration": (datetime.now() - self.start_time).total_seconds(), + "interactions": len(self.conversation_log), + "fields_collected": len(self.data), + "triage_complete": self.triage_complete + } \ No newline at end of file diff --git a/agentic-healthcare-booking-app/voice_agent/no_agntcy/utils/__init__.py b/agentic-healthcare-booking-app/voice_agent/no_agntcy/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/agentic-healthcare-booking-app/voice_agent/no_agntcy/utils/helpers.py b/agentic-healthcare-booking-app/voice_agent/no_agntcy/utils/helpers.py new file mode 100644 index 0000000..07a4749 --- /dev/null +++ b/agentic-healthcare-booking-app/voice_agent/no_agntcy/utils/helpers.py @@ -0,0 +1,151 @@ +""" +Utility functions for data formatting and parsing +""" +import re +from typing import Tuple + + +def split_name(name: str) -> Tuple[str, str]: + """ + Split full name into first and last name + + Args: + name: Full name string + + Returns: + Tuple of (first_name, last_name) + """ + parts = name.strip().split() + if len(parts) == 1: + return parts[0], "" + elif len(parts) == 2: + return parts[0], parts[1] + else: + return parts[0], " ".join(parts[1:]) + + +def format_date_of_birth(dob: str) -> str: + """ + Format date of birth to YYYY-MM-DD format + + Args: + dob: Date string in various formats (MM/DD/YYYY or YYYY-MM-DD) + + Returns: + Formatted date string in YYYY-MM-DD format + """ + if not dob: + return "" + + # Handle MM/DD/YYYY format + if re.match(r'^\d{1,2}/\d{1,2}/\d{4}$', dob): + month, day, year = dob.split('/') + return f"{year}-{month.zfill(2)}-{day.zfill(2)}" + + # Already in YYYY-MM-DD format + if re.match(r'^\d{4}-\d{1,2}-\d{1,2}$', dob): + return dob + + # Return as-is if format unknown + return dob + + +def format_state(state: str) -> str: + """ + Format state name to title case + + Args: + state: State name string + + Returns: + Title-cased state name + """ + return state.strip().title() if state else "" + + +def clean_provider_name(provider_name: str) -> str: + """ + Remove titles from provider name (Dr., MD, DO, etc.) + + Args: + provider_name: Full provider name with potential titles + + Returns: + Cleaned provider name + """ + return re.sub(r'\b(Dr\.?|MD|DO)\b', '', provider_name, flags=re.IGNORECASE).strip() + + +def extract_pattern(text: str, patterns: list) -> str: + """ + Extract text matching any of the provided regex patterns + + Args: + text: Text to search + patterns: List of regex patterns to try + + Returns: + Matched text or empty string + """ + for pattern in patterns: + match = re.search(pattern, text.lower()) + if match: + return match.group(1).strip() + return "" + + +def extract_copay(text: str) -> str: + """ + Extract copay amount from text + + Args: + text: Text containing copay information + + Returns: + Copay amount as string + """ + patterns = [ + r'co-?pay[:\s]*\$?([0-9,]+)', + r'copayment[:\s]*\$?([0-9,]+)', + r'patient\s+responsibility[:\s]*\$?([0-9,]+)' + ] + return extract_pattern(text, patterns) + + +def extract_payer(text: str) -> str: + """ + Extract payer/insurance name from text + + Args: + text: Text containing payer information + + Returns: + Payer name in title case + """ + patterns = [ + r'payer[:\s]*([^\n,;]+)', + r'insurance[:\s]*([^\n,;]+)', + r'plan[:\s]*([^\n,;]+)' + ] + result = extract_pattern(text, patterns) + return result.title() if result else "" + + +def extract_member_id(text: str) -> str: + """ + Extract member/policy ID from text + + Args: + text: Text containing member ID + + Returns: + Member ID in uppercase + """ + patterns = [ + r'member\s*id[:\s]*([a-za-z0-9\-]+)', + r'subscriber\s*id[:\s]*([a-za-z0-9\-]+)', + r'policy\s*id[:\s]*([a-za-z0-9\-]+)', + r'policy[:\s]*([a-za-z0-9\-]+)' + ] + result = extract_pattern(text, patterns) + return result.upper() if result else "" \ No newline at end of file diff --git a/agentic-healthcare-booking-app/voice_agent/va_a2a_mcp.py b/agentic-healthcare-booking-app/voice_agent/va_a2a_mcp.py deleted file mode 100644 index 9b71f38..0000000 --- a/agentic-healthcare-booking-app/voice_agent/va_a2a_mcp.py +++ /dev/null @@ -1,928 +0,0 @@ -""" -Healthcare Voice + A2A + MCP Agent (Hosted A2A Service) -""" -import asyncio -import json -import os -import re -import uuid -import random -import string -import tempfile -import time -from datetime import datetime -from http import HTTPStatus -from typing import Dict, Optional, List, Any -from enum import Enum - -import requests - -from a2a.types import TaskState - -# if need default chat mode - set this flag to False. -AUDIO_AVAILABLE = True - -# Audio imports with fallback -try: - import speech_recognition as sr - import pygame - from gtts import gTTS -except ImportError: - AUDIO_AVAILABLE = False - -# Load environment -def load_env(): - try: - from dotenv import load_dotenv - load_dotenv() - except: - pass - -load_env() - -# Session Management -class Session: - def __init__(self): - self.id = str(uuid.uuid4())[:8] - self.data = {} - self.triage_complete = False - self.triage_attempts = 0 - self.conversation_log = [] - self.start_time = datetime.now() - self.triage_task_id = None - self.triage_context_id = None - self.triage_results = {} - self.in_triage_mode = False - - def add_interaction(self, role, message, extra_data=None): - interaction = { - "timestamp": datetime.now().isoformat(), - "role": role, - "message": message, - "session_data_snapshot": self.data.copy() - } - if extra_data: - interaction["extra_data"] = extra_data - self.conversation_log.append(interaction) - print(f"SESSION-LOG: {role.upper()} - {message[:100]}...") - - def save_to_file(self): - try: - os.makedirs("sessions", exist_ok=True) - filename = f"sessions/session_{datetime.now().strftime('%Y%m%d_%H%M%S')}_{self.id}.json" - - session_data = { - "session_id": self.id, - "start_time": self.start_time.isoformat(), - "end_time": datetime.now().isoformat(), - "duration_minutes": (datetime.now() - self.start_time).total_seconds() / 60, - "final_data": self.data, - "triage_complete": self.triage_complete, - "triage_attempts": self.triage_attempts, - "conversation_log": self.conversation_log, - "data_fields_collected": list(self.data.keys()), - "total_interactions": len(self.conversation_log) - } - - with open(filename, 'w') as f: - json.dump(session_data, f, indent=2, default=str) - - print(f"SESSION: Saved complete session to {filename}") - return filename - except Exception as e: - print(f"SESSION: Save failed: {e}") - return None - -# Audio System -class AudioSystem: - def __init__(self): - self.enabled = AUDIO_AVAILABLE - self.tts_enabled = False - self.speech_enabled = False - - if self.enabled: - try: - print("Initializing audio...") - - try: - self.recognizer = sr.Recognizer() - self.microphone = sr.Microphone() - with self.microphone as source: - self.recognizer.adjust_for_ambient_noise(source, duration=1) - - self.recognizer.energy_threshold = 300 - self.recognizer.dynamic_energy_threshold = True - self.recognizer.pause_threshold = 0.8 - self.speech_enabled = True - print("Speech recognition ready") - except Exception as e: - print(f"Speech recognition failed: {e}") - self.speech_enabled = False - - try: - pygame.mixer.pre_init(frequency=22050, size=-16, channels=2, buffer=1024) - pygame.mixer.init() - self.tts_enabled = True - print("TTS system ready") - except Exception as e: - print(f"TTS init failed: {e}") - self.tts_enabled = False - - except Exception as e: - print(f"Audio init failed: {e}") - self.enabled = False - - async def listen(self, timeout=5): - if not self.speech_enabled: - return input("You: ").strip() - - print("Listening...") - - def _listen(): - try: - with self.microphone as source: - audio = self.recognizer.listen(source, timeout=timeout, phrase_time_limit=6) - result = self.recognizer.recognize_google(audio, language='en-US') - print(f"Recognized: '{result}'") - return result.strip() - except sr.UnknownValueError: - return "UNCLEAR" - except sr.WaitTimeoutError: - return "TIMEOUT" - except Exception: - return "ERROR" - - loop = asyncio.get_event_loop() - return await loop.run_in_executor(None, _listen) - - async def speak(self, text): - print(f"Agent: {text}") - - if not self.tts_enabled: - print("TTS: Not enabled, skipping audio") - return - - def _speak(): - try: - tts = gTTS(text=text, lang='en', slow=False) - - with tempfile.NamedTemporaryFile(suffix='.mp3', delete=False) as tmp: - temp_file = tmp.name - - try: - tts.save(temp_file) - pygame.mixer.music.load(temp_file) - pygame.mixer.music.play() - - max_wait = 30 - wait_count = 0 - while pygame.mixer.music.get_busy() and wait_count < max_wait * 20: - pygame.time.wait(50) - wait_count += 1 - - if pygame.mixer.music.get_busy(): - pygame.mixer.music.stop() - - finally: - try: - os.unlink(temp_file) - except Exception: - pass - - return True - - except Exception as e: - print(f"TTS error: {e}") - return False - - if self.tts_enabled: - try: - loop = asyncio.get_event_loop() - await asyncio.wait_for( - loop.run_in_executor(None, _speak), - timeout=35 - ) - except Exception as e: - print(f"TTS: Error: {e}") - -# A2A Client for Hosted Service -class A2AClient: - def __init__(self): - self.base_url = os.getenv('A2A_SERVICE_URL', 'http://localhost:8887') - self.message_url = os.getenv('A2A_MESSAGE_URL', self.base_url) - self.api_key = os.getenv('A2A_API_KEY') - self.agent_id = f"client_{uuid.uuid4().hex[:8]}" - self.agent_card = None - - print(f"A2A-CLIENT: Initialized as {self.agent_id}") - print(f"A2A-CLIENT: Discovery URL: {self.base_url}") - print(f"A2A-CLIENT: Message URL: {self.message_url}") - print(f"A2A-CLIENT: API Key: {'Set' if self.api_key else 'Not set'}") - - def _timed_request(self, method, url, description, **kwargs): - start_time = time.time() - timestamp = time.strftime("%H:%M:%S", time.localtime(start_time)) - print(f"A2A-CLIENT: [{timestamp}] >>> {method} {description}") - print(f"A2A-CLIENT: URL: {url}") - - try: - if method == 'GET': - response = requests.get(url, **kwargs) - else: - response = requests.post(url, **kwargs) - - elapsed = time.time() - start_time - end_timestamp = time.strftime("%H:%M:%S", time.localtime()) - elapsed_ms = elapsed * 1000 - print(f"A2A-CLIENT: [{end_timestamp}] <<< {response.status_code} | {elapsed:.3f}s ({elapsed_ms:.0f}ms)") - - if response.status_code != HTTPStatus.OK: - print(f"A2A-CLIENT: Error response: {response.text[:200]}") - else: - print(f"A2A-CLIENT: Success - response length: {len(response.text)} chars") - - return response, elapsed - except Exception as e: - elapsed = time.time() - start_time - end_timestamp = time.strftime("%H:%M:%S", time.localtime()) - elapsed_ms = elapsed * 1000 - print(f"A2A-CLIENT: [{end_timestamp}] <<< ERROR: {e} | {elapsed:.3f}s ({elapsed_ms:.0f}ms)") - return None, elapsed - - async def discover_agent(self): - try: - def _request(): - return self._timed_request('GET', f"{self.base_url}/.well-known/agent-card.json", - "Agent Discovery", timeout=30) - - loop = asyncio.get_event_loop() - response, elapsed = await loop.run_in_executor(None, _request) - - if response and response.status_code == HTTPStatus.OK: - self.agent_card = response.json() - print(f"A2A-CLIENT: Discovered agent: {self.agent_card['name']}") - return True - else: - if response: - print(f"A2A-CLIENT: Discovery failed: {response.text[:200]}") - return False - except Exception as e: - print(f"A2A-CLIENT: Discovery error: {e}") - return False - - async def send_message(self, message_parts, task_id=None, context_id=None): - message = { - "role": "user", - "parts": message_parts, - "messageId": str(uuid.uuid4()), - "kind": "message" - } - - if task_id: - message["taskId"] = task_id - if context_id: - message["contextId"] = context_id - - payload = { - "jsonrpc": "2.0", - "id": str(uuid.uuid4()), - "method": "message/send", - "params": { - "message": message, - "configuration": { - "acceptedOutputModes": ["text/plain", "application/json"], - "blocking": True - } - } - } - - # Log the message being sent - message_text = "" - for part in message_parts: - if part.get('kind') == 'text': - message_text = part.get('text', '') - break - print(f"A2A-CLIENT: Sending message: '{message_text[:100]}...'") - - try: - def _request(): - headers = {"Content-Type": "application/json"} - if self.api_key: - headers['X-Shared-Key'] = self.api_key - - description = f"Send Message" - if task_id: - description += f" (Task: {task_id})" - - return self._timed_request('POST', self.message_url, description, - json=payload, headers=headers, timeout=60) - - loop = asyncio.get_event_loop() - response, elapsed = await loop.run_in_executor(None, _request) - - if response and response.status_code == HTTPStatus.OK: - data = response.json() - if 'result' in data: - result = data['result'] - state = result['status']['state'] - task_id = result.get('id', task_id) - - print(f"A2A-CLIENT: Task {task_id} state: {state}") - - # Log agent response if present - if result['status'].get('message'): - agent_response = "" - for part in result['status']['message'].get('parts', []): - if part.get('kind') == 'text': - agent_response = part.get('text', '') - break - if agent_response: - print(f"A2A-CLIENT: Agent response: '{agent_response[:100]}...'") - - # Log artifacts if present (final results) - if result.get('artifacts'): - print(f"A2A-CLIENT: Task completed with {len(result['artifacts'])} artifact(s)") - - return result - elif 'error' in data: - print(f"A2A-CLIENT: Server error: {data['error']}") - return None - else: - if response: - print(f"A2A-CLIENT: HTTP error {response.status_code}: {response.text[:200]}") - return None - - except Exception as e: - print(f"A2A-CLIENT: Request failed: {e}") - return None - -# LLM Client -class LLMClient: - def __init__(self, jwt_token, endpoint_url, project_id, connection_id): - self.headers = { - 'Content-Type': 'application/json', - 'Authorization': f'Bearer {jwt_token}' - } - self.endpoint_url = endpoint_url - self.project_id = project_id - self.connection_id = connection_id - print("LLM: Initialized with JWT endpoint") - - async def process(self, user_input, session): - print(f"LLM: Processing: '{user_input[:50]}...'") - - if session.in_triage_mode: - prompt = f"""You are in TRIAGE MODE. The user is answering medical assessment questions. - -Current triage task: {session.triage_task_id} -User response to triage question: "{user_input}" - -Respond with: -{{ - "response": "I understand your answer. Let me continue the medical assessment.", - "extract": {{}}, - "need_triage": false, - "call_discovery": false, - "call_eligibility": false, - "done": false, - "continue_triage": true -}}""" - else: - prompt = f"""You are a healthcare appointment scheduler with this specific flow: - -1. Ask name, phone -2. Ask reason for visit -3. If medical symptoms → start triage (use default demographics) -4. After triage → collect DOB (for insurance), state → call discovery → announce insurance found -5. Collect provider → call eligibility → announce payer, policy ID, copay -6. Schedule appointment → confirmation code → end - -Current session data: {json.dumps(session.data)} -Triage complete: {session.triage_complete} -Triage results: {json.dumps(session.triage_results)} -User input: "{user_input}" - -EXTRACTION RULES: -- Extract name as "name" -- Extract phone as "phone" -- Extract medical reason as "reason" -- Extract date of birth as "date_of_birth" (MM/DD/YYYY format) -- Extract US state as "state" -- Extract provider name as "provider_name" -- Extract appointment date as "preferred_date" - -JSON response: -{{ - "response": "what to say to user", - "extract": {{"field": "value"}}, - "need_triage": true/false, - "call_discovery": true/false, - "call_eligibility": true/false, - "done": true/false -}}""" - - payload = { - "messages": [ - {"role": "system", "content": prompt}, - {"role": "user", "content": user_input} - ], - "project_id": self.project_id, - "connection_id": self.connection_id, - "max_tokens": 400, - "temperature": 0.2 - } - - def _request(): - return requests.post(self.endpoint_url, headers=self.headers, json=payload, timeout=30) - - loop = asyncio.get_event_loop() - response = await loop.run_in_executor(None, _request) - - if response.status_code == HTTPStatus.OK: - data = response.json() - if 'choices' in data and data['choices']: - content = data['choices'][0]['message']['content'] - - try: - if content.startswith('```json'): - content = content[7:] - if content.endswith('```'): - content = content[:-3] - - result = json.loads(content.strip()) - print("LLM: Response parsed") - return result - except: - pass - - return { - "response": "I understand. Please continue.", - "extract": {}, - "need_triage": False, - "call_discovery": False, - "call_eligibility": False, - "done": False - } - -# Insurance Client -class InsuranceClient: - def __init__(self, mcp_url, api_key): - self.mcp_url = mcp_url - self.headers = {"Content-Type": "application/json", "X-INF-API-KEY": api_key} - print("INSURANCE: Client initialized") - - def _split_name(self, name): - parts = name.strip().split() - if len(parts) == 1: - return parts[0], "" - elif len(parts) == 2: - return parts[0], parts[1] - else: - return parts[0], " ".join(parts[1:]) - - def _format_dob(self, dob): - if not dob: - return "" - - if re.match(r'^\d{1,2}/\d{1,2}/\d{4}$', dob): - month, day, year = dob.split('/') - formatted = f"{year}-{month.zfill(2)}-{day.zfill(2)}" - return formatted - - if re.match(r'^\d{4}-\d{1,2}-\d{1,2}$', dob): - return dob - - return dob - - async def discovery(self, name, dob, state): - print(f"INSURANCE: Discovery - {name}, {dob}, {state}") - first, last = self._split_name(name) - formatted_dob = self._format_dob(dob) - formatted_state = state.strip().title() if state else "" - - payload = { - "jsonrpc": "2.0", - "id": f"discovery_{datetime.now().strftime('%Y%m%d_%H%M%S')}", - "method": "tools/call", - "params": { - "name": "insurance_discovery", - "arguments": { - "patientDateOfBirth": formatted_dob, - "patientFirstName": first, - "patientLastName": last, - "patientState": formatted_state - } - } - } - - def _request(): - return requests.post(self.mcp_url, headers=self.headers, json=payload, timeout=45) - - loop = asyncio.get_event_loop() - response = await loop.run_in_executor(None, _request) - - if response.status_code == HTTPStatus.OK: - data = response.json() - - if "result" in data: - result_text = str(data["result"]) - - payer = "" - member_id = "" - - for pattern in [r'payer[:\s]*([^\n,;]+)', r'insurance[:\s]*([^\n,;]+)', r'plan[:\s]*([^\n,;]+)']: - match = re.search(pattern, result_text.lower()) - if match: - payer = match.group(1).strip().title() - break - - for pattern in [r'member\s*id[:\s]*([a-za-z0-9\-]+)', r'subscriber\s*id[:\s]*([a-za-z0-9\-]+)', r'policy\s*id[:\s]*([a-za-z0-9\-]+)', r'policy[:\s]*([a-za-z0-9\-]+)']: - match = re.search(pattern, result_text.lower()) - if match: - member_id = match.group(1).strip().upper() - break - - return {"success": True, "payer": payer, "member_id": member_id} - - return {"success": False} - - async def eligibility(self, name, dob, subscriber_id, payer_name, provider_name): - print(f"INSURANCE: Eligibility check") - first, last = self._split_name(name) - formatted_dob = self._format_dob(dob) - - provider_clean = re.sub(r'\b(Dr\.?|MD|DO)\b', '', provider_name, flags=re.IGNORECASE).strip() - provider_first, provider_last = self._split_name(provider_clean) - - payload = { - "jsonrpc": "2.0", - "id": f"eligibility_{datetime.now().strftime('%Y%m%d_%H%M%S')}", - "method": "tools/call", - "params": { - "name": "benefits_eligibility", - "arguments": { - "patientFirstName": first, - "patientLastName": last, - "patientDateOfBirth": formatted_dob, - "subscriberId": subscriber_id, - "payerName": payer_name, - "providerFirstName": provider_first, - "providerLastName": provider_last, - "providerNpi": "1234567890" - } - } - } - - def _request(): - return requests.post(self.mcp_url, headers=self.headers, json=payload, timeout=45) - - loop = asyncio.get_event_loop() - response = await loop.run_in_executor(None, _request) - - if response.status_code == HTTPStatus.OK: - data = response.json() - - if "result" in data: - result_text = str(data["result"]) - - copay = "" - copay_patterns = [r'co-?pay[:\s]*\$?([0-9,]+)', r'copayment[:\s]*\$?([0-9,]+)', r'patient\s+responsibility[:\s]*\$?([0-9,]+)'] - - for pattern in copay_patterns: - copay_match = re.search(pattern, result_text.lower()) - if copay_match: - copay = copay_match.group(1) - break - - return {"success": True, "copay": copay} - - return {"success": False} - -# Healthcare Agent -class HealthcareAgent: - def __init__(self): - self.session = Session() - self.audio = AudioSystem() - - # Initialize LLM client - jwt_token = os.getenv('JWT_TOKEN') - endpoint_url = os.getenv('ENDPOINT_URL') - project_id = os.getenv('PROJECT_ID') - connection_id = os.getenv('CONNECTION_ID') - - if not all([jwt_token, endpoint_url, project_id, connection_id]): - raise Exception("Missing JWT config") - - self.llm = LLMClient(jwt_token, endpoint_url, project_id, connection_id) - - # Initialize insurance client - mcp_url = os.getenv('MCP_URL') - insurance_key = os.getenv('X_INF_API_KEY') - if not mcp_url or not insurance_key: - raise Exception("Missing insurance config") - - self.insurance = InsuranceClient(mcp_url, insurance_key) - - # Initialize A2A client - self.a2a_client = None - try: - self.a2a_client = A2AClient() - except: - print("A2A client not available") - - async def start(self): - print(f"Healthcare Agent starting - Session {self.session.id}") - - if self.a2a_client: - await self.a2a_client.discover_agent() - - initial_message = "Hello! I'm your healthcare appointment assistant. Let's start by getting your basic information. What's your full name?" - await self.audio.speak(initial_message) - self.session.add_interaction("assistant", initial_message) - - turn = 0 - errors = 0 - - while turn < 50 and errors < 3: - turn += 1 - print(f"--- Turn {turn} ---") - - user_input = await self.audio.listen(timeout=5) - - if user_input in ["UNCLEAR", "TIMEOUT", "ERROR"]: - errors += 1 - if user_input == "TIMEOUT": - await self.audio.speak("I'm still here. What would you like me to help you with?") - else: - await self.audio.speak("I didn't catch that clearly. Could you please repeat?") - continue - - if not user_input: - continue - - errors = 0 - print(f"USER: {user_input}") - self.session.add_interaction("user", user_input) - - if any(phrase in user_input.lower() for phrase in ['bye', 'goodbye', 'end', 'quit']): - await self.audio.speak("Thank you for calling. Have a great day!") - self.session.add_interaction("assistant", "Thank you for calling. Have a great day!") - break - - if self.session.in_triage_mode: - await self._handle_triage_conversation(user_input) - else: - result = await self.llm.process(user_input, self.session) - - if result.get("extract"): - for key, value in result["extract"].items(): - if value: - self.session.data[key] = value - print(f"SESSION-UPDATE: Set {key} = {value}") - - if (result.get("need_triage") and not self.session.triage_complete and - self.session.triage_attempts < 1 and self.a2a_client): - - print("TRIAGE: Starting integrated triage conversation") - await self._start_integrated_triage() - continue - - if result.get("call_discovery"): - required = ['name', 'date_of_birth', 'state'] - if all(k in self.session.data and self.session.data[k] for k in required): - print("INSURANCE-DISCOVERY: Calling API...") - discovery = await self.insurance.discovery( - self.session.data['name'], - self.session.data['date_of_birth'], - self.session.data['state'] - ) - if discovery["success"]: - self.session.data['payer'] = discovery['payer'] - self.session.data['member_id'] = discovery['member_id'] - - insurance_message = f"Great! I found your insurance: {discovery['payer']}, Policy ID: {discovery['member_id']}." - await self.audio.speak(insurance_message) - self.session.add_interaction("assistant", insurance_message) - else: - fallback_msg = "I had some trouble finding your insurance, but we can proceed." - await self.audio.speak(fallback_msg) - self.session.add_interaction("assistant", fallback_msg) - - if result.get("call_eligibility"): - required = ['name', 'date_of_birth', 'member_id', 'payer', 'provider_name'] - if all(k in self.session.data and self.session.data[k] for k in required): - print("INSURANCE-ELIGIBILITY: Calling API...") - eligibility = await self.insurance.eligibility( - self.session.data['name'], - self.session.data['date_of_birth'], - self.session.data['member_id'], - self.session.data['payer'], - self.session.data['provider_name'] - ) - if eligibility["success"] and eligibility.get('copay'): - eligibility_message = f"Perfect! Your insurance is verified. Payer: {self.session.data['payer']}, Policy ID: {self.session.data['member_id']}, Your copay will be ${eligibility['copay']}." - await self.audio.speak(eligibility_message) - self.session.add_interaction("assistant", eligibility_message) - else: - fallback_message = f"Your insurance {self.session.data['payer']} with Policy ID {self.session.data['member_id']} is on file. We can proceed with scheduling." - await self.audio.speak(fallback_message) - self.session.add_interaction("assistant", fallback_message) - - response = result.get("response", "") - if response: - await self.audio.speak(response) - self.session.add_interaction("assistant", response) - - if result.get("done"): - confirmation = ''.join(random.choices(string.ascii_uppercase + string.digits, k=6)) - final_message = f"Excellent! Your appointment is confirmed. Confirmation number: {confirmation}. You'll receive an email confirmation shortly. Thank you for calling!" - await self.audio.speak(final_message) - self.session.add_interaction("assistant", final_message) - break - - print(f"Conversation ended. Final data: {self.session.data}") - - saved_file = self.session.save_to_file() - if saved_file: - print(f"Session saved to: {saved_file}") - - async def _start_integrated_triage(self): - self.session.triage_attempts += 1 - self.session.in_triage_mode = True - - print("TRIAGE: Starting integrated triage conversation") - - triage_intro = "I need to do a quick medical assessment to better assist you. Let me ask you a few health-related questions." - - try: - await self.audio.speak(triage_intro) - self.session.add_interaction("assistant", triage_intro) - - age = 33 - sex = "female" - complaint = self.session.data.get('reason', 'general health concern') - - message_parts = [{"kind": "text", "text": f"I am {age} years old, {sex}. {complaint}"}] - result = await self.a2a_client.send_message(message_parts) - - if not result: - print("TRIAGE: Failed to start - falling back to normal flow") - await self._end_triage_mode("I'll help you schedule your appointment without the assessment.") - return - - if result.get('kind') == 'task': - self.session.triage_task_id = result['id'] - self.session.triage_context_id = result['contextId'] - - print(f"TRIAGE: Started task {self.session.triage_task_id}") - - if result['status'].get('message'): - triage_question = self._extract_text_from_message(result['status']['message']) - if triage_question: - await self.audio.speak(triage_question) - self.session.add_interaction("assistant", triage_question) - - except Exception as e: - print(f"TRIAGE: Error starting: {e}") - await self._end_triage_mode("Let me help you schedule your appointment.") - - async def _handle_triage_conversation(self, user_input): - print(f"TRIAGE: User response: {user_input}") - - try: - message_parts = [{"kind": "text", "text": user_input}] - result = await self.a2a_client.send_message( - message_parts, - task_id=self.session.triage_task_id, - context_id=self.session.triage_context_id - ) - - if not result: - print("TRIAGE: Failed to continue - ending triage") - await self._end_triage_mode("Let me help you continue with scheduling your appointment.") - return - - task_state = result['status']['state'] - print(f"TRIAGE: A2A task state: {task_state}") - - if task_state == TaskState.completed: - print("TRIAGE: Assessment COMPLETED - exiting A2A mode") - - if result.get('artifacts'): - artifact = result['artifacts'][0] - triage_data = self._extract_triage_results(artifact) - if triage_data: - self.session.triage_results.update(triage_data) - print(f"TRIAGE: Results extracted: {triage_data}") - - urgency = self.session.triage_results.get('urgency_level', 'standard') - doctor_type = self.session.triage_results.get('doctor_type', 'general practitioner') - - completion_message = f"Thank you for the assessment. Based on your responses, I recommend seeing a {doctor_type}. Priority level: {urgency}. Now let's get you scheduled. I'll need your date of birth for insurance verification." - - await self._end_triage_mode() - - await self.audio.speak(completion_message) - self.session.add_interaction("assistant", completion_message) - - return - - elif task_state == TaskState.input_required: - if result['status'].get('message'): - next_question = self._extract_text_from_message(result['status']['message']) - if next_question: - await self.audio.speak(next_question) - self.session.add_interaction("assistant", next_question) - else: - print("TRIAGE: No message in input-required state - ending triage") - await self._end_triage_mode("Let me help you continue with scheduling your appointment.") - - elif task_state in [TaskState.failed, TaskState.canceled]: - print(f"TRIAGE: Task ended with state: {task_state}") - await self._end_triage_mode("Let me help you continue with scheduling your appointment.") - - except Exception as e: - print(f"TRIAGE: Error in conversation: {e}") - await self._end_triage_mode("Let me help you continue with scheduling your appointment.") - - async def _end_triage_mode(self, message=None): - print("TRIAGE: Ending triage mode - cleaning up A2A connection") - - self.session.in_triage_mode = False - self.session.triage_complete = True - - self.session.triage_task_id = None - self.session.triage_context_id = None - - print("TRIAGE: Mode ended - returning to normal appointment flow") - - if message: - await self.audio.speak(message) - self.session.add_interaction("assistant", message) - - def _extract_text_from_message(self, message): - if not message or not message.get('parts'): - return None - - for part in message['parts']: - if part.get('kind') == 'text': - return part.get('text', '') - - return None - - def _extract_triage_results(self, artifact): - if not artifact or not artifact.get('parts'): - return {} - - for part in artifact['parts']: - if part.get('kind') == 'data' and part.get('data'): - return part['data'] - - return {} - -def run_agent(): - print("=" * 50) - print("HEALTHCARE VOICE + A2A + MCP AGENT") - print("=" * 50) - - jwt_required = ['JWT_TOKEN', 'ENDPOINT_URL', 'PROJECT_ID', 'CONNECTION_ID'] - insurance_required = ['MCP_URL', 'X_INF_API_KEY'] - a2a_required = ['A2A_SERVICE_URL', 'A2A_MESSAGE_URL', 'A2A_API_KEY'] - - missing = [] - missing.extend([var for var in jwt_required if not os.getenv(var)]) - missing.extend([var for var in insurance_required if not os.getenv(var)]) - missing.extend([var for var in a2a_required if not os.getenv(var)]) - - if missing: - print(f"ERROR: Missing config: {missing}") - return - - print("Configuration validated") - print(f"A2A Service URL: {os.getenv('A2A_SERVICE_URL')}") - print(f"A2A Message URL: {os.getenv('A2A_MESSAGE_URL')}") - - if AUDIO_AVAILABLE: - print("Audio system available - Triage conversation integrated") - else: - print("Console mode only") - - async def start(): - try: - agent = HealthcareAgent() - await agent.start() - except KeyboardInterrupt: - print("\nAgent stopped by user") - except Exception as e: - print(f"Agent error: {e}") - - try: - asyncio.run(start()) - except KeyboardInterrupt: - print("\nShutting down...") - -def main(): - run_agent() - -if __name__ == "__main__": - main() \ No newline at end of file From ca6dfe1182e3c8422056c3aba55b6aec2f1c1366 Mon Sep 17 00:00:00 2001 From: ssyechuri Date: Fri, 14 Nov 2025 12:47:58 -0600 Subject: [PATCH 2/2] agntcy voice_agent refactored --- .../agntcy/agent/healthcare_agent.py | 331 +++++++++--------- .../voice_agent/agntcy/audio/audio.py | 87 ++--- .../voice_agent/agntcy/clients/a2a_client.py | 53 +-- .../agntcy/clients/insurance_client.py | 46 +-- .../voice_agent/agntcy/clients/llm_client.py | 113 +++--- .../voice_agent/agntcy/config/__init__.py | 0 .../voice_agent/agntcy/config/settings.py | 53 --- .../voice_agent/agntcy/main.py | 236 +++++-------- .../voice_agent/agntcy/session/session.py | 22 +- 9 files changed, 375 insertions(+), 566 deletions(-) delete mode 100644 agentic-healthcare-booking-app/voice_agent/agntcy/config/__init__.py delete mode 100644 agentic-healthcare-booking-app/voice_agent/agntcy/config/settings.py diff --git a/agentic-healthcare-booking-app/voice_agent/agntcy/agent/healthcare_agent.py b/agentic-healthcare-booking-app/voice_agent/agntcy/agent/healthcare_agent.py index 1db21b2..ce9a7c9 100644 --- a/agentic-healthcare-booking-app/voice_agent/agntcy/agent/healthcare_agent.py +++ b/agentic-healthcare-booking-app/voice_agent/agntcy/agent/healthcare_agent.py @@ -1,103 +1,84 @@ -""" -Healthcare Voice Agent -""" +# Healthcare Agent +import os import time import random import string from ioa_observe.sdk.decorators import agent, workflow -from ioa_observe.sdk.metrics.agents.availability import agent_availability -from config.settings import Settings -from clients.a2a_client import A2AClient + +from session.session import Session +from audio.audio import AudioSystem from clients.llm_client import LLMClient from clients.insurance_client import InsuranceClient -from audio.audio import AudioSystem -from session.session import Session +from clients.a2a_client import A2AClient from a2a.types import TaskState @agent(name="healthcare_agent", description="healthcare voice agent", version="1.0.0", protocol="A2A") class HealthcareAgent: - """Main healthcare appointment scheduling agent""" - def __init__(self): - settings = Settings() - - self.session = Session(session_dir=settings.session_dir) + self.session = Session() self.audio = AudioSystem() # Initialize LLM client - self.llm = LLMClient( - settings.jwt_token, - settings.endpoint_url, - settings.project_id, - settings.connection_id - ) + jwt_token = os.getenv('JWT_TOKEN') + endpoint_url = os.getenv('ENDPOINT_URL') + project_id = os.getenv('PROJECT_ID') + connection_id = os.getenv('CONNECTION_ID') + + if not all([jwt_token, endpoint_url, project_id, connection_id]): + raise Exception("Missing JWT config") + + self.llm = LLMClient(jwt_token, endpoint_url, project_id, connection_id) # Initialize insurance client - self.insurance = InsuranceClient(settings.mcp_url, settings.insurance_api_key) + mcp_url = os.getenv('MCP_URL') + insurance_key = os.getenv('X_INF_API_KEY') + if not mcp_url or not insurance_key: + raise Exception("Missing insurance config") + + self.insurance = InsuranceClient(mcp_url, insurance_key) # Initialize A2A client self.a2a_client = None try: - self.a2a_client = A2AClient( - settings.a2a_service_url, - settings.a2a_message_url, - settings.a2a_api_key, - settings.otlp_endpoint - ) - except Exception as e: - print(f"A2A client initialization failed: {e}") - - self.max_turns = settings.max_turns - self.max_errors = settings.max_errors + self.a2a_client = A2AClient() + except: + print("A2A client not available") async def start(self): - """Start the agent conversation""" - print(f"\n{'='*60}") - print(f"Healthcare Agent Starting - Session {self.session.id}") - print(f"{'='*60}\n") - - # Record agent heartbeat + print(f"Healthcare Agent starting - Session {self.session.id}") + from ioa_observe.sdk.metrics.agents.availability import agent_availability start = time.time() agent_availability.record_agent_heartbeat("healthcare_voice_agent") observe_latency = time.time() - start - print(f"✓ Observe heartbeat latency: {observe_latency:.2f}s\n") - - # Discover A2A agent if available + print(f"Observe heartbeat latency: {observe_latency:.2f} seconds") + print(f"Healthcare Agent starting - Session {self.session.id}") if self.a2a_client: await self.a2a_client.discover_agent() - # Initial greeting initial_message = "Hello! I'm your healthcare appointment assistant. Let's start by getting your basic information. What's your full name?" await self.audio.speak(initial_message) self.session.add_interaction("assistant", initial_message) - # Main conversation loop turn = 0 errors = 0 - while turn < self.max_turns and errors < self.max_errors: + while turn < 50 and errors < 3: turn += 1 - print(f"\n--- Turn {turn} ---") - - # Periodic heartbeat - if turn % 5 == 0: + print(f"--- Turn {turn} ---") + if turn %5 ==0: agent_availability.record_agent_heartbeat("healthcare_voice_agent") - # Listen for user input user_input = await self.audio.listen(timeout=5) - # Handle audio errors if user_input in ["UNCLEAR", "TIMEOUT", "ERROR"]: errors += 1 agent_availability.record_agent_activity("healthcare_voice_agent", success=False) - if user_input == "TIMEOUT": await self.audio.speak("I'm still here. What would you like me to help you with?") else: await self.audio.speak("I didn't catch that clearly. Could you please repeat?") continue - agent_availability.record_agent_activity("healthcare_voice_agent", success=True) if not user_input: @@ -107,114 +88,90 @@ async def start(self): print(f"USER: {user_input}") self.session.add_interaction("user", user_input) - # Check for exit commands if any(phrase in user_input.lower() for phrase in ['bye', 'goodbye', 'end', 'quit']): await self.audio.speak("Thank you for calling. Have a great day!") self.session.add_interaction("assistant", "Thank you for calling. Have a great day!") break - # Handle conversation based on mode if self.session.in_triage_mode: await self._handle_triage_conversation(user_input) else: - await self._handle_appointment_flow(user_input) + result = await self.llm.process(user_input, self.session) + + if result.get("extract"): + for key, value in result["extract"].items(): + if value: + self.session.data[key] = value + print(f"SESSION-UPDATE: Set {key} = {value}") + + if (result.get("need_triage") and not self.session.triage_complete and + self.session.triage_attempts < 1 and self.a2a_client): + + print("TRIAGE: Starting integrated triage conversation") + await self._start_integrated_triage() + continue + + if result.get("call_discovery"): + required = ['name', 'date_of_birth', 'state'] + if all(k in self.session.data and self.session.data[k] for k in required): + print("INSURANCE-DISCOVERY: Calling API...") + discovery = await self.insurance.discovery( + self.session.data['name'], + self.session.data['date_of_birth'], + self.session.data['state'] + ) + if discovery["success"]: + self.session.data['payer'] = discovery['payer'] + self.session.data['member_id'] = discovery['member_id'] + + insurance_message = f"Great! I found your insurance: {discovery['payer']}, Policy ID: {discovery['member_id']}." + await self.audio.speak(insurance_message) + self.session.add_interaction("assistant", insurance_message) + else: + fallback_msg = "I had some trouble finding your insurance, but we can proceed." + await self.audio.speak(fallback_msg) + self.session.add_interaction("assistant", fallback_msg) + + if result.get("call_eligibility"): + required = ['name', 'date_of_birth', 'member_id', 'payer', 'provider_name'] + if all(k in self.session.data and self.session.data[k] for k in required): + print("INSURANCE-ELIGIBILITY: Calling API...") + eligibility = await self.insurance.eligibility( + self.session.data['name'], + self.session.data['date_of_birth'], + self.session.data['member_id'], + self.session.data['payer'], + self.session.data['provider_name'] + ) + if eligibility["success"] and eligibility.get('copay'): + eligibility_message = f"Perfect! Your insurance is verified. Payer: {self.session.data['payer']}, Policy ID: {self.session.data['member_id']}, Your copay will be ${eligibility['copay']}." + await self.audio.speak(eligibility_message) + self.session.add_interaction("assistant", eligibility_message) + else: + fallback_message = f"Your insurance {self.session.data['payer']} with Policy ID {self.session.data['member_id']} is on file. We can proceed with scheduling." + await self.audio.speak(fallback_message) + self.session.add_interaction("assistant", fallback_message) + + response = result.get("response", "") + if response: + await self.audio.speak(response) + self.session.add_interaction("assistant", response) + + if result.get("done"): + confirmation = ''.join(random.choices(string.ascii_uppercase + string.digits, k=6)) + final_message = f"Excellent! Your appointment is confirmed. Confirmation number: {confirmation}. You'll receive an email confirmation shortly. Thank you for calling!" + await self.audio.speak(final_message) + self.session.add_interaction("assistant", final_message) + break - # End of conversation - print(f"\n{'='*60}") print(f"Conversation ended. Final data: {self.session.data}") - print(f"{'='*60}\n") - # Save session saved_file = self.session.save_to_file() if saved_file: - print(f"✓ Session saved to: {saved_file}") - - async def _handle_appointment_flow(self, user_input): - """Handle normal appointment scheduling flow""" - result = await self.llm.process(user_input, self.session) - - # Extract data fields - if result.get("extract"): - for key, value in result["extract"].items(): - if value: - self.session.data[key] = value - print(f"SESSION-UPDATE: Set {key} = {value}") - - # Start triage if needed - if (result.get("need_triage") and not self.session.triage_complete and - self.session.triage_attempts < 1 and self.a2a_client): - - print("TRIAGE: Starting integrated triage conversation") - await self._start_integrated_triage() - return - - # Call insurance discovery - if result.get("call_discovery"): - await self._handle_insurance_discovery() - - # Call insurance eligibility - if result.get("call_eligibility"): - await self._handle_insurance_eligibility() - - # Speak response - response = result.get("response", "") - if response: - await self.audio.speak(response) - self.session.add_interaction("assistant", response) - - # Complete appointment - if result.get("done"): - confirmation = ''.join(random.choices(string.ascii_uppercase + string.digits, k=6)) - final_message = f"Excellent! Your appointment is confirmed. Confirmation number: {confirmation}. You'll receive an email confirmation shortly. Thank you for calling!" - await self.audio.speak(final_message) - self.session.add_interaction("assistant", final_message) - - async def _handle_insurance_discovery(self): - """Handle insurance discovery API call""" - required = ['name', 'date_of_birth', 'state'] - if all(k in self.session.data and self.session.data[k] for k in required): - print("INSURANCE-DISCOVERY: Calling API...") - discovery = await self.insurance.discovery( - self.session.data['name'], - self.session.data['date_of_birth'], - self.session.data['state'] - ) - if discovery["success"]: - self.session.data['payer'] = discovery['payer'] - self.session.data['member_id'] = discovery['member_id'] - - insurance_message = f"Great! I found your insurance: {discovery['payer']}, Policy ID: {discovery['member_id']}." - await self.audio.speak(insurance_message) - self.session.add_interaction("assistant", insurance_message) - else: - fallback_msg = "I had some trouble finding your insurance, but we can proceed." - await self.audio.speak(fallback_msg) - self.session.add_interaction("assistant", fallback_msg) - - async def _handle_insurance_eligibility(self): - """Handle insurance eligibility API call""" - required = ['name', 'date_of_birth', 'member_id', 'payer', 'provider_name'] - if all(k in self.session.data and self.session.data[k] for k in required): - print("INSURANCE-ELIGIBILITY: Calling API...") - eligibility = await self.insurance.eligibility( - self.session.data['name'], - self.session.data['date_of_birth'], - self.session.data['member_id'], - self.session.data['payer'], - self.session.data['provider_name'] - ) - if eligibility["success"] and eligibility.get('copay'): - eligibility_message = f"Perfect! Your insurance is verified. Payer: {self.session.data['payer']}, Policy ID: {self.session.data['member_id']}, Your copay will be ${eligibility['copay']}." - await self.audio.speak(eligibility_message) - self.session.add_interaction("assistant", eligibility_message) - else: - fallback_message = f"Your insurance {self.session.data['payer']} with Policy ID {self.session.data['member_id']} is on file. We can proceed with scheduling." - await self.audio.speak(fallback_message) - self.session.add_interaction("assistant", fallback_message) + print(f"Session saved to: {saved_file}") @workflow(name="integrated_triage_workflow") async def _start_integrated_triage(self): - """Start integrated triage conversation with A2A service""" self.session.triage_attempts += 1 self.session.in_triage_mode = True @@ -226,7 +183,6 @@ async def _start_integrated_triage(self): await self.audio.speak(triage_intro) self.session.add_interaction("assistant", triage_intro) - # Default demographics for triage age = 33 sex = "female" complaint = self.session.data.get('reason', 'general health concern') @@ -237,7 +193,12 @@ async def _start_integrated_triage(self): if not result: print("TRIAGE: Failed to start - falling back to normal flow") await self._end_triage_mode("I'll help you schedule your appointment without the assessment.") - return + return { + "goto":"__end__", + "error":True, + "success":False, + "reason":"triage_start_failed" + } if result.get('kind') == 'task': self.session.triage_task_id = result['id'] @@ -250,38 +211,55 @@ async def _start_integrated_triage(self): if triage_question: await self.audio.speak(triage_question) self.session.add_interaction("assistant", triage_question) + return { + "goto": "triage_service_agent", + "success": True, + "task_id" : result['id'], + "context_id":result['contextId'], + "action":"triage_started" + } except Exception as e: print(f"TRIAGE: Error starting: {e}") await self._end_triage_mode("Let me help you schedule your appointment.") + return{ + "goto":"__end__", + "error":True, + "success":False, + "error_message":str(e) + } @workflow(name="triage_conversational_flow") async def _handle_triage_conversation(self, user_input): - """Handle multi-turn triage conversation""" print(f"TRIAGE: User response: {user_input}") try: message_parts = [{"kind": "text", "text": user_input}] result = await self.a2a_client.send_message( - message_parts, - task_id=self.session.triage_task_id, + message_parts, + task_id=self.session.triage_task_id, context_id=self.session.triage_context_id ) if not result: print("TRIAGE: Failed to continue - ending triage") await self._end_triage_mode("Let me help you continue with scheduling your appointment.") - return + return{ + "goto":"__end__", + "error":True, + "success":False, + "reason":"triage_communication_failed" + } + task_data = result.get('a2a_response', result) task_state = result['status']['state'] print(f"TRIAGE: A2A task state: {task_state}") if task_state == TaskState.completed: - print("TRIAGE: Assessment COMPLETED") + print("TRIAGE: Assessment COMPLETED - exiting A2A mode") - # Extract triage results - if result.get('artifacts'): - artifact = result['artifacts'][0] + if task_data.get('artifacts'): + artifact = task_data['artifacts'][0] triage_data = self._extract_triage_results(artifact) if triage_data: self.session.triage_results.update(triage_data) @@ -293,42 +271,76 @@ async def _handle_triage_conversation(self, user_input): completion_message = f"Thank you for the assessment. Based on your responses, I recommend seeing a {doctor_type}. Priority level: {urgency}. Now let's get you scheduled. I'll need your date of birth for insurance verification." await self._end_triage_mode() + await self.audio.speak(completion_message) self.session.add_interaction("assistant", completion_message) + return { + "goto":"__end__", + "success":True, + "triage_complete":True, + "urgency_level":urgency, + "doctor_type":doctor_type + } + elif task_state == TaskState.input_required: if result['status'].get('message'): next_question = self._extract_text_from_message(result['status']['message']) if next_question: await self.audio.speak(next_question) self.session.add_interaction("assistant", next_question) + return { + "goto":"triage_service_agent", + "success":True, + "action":"continue_triage", + "state":"awaiting_user_input" + } else: print("TRIAGE: No message in input-required state - ending triage") await self._end_triage_mode("Let me help you continue with scheduling your appointment.") - + return { + "goto":"__end__", + "error":True, + "success":False, + "reason":"no_triage_message" + } + elif task_state in [TaskState.failed, TaskState.canceled]: print(f"TRIAGE: Task ended with state: {task_state}") await self._end_triage_mode("Let me help you continue with scheduling your appointment.") - + return { + "goto":"__end__", + "error":True, + "success":False, + "task_state":task_state + } + except Exception as e: print(f"TRIAGE: Error in conversation: {e}") await self._end_triage_mode("Let me help you continue with scheduling your appointment.") + return { + "goto":"__end__", + "error":True, + "success":False, + "error_message":str(e) + } async def _end_triage_mode(self, message=None): - """End triage mode and return to normal flow""" - print("TRIAGE: Ending triage mode") + print("TRIAGE: Ending triage mode - cleaning up A2A connection") self.session.in_triage_mode = False self.session.triage_complete = True + self.session.triage_task_id = None self.session.triage_context_id = None + print("TRIAGE: Mode ended - returning to normal appointment flow") + if message: await self.audio.speak(message) self.session.add_interaction("assistant", message) def _extract_text_from_message(self, message): - """Extract text content from A2A message""" if not message or not message.get('parts'): return None @@ -339,7 +351,6 @@ def _extract_text_from_message(self, message): return None def _extract_triage_results(self, artifact): - """Extract triage results from artifact""" if not artifact or not artifact.get('parts'): return {} @@ -347,4 +358,4 @@ def _extract_triage_results(self, artifact): if part.get('kind') == 'data' and part.get('data'): return part['data'] - return {} \ No newline at end of file + return {} diff --git a/agentic-healthcare-booking-app/voice_agent/agntcy/audio/audio.py b/agentic-healthcare-booking-app/voice_agent/agntcy/audio/audio.py index d06c1c5..c86c052 100644 --- a/agentic-healthcare-booking-app/voice_agent/agntcy/audio/audio.py +++ b/agentic-healthcare-booking-app/voice_agent/agntcy/audio/audio.py @@ -1,80 +1,70 @@ -""" -Audio System for Speech Recognition and Text-to-Speech -""" -import asyncio +# Audio System import os +import asyncio import tempfile -from ioa_observe.sdk.decorators import tool try: - import speech_recognition as sr - import pygame from gtts import gTTS + import pygame + import speech_recognition as sr AUDIO_AVAILABLE = True except ImportError: - AUDIO_AVAILABLE = False + AUDIO_AVAILABLE = False # Speech recognition not available + +from ioa_observe.sdk.decorators import tool class AudioSystem: - """Handles speech recognition and text-to-speech""" - def __init__(self): self.enabled = AUDIO_AVAILABLE self.tts_enabled = False self.speech_enabled = False if self.enabled: - self._initialize_audio() - - def _initialize_audio(self): - """Initialize audio components""" - try: - print("Initializing audio...") - - # Initialize speech recognition try: - self.recognizer = sr.Recognizer() - self.microphone = sr.Microphone() - with self.microphone as source: - self.recognizer.adjust_for_ambient_noise(source, duration=1) + print("Initializing audio...") - self.recognizer.energy_threshold = 300 - self.recognizer.dynamic_energy_threshold = True - self.recognizer.pause_threshold = 0.8 - self.speech_enabled = True - print("✓ Speech recognition ready") - except Exception as e: - print(f"✗ Speech recognition failed: {e}") - self.speech_enabled = False - - # Initialize TTS - try: - pygame.mixer.pre_init(frequency=22050, size=-16, channels=2, buffer=1024) - pygame.mixer.init() - self.tts_enabled = True - print("✓ TTS system ready") - except Exception as e: - print(f"✗ TTS init failed: {e}") - self.tts_enabled = False + try: + self.recognizer = sr.Recognizer() + self.microphone = sr.Microphone() + with self.microphone as source: + self.recognizer.adjust_for_ambient_noise(source, duration=1) + + self.recognizer.energy_threshold = 300 + self.recognizer.dynamic_energy_threshold = True + self.recognizer.pause_threshold = 0.8 + self.speech_enabled = True + print("Speech recognition ready") + except Exception as e: + print(f"Speech recognition failed: {e}") + self.speech_enabled = False - except Exception as e: - print(f"✗ Audio init failed: {e}") - self.enabled = False + try: + pygame.mixer.pre_init(frequency=22050, size=-16, channels=2, buffer=1024) + pygame.mixer.init() + self.tts_enabled = True + print("TTS system ready") + except Exception as e: + print(f"TTS init failed: {e}") + self.tts_enabled = False + + except Exception as e: + print(f"Audio init failed: {e}") + self.enabled = False @tool(name="listening_tool") async def listen(self, timeout=5): - """Listen for user input via microphone or console""" if not self.speech_enabled: return input("You: ").strip() - print("🎤 Listening...") + print("Listening...") def _listen(): try: with self.microphone as source: audio = self.recognizer.listen(source, timeout=timeout, phrase_time_limit=6) result = self.recognizer.recognize_google(audio, language='en-US') - print(f"✓ Recognized: '{result}'") + print(f"Recognized: '{result}'") return result.strip() except sr.UnknownValueError: return "UNCLEAR" @@ -88,10 +78,10 @@ def _listen(): @tool(name="speaking_tool") async def speak(self, text): - """Speak text using TTS or print to console""" - print(f"🤖 Agent: {text}") + print(f"Agent: {text}") if not self.tts_enabled: + print("TTS: Not enabled, skipping audio") return def _speak(): @@ -106,7 +96,6 @@ def _speak(): pygame.mixer.music.load(temp_file) pygame.mixer.music.play() - # Wait for playback to complete (max 30 seconds) max_wait = 30 wait_count = 0 while pygame.mixer.music.get_busy() and wait_count < max_wait * 20: diff --git a/agentic-healthcare-booking-app/voice_agent/agntcy/clients/a2a_client.py b/agentic-healthcare-booking-app/voice_agent/agntcy/clients/a2a_client.py index 3efd1ff..b8d4f38 100644 --- a/agentic-healthcare-booking-app/voice_agent/agntcy/clients/a2a_client.py +++ b/agentic-healthcare-booking-app/voice_agent/agntcy/clients/a2a_client.py @@ -1,10 +1,10 @@ -""" -A2A (Agent-to-Agent) Client for Hosted Service Communication -""" -import asyncio +# A2A Client for Hosted Service +import os import time import uuid +import asyncio import requests + from ioa_observe.sdk import Observe from ioa_observe.sdk.instrumentations.a2a import A2AInstrumentor from ioa_observe.sdk.decorators import tool @@ -12,17 +12,14 @@ class A2AClient: - """Client for communicating with A2A hosted services""" - - def __init__(self, base_url, message_url, api_key, otlp_endpoint): - self.base_url = base_url - self.message_url = message_url - self.api_key = api_key + def __init__(self): + self.base_url = os.getenv('A2A_SERVICE_URL', 'http://localhost:8887') + self.message_url = os.getenv('A2A_MESSAGE_URL', self.base_url) + self.api_key = os.getenv('A2A_API_KEY') self.agent_id = f"client_{uuid.uuid4().hex[:8]}" self.agent_card = None - - # Initialize observability - Observe.init("A2A_Client", api_endpoint=otlp_endpoint) + api_endpoint = os.getenv('OTLP_ENDPOINT', 'http://localhost:4318') + Observe.init("A2A_Client", api_endpoint=api_endpoint) A2AInstrumentor().instrument() print(f"A2A-CLIENT: Initialized as {self.agent_id}") @@ -31,7 +28,6 @@ def __init__(self, base_url, message_url, api_key, otlp_endpoint): print(f"A2A-CLIENT: API Key: {'Set' if self.api_key else 'Not set'}") def _timed_request(self, method, url, description, **kwargs): - """Execute a timed HTTP request with detailed logging""" start_time = time.time() timestamp = time.strftime("%H:%M:%S", time.localtime(start_time)) print(f"A2A-CLIENT: [{timestamp}] >>> {method} {description}") @@ -62,22 +58,17 @@ def _timed_request(self, method, url, description, **kwargs): return None, elapsed async def discover_agent(self): - """Discover the A2A agent's capabilities""" try: def _request(): - return self._timed_request( - 'GET', - f"{self.base_url}/.well-known/agent-card.json", - "Agent Discovery", - timeout=30 - ) + return self._timed_request('GET', f"{self.base_url}/.well-known/agent-card.json", + "Agent Discovery", timeout=30) loop = asyncio.get_event_loop() response, elapsed = await loop.run_in_executor(None, _request) if response and response.status_code == 200: self.agent_card = response.json() - print(f"A2A-CLIENT: Discovered agent: {self.agent_card.get('name', 'Unknown')}") + print(f"A2A-CLIENT: Discovered agent: {self.agent_card['name']}") return True else: if response: @@ -89,7 +80,6 @@ def _request(): @tool(name="a2a_message_tool") async def send_message(self, message_parts, task_id=None, context_id=None): - """Send a message to the A2A service""" message = { "role": "user", "parts": message_parts, @@ -101,10 +91,7 @@ async def send_message(self, message_parts, task_id=None, context_id=None): message["taskId"] = task_id if context_id: message["contextId"] = context_id - - # Start observability session session_start() - payload = { "jsonrpc": "2.0", "id": str(uuid.uuid4()), @@ -129,8 +116,6 @@ async def send_message(self, message_parts, task_id=None, context_id=None): try: def _request(): headers = {"Content-Type": "application/json"} - - # Add API key if available if self.api_key: headers['X-Shared-Key'] = self.api_key @@ -138,14 +123,8 @@ def _request(): if task_id: description += f" (Task: {task_id})" - return self._timed_request( - 'POST', - self.message_url, - description, - json=payload, - headers=headers, - timeout=60 - ) + return self._timed_request('POST', self.message_url, description, + json=payload, headers=headers, timeout=60) loop = asyncio.get_event_loop() response, elapsed = await loop.run_in_executor(None, _request) @@ -184,4 +163,4 @@ def _request(): except Exception as e: print(f"A2A-CLIENT: Request failed: {e}") - return None \ No newline at end of file + return None diff --git a/agentic-healthcare-booking-app/voice_agent/agntcy/clients/insurance_client.py b/agentic-healthcare-booking-app/voice_agent/agntcy/clients/insurance_client.py index e42dfa2..e34bb68 100644 --- a/agentic-healthcare-booking-app/voice_agent/agntcy/clients/insurance_client.py +++ b/agentic-healthcare-booking-app/voice_agent/agntcy/clients/insurance_client.py @@ -1,23 +1,17 @@ -""" -Insurance/MCP Client for discovery and eligibility checks -""" -import asyncio +# Insurance Client import re import requests +import asyncio from datetime import datetime from ioa_observe.sdk.decorators import tool - class InsuranceClient: - """Client for insurance discovery and eligibility verification via MCP""" - def __init__(self, mcp_url, api_key): self.mcp_url = mcp_url self.headers = {"Content-Type": "application/json", "X-INF-API-KEY": api_key} print("INSURANCE: Client initialized") def _split_name(self, name): - """Split full name into first and last""" parts = name.strip().split() if len(parts) == 1: return parts[0], "" @@ -27,16 +21,14 @@ def _split_name(self, name): return parts[0], " ".join(parts[1:]) def _format_dob(self, dob): - """Format date of birth to YYYY-MM-DD""" if not dob: return "" - # Handle MM/DD/YYYY format if re.match(r'^\d{1,2}/\d{1,2}/\d{4}$', dob): month, day, year = dob.split('/') - return f"{year}-{month.zfill(2)}-{day.zfill(2)}" + formatted = f"{year}-{month.zfill(2)}-{day.zfill(2)}" + return formatted - # Already in YYYY-MM-DD format if re.match(r'^\d{4}-\d{1,2}-\d{1,2}$', dob): return dob @@ -44,9 +36,7 @@ def _format_dob(self, dob): @tool(name="insurance_discovery_tool") async def discovery(self, name, dob, state): - """Discover insurance information for a patient""" print(f"INSURANCE: Discovery - {name}, {dob}, {state}") - first, last = self._split_name(name) formatted_dob = self._format_dob(dob) formatted_state = state.strip().title() if state else "" @@ -78,38 +68,31 @@ def _request(): if "result" in data: result_text = str(data["result"]) - # Extract payer name payer = "" + member_id = "" + for pattern in [r'payer[:\s]*([^\n,;]+)', r'insurance[:\s]*([^\n,;]+)', r'plan[:\s]*([^\n,;]+)']: match = re.search(pattern, result_text.lower()) if match: payer = match.group(1).strip().title() break - # Extract member ID - member_id = "" - for pattern in [r'member\s*id[:\s]*([a-za-z0-9\-]+)', r'subscriber\s*id[:\s]*([a-za-z0-9\-]+)', - r'policy\s*id[:\s]*([a-za-z0-9\-]+)', r'policy[:\s]*([a-za-z0-9\-]+)']: + for pattern in [r'member\s*id[:\s]*([a-za-z0-9\-]+)', r'subscriber\s*id[:\s]*([a-za-z0-9\-]+)', r'policy\s*id[:\s]*([a-za-z0-9\-]+)', r'policy[:\s]*([a-za-z0-9\-]+)']: match = re.search(pattern, result_text.lower()) if match: member_id = match.group(1).strip().upper() break - print(f"INSURANCE: Discovery success - Payer: {payer}, Member ID: {member_id}") return {"success": True, "payer": payer, "member_id": member_id} - print("INSURANCE: Discovery failed") return {"success": False} @tool(name="insurance_eligibility_tool") async def eligibility(self, name, dob, subscriber_id, payer_name, provider_name): - """Check insurance eligibility and benefits""" - print(f"INSURANCE: Eligibility check for {name}") - + print(f"INSURANCE: Eligibility check") first, last = self._split_name(name) formatted_dob = self._format_dob(dob) - # Clean provider name provider_clean = re.sub(r'\b(Dr\.?|MD|DO)\b', '', provider_name, flags=re.IGNORECASE).strip() provider_first, provider_last = self._split_name(provider_clean) @@ -127,7 +110,7 @@ async def eligibility(self, name, dob, subscriber_id, payer_name, provider_name) "payerName": payer_name, "providerFirstName": provider_first, "providerLastName": provider_last, - "providerNpi": "1234567890" # Default NPI + "providerNpi": "1234567890" } } } @@ -144,13 +127,8 @@ def _request(): if "result" in data: result_text = str(data["result"]) - # Extract copay amount copay = "" - copay_patterns = [ - r'co-?pay[:\s]*\$?([0-9,]+)', - r'copayment[:\s]*\$?([0-9,]+)', - r'patient\s+responsibility[:\s]*\$?([0-9,]+)' - ] + copay_patterns = [r'co-?pay[:\s]*\$?([0-9,]+)', r'copayment[:\s]*\$?([0-9,]+)', r'patient\s+responsibility[:\s]*\$?([0-9,]+)'] for pattern in copay_patterns: copay_match = re.search(pattern, result_text.lower()) @@ -158,8 +136,6 @@ def _request(): copay = copay_match.group(1) break - print(f"INSURANCE: Eligibility success - Copay: ${copay}") return {"success": True, "copay": copay} - print("INSURANCE: Eligibility check failed") - return {"success": False} \ No newline at end of file + return {"success": False} diff --git a/agentic-healthcare-booking-app/voice_agent/agntcy/clients/llm_client.py b/agentic-healthcare-booking-app/voice_agent/agntcy/clients/llm_client.py index 1563168..0dd29f1 100644 --- a/agentic-healthcare-booking-app/voice_agent/agntcy/clients/llm_client.py +++ b/agentic-healthcare-booking-app/voice_agent/agntcy/clients/llm_client.py @@ -1,15 +1,11 @@ -""" -LLM Client for processing user inputs -""" +# LLM Client import asyncio import json import requests -from ioa_observe.sdk.decorators import tool +from ioa_observe.sdk.decorators import tool class LLMClient: - """Client for LLM-based conversation processing""" - def __init__(self, jwt_token, endpoint_url, project_id, connection_id): self.headers = { 'Content-Type': 'application/json', @@ -22,62 +18,10 @@ def __init__(self, jwt_token, endpoint_url, project_id, connection_id): @tool(name="llm_tool") async def process(self, user_input, session): - """Process user input and return structured response""" print(f"LLM: Processing: '{user_input[:50]}...'") if session.in_triage_mode: - prompt = self._build_triage_prompt(user_input, session) - else: - prompt = self._build_appointment_prompt(user_input, session) - - payload = { - "messages": [ - {"role": "system", "content": prompt}, - {"role": "user", "content": user_input} - ], - "project_id": self.project_id, - "connection_id": self.connection_id, - "max_tokens": 400, - "temperature": 0.2 - } - - def _request(): - return requests.post(self.endpoint_url, headers=self.headers, json=payload, timeout=30) - - loop = asyncio.get_event_loop() - response = await loop.run_in_executor(None, _request) - - if response.status_code == 200: - data = response.json() - if 'choices' in data and data['choices']: - content = data['choices'][0]['message']['content'] - - try: - # Clean JSON response - if content.startswith('```json'): - content = content[7:] - if content.endswith('```'): - content = content[:-3] - - result = json.loads(content.strip()) - print("LLM: Response parsed successfully") - return result - except Exception as e: - print(f"LLM: Failed to parse response: {e}") - - # Return default response on failure - return { - "response": "I understand. Please continue.", - "extract": {}, - "need_triage": False, - "call_discovery": False, - "call_eligibility": False, - "done": False - } - - def _build_triage_prompt(self, user_input, session): - """Build prompt for triage mode""" - return f"""You are in TRIAGE MODE. The user is answering medical assessment questions. + prompt = f"""You are in TRIAGE MODE. The user is answering medical assessment questions. Current triage task: {session.triage_task_id} User response to triage question: "{user_input}" @@ -92,10 +36,8 @@ def _build_triage_prompt(self, user_input, session): "done": false, "continue_triage": true }}""" - - def _build_appointment_prompt(self, user_input, session): - """Build prompt for appointment scheduling mode""" - return f"""You are a healthcare appointment scheduler with this specific flow: + else: + prompt = f"""You are a healthcare appointment scheduler with this specific flow: 1. Ask name, phone 2. Ask reason for visit @@ -126,4 +68,47 @@ def _build_appointment_prompt(self, user_input, session): "call_discovery": true/false, "call_eligibility": true/false, "done": true/false -}}""" \ No newline at end of file +}}""" + + payload = { + "messages": [ + {"role": "system", "content": prompt}, + {"role": "user", "content": user_input} + ], + "project_id": self.project_id, + "connection_id": self.connection_id, + "max_tokens": 400, + "temperature": 0.2 + } + + def _request(): + return requests.post(self.endpoint_url, headers=self.headers, json=payload, timeout=30) + + loop = asyncio.get_event_loop() + response = await loop.run_in_executor(None, _request) + + if response.status_code == 200: + data = response.json() + if 'choices' in data and data['choices']: + content = data['choices'][0]['message']['content'] + + try: + if content.startswith('```json'): + content = content[7:] + if content.endswith('```'): + content = content[:-3] + + result = json.loads(content.strip()) + print("LLM: Response parsed") + return result + except: + pass + + return { + "response": "I understand. Please continue.", + "extract": {}, + "need_triage": False, + "call_discovery": False, + "call_eligibility": False, + "done": False + } \ No newline at end of file diff --git a/agentic-healthcare-booking-app/voice_agent/agntcy/config/__init__.py b/agentic-healthcare-booking-app/voice_agent/agntcy/config/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/agentic-healthcare-booking-app/voice_agent/agntcy/config/settings.py b/agentic-healthcare-booking-app/voice_agent/agntcy/config/settings.py deleted file mode 100644 index 7540e36..0000000 --- a/agentic-healthcare-booking-app/voice_agent/agntcy/config/settings.py +++ /dev/null @@ -1,53 +0,0 @@ -""" -Configuration Settings -""" -import os -from dotenv import load_dotenv - -# Try to import audio dependencies -try: - import speech_recognition as sr - import pygame - from gtts import gTTS - AUDIO_AVAILABLE = True -except ImportError: - AUDIO_AVAILABLE = False - - -class Settings: - """Application settings from environment variables""" - - def __init__(self): - load_dotenv() - - # LLM Configuration - self.jwt_token = os.getenv('JWT_TOKEN') - self.endpoint_url = os.getenv('ENDPOINT_URL') - self.project_id = os.getenv('PROJECT_ID') - self.connection_id = os.getenv('CONNECTION_ID') - - # Insurance/MCP Configuration - self.mcp_url = os.getenv('MCP_URL') - self.insurance_api_key = os.getenv('X_INF_API_KEY') - - # A2A Configuration - self.a2a_service_url = os.getenv('A2A_SERVICE_URL', 'http://localhost:8887') - self.a2a_message_url = os.getenv('A2A_MESSAGE_URL', self.a2a_service_url) - self.a2a_api_key = os.getenv('A2A_API_KEY') - - # TBAC Configuration - self.client_agent_api_key = os.getenv('CLIENT_AGENT_API_KEY') - self.client_agent_id = os.getenv('CLIENT_AGENT_ID') - self.a2a_service_api_key = os.getenv('A2A_SERVICE_API_KEY') - self.a2a_service_id = os.getenv('A2A_SERVICE_ID') - - # Observability Configuration - self.otlp_endpoint = os.getenv('OTLP_ENDPOINT', 'http://localhost:4318') - - # Audio Configuration - self.audio_available = AUDIO_AVAILABLE - - # Session Configuration - self.session_dir = os.getenv('SESSION_DIR', 'sessions') - self.max_turns = int(os.getenv('MAX_TURNS', '50')) - self.max_errors = int(os.getenv('MAX_ERRORS', '3')) \ No newline at end of file diff --git a/agentic-healthcare-booking-app/voice_agent/agntcy/main.py b/agentic-healthcare-booking-app/voice_agent/agntcy/main.py index 0415047..ddeef06 100644 --- a/agentic-healthcare-booking-app/voice_agent/agntcy/main.py +++ b/agentic-healthcare-booking-app/voice_agent/agntcy/main.py @@ -1,98 +1,57 @@ -""" -Healthcare Voice Agent - Main Entry Point with TBAC Patching -""" -import asyncio import os -import sys -from config.settings import Settings -from common.identity.tbac import TBAC -from common.observe.observe_config import initialize_observability - +import asyncio +from agent.healthcare_agent import HealthcareAgent +from audio.audio import AUDIO_AVAILABLE -# Global TBAC instance -tbac = TBAC() +from common.observe.observe_config import initialize_observability +from common.identity.tbac import TBAC +from agent.healthcare_agent import HealthcareAgent +from dotenv import load_dotenv +load_dotenv() -def display_startup_banner(): - """Display startup information""" - print("=" * 60) +def run_agent(): + print("=" * 50) print("HEALTHCARE VOICE + A2A + MCP AGENT") - print("=" * 60) - + print("=" * 50) -def check_configuration(): - """Validate required environment variables""" - settings = Settings() - - missing = [] - - # Check JWT/LLM config - if not all([settings.jwt_token, settings.endpoint_url, - settings.project_id, settings.connection_id]): - missing.extend(['JWT_TOKEN', 'ENDPOINT_URL', 'PROJECT_ID', 'CONNECTION_ID']) + service_name = "Healthcare_Voice_Agent" + initialize_observability(service_name) - # Check Insurance/MCP config - if not all([settings.mcp_url, settings.insurance_api_key]): - missing.extend(['MCP_URL', 'X_INF_API_KEY']) + jwt_required = ['JWT_TOKEN', 'ENDPOINT_URL', 'PROJECT_ID', 'CONNECTION_ID'] + insurance_required = ['MCP_URL', 'X_INF_API_KEY'] + a2a_required = ['A2A_SERVICE_URL', 'A2A_MESSAGE_URL', 'A2A_API_KEY'] - # Check A2A config - if not all([settings.a2a_service_url, settings.a2a_message_url, settings.a2a_api_key]): - missing.extend(['A2A_SERVICE_URL', 'A2A_MESSAGE_URL', 'A2A_API_KEY']) + missing = [] + missing.extend([var for var in jwt_required if not os.getenv(var)]) + missing.extend([var for var in insurance_required if not os.getenv(var)]) + missing.extend([var for var in a2a_required if not os.getenv(var)]) if missing: - print(f"❌ ERROR: Missing configuration: {', '.join(set(missing))}") - return False - - print("✓ Configuration validated") - return True - - -def check_tbac_status(): - """Initialize and check TBAC authorization""" - print("\n" + "=" * 60) - print("TBAC (Task-Based Access Control) Status") - print("=" * 60) - - # Display TBAC configuration - if not all([tbac.client_api_key, tbac.client_id, tbac.a2a_api_key, tbac.a2a_id]): - print("⚠️ TBAC: DISABLED (missing credentials)") - print(" Agent will run without token-based authorization") + print(f"ERROR: Missing config: {missing}") return - print("✓ TBAC: ENABLED") - print(f" Client Agent ID: {tbac.client_id}") - print(f" A2A Service ID: {tbac.a2a_id}") + print("Configuration validated") + print(f"A2A Service URL: {os.getenv('A2A_SERVICE_URL')}") + print(f"A2A Message URL: {os.getenv('A2A_MESSAGE_URL')}") - # Perform bidirectional authorization - print("\nPerforming bidirectional authorization...") - - if tbac.authorize_bidirectional(): - print("✓ TBAC: FULLY AUTHORIZED") - print(f" ✓ Voice Agent → A2A Service: AUTHORIZED") - print(f" ✓ A2A Service → Voice Agent: AUTHORIZED") + if AUDIO_AVAILABLE: + print("Audio system available - Triage conversation integrated") else: - print("❌ TBAC: AUTHORIZATION FAILED") - if not tbac.is_client_authorized(): - print(" ✗ Voice Agent → A2A Service: UNAUTHORIZED") - if not tbac.is_a2a_authorized(): - print(" ✗ A2A Service → Voice Agent: UNAUTHORIZED") - - print("\n⚠️ WARNING: Agent will be blocked from A2A communication") - + print("Console mode only") + + # TBAC + print("TBAC Authorization.....") + tbac = TBAC() + auth_success = tbac.authorize_bidirectional() + if auth_success: + print("TBAC Authorization successful") + else: + print("TBAC Authorization failed") -def patch_with_tbac(): - """Patch agent components with TBAC authorization checks""" - print("\n" + "=" * 60) - print("TBAC Patching") - print("=" * 60) - - # Skip patching if TBAC not configured - if not all([tbac.client_api_key, tbac.client_id, tbac.a2a_api_key, tbac.a2a_id]): - print("⚠️ TBAC patching skipped (not configured)") - return - + # APPLY TBAC PATCHES + print("\n--- Applying TBAC Patches ---") try: - # Import modules to patch from clients import a2a_client from agent import healthcare_agent @@ -101,99 +60,74 @@ def patch_with_tbac(): original_send = a2a_client.A2AClient.send_message async def patched_send(self, message_parts, task_id=None, context_id=None): - """Patched send_message with TBAC authorization check""" - if not tbac.is_voice_authorized(): - print("❌ TBAC: Voice agent NOT AUTHORIZED to send message to A2A service") + if not tbac.authorize_bidirectional(): + print("TBAC: Voice agent not authorized to send message to A2A service") return None - - print("✓ TBAC: Voice agent authorized - allowing A2A message") return await original_send(self, message_parts, task_id, context_id) a2a_client.A2AClient.send_message = patched_send - print("✓ Patched: A2AClient.send_message with authorization check") + print("✓ TBAC: Patched A2AClient.send_message") # Patch HealthcareAgent._start_integrated_triage if hasattr(healthcare_agent, 'HealthcareAgent'): original_triage = healthcare_agent.HealthcareAgent._start_integrated_triage async def patched_triage(self): - """Patched triage start with TBAC authorization check""" - if not tbac.is_voice_authorized(): - print("❌ TBAC: Medical triage BLOCKED - Voice agent not authorized") - await self.audio.speak("I apologize, but medical triage is not available at this time.") - self.session.add_interaction("assistant", "Medical triage unavailable due to authorization.") - return - - print("✓ TBAC: Triage authorized - proceeding") + if not tbac.authorize_bidirectional(): + print("TBAC: Triage blocked - voice agent not authorized") + await self.audio.speak("Medical triage is currently unavailable. Let me help you schedule your appointment.") + self.session.add_interaction("assistant", "Medical triage is currently unavailable. Let me help you schedule your appointment.") + return { + "goto": "__end__", + "error": True, + "success": False, + "reason": "tbac_authorization_failed" + } return await original_triage(self) healthcare_agent.HealthcareAgent._start_integrated_triage = patched_triage - print("✓ Patched: HealthcareAgent._start_integrated_triage with authorization check") + print("✓ TBAC: Patched HealthcareAgent._start_integrated_triage") - print("\n✓ TBAC patching complete - authorization checks active") + # Patch HealthcareAgent._handle_triage_conversation + if hasattr(healthcare_agent, 'HealthcareAgent'): + original_handle = healthcare_agent.HealthcareAgent._handle_triage_conversation + + async def patched_handle(self, user_input): + if not tbac.authorize_bidirectional(): + print("TBAC: Triage conversation blocked - voice agent not authorized") + await self._end_triage_mode("I apologize, but I need to end the medical assessment. Let me help you continue with scheduling.") + return { + "goto": "__end__", + "error": True, + "success": False, + "reason": "tbac_authorization_lost" + } + return await original_handle(self, user_input) + + healthcare_agent.HealthcareAgent._handle_triage_conversation = patched_handle + print("✓ TBAC: Patched HealthcareAgent._handle_triage_conversation") + + print("✓ TBAC: All patches applied successfully\n") - except ImportError as e: - print(f"❌ TBAC patching failed: {e}") - print(" Agent modules not found - ensure correct import paths") - sys.exit(1) - except Exception as e: - print(f"❌ TBAC patching error: {e}") - sys.exit(1) - - -def display_system_info(settings): - """Display system configuration""" - print("\n" + "=" * 60) - print("System Configuration") - print("=" * 60) - print(f"A2A Service URL: {settings.a2a_service_url}") - print(f"A2A Message URL: {settings.a2a_message_url}") - print(f"MCP URL: {settings.mcp_url}") - print(f"Audio System: {'ENABLED' if settings.audio_available else 'DISABLED (console mode)'}") - print("=" * 60 + "\n") - - -async def run_agent(): - """Run the healthcare agent""" - try: - from agent.healthcare_agent import HealthcareAgent - agent = HealthcareAgent() - await agent.start() - except KeyboardInterrupt: - print("\n\n✓ Agent stopped by user") except Exception as e: - print(f"\n❌ Agent error: {e}") - import traceback - traceback.print_exc() - - -def main(): - """Main entry point""" - display_startup_banner() - - # Initialize observability - initialize_observability("Healthcare_Voice_Agent") + print(f"⚠ TBAC: Patching failed: {e}\n") - # Check configuration - if not check_configuration(): - return - - # Check and display TBAC status - check_tbac_status() - - # Patch components with TBAC authorization - patch_with_tbac() + async def start(): + try: + agent = HealthcareAgent() + await agent.start() + except KeyboardInterrupt: + print("\nAgent stopped by user") + except Exception as e: + print(f"Agent error: {e}") - # Display system info - settings = Settings() - display_system_info(settings) - - # Run the agent try: - asyncio.run(run_agent()) + asyncio.run(start()) except KeyboardInterrupt: - print("\n✓ Shutting down gracefully...") + print("\nShutting down...") +def main(): + run_agent() if __name__ == "__main__": main() \ No newline at end of file diff --git a/agentic-healthcare-booking-app/voice_agent/agntcy/session/session.py b/agentic-healthcare-booking-app/voice_agent/agntcy/session/session.py index 8ec8db4..d78d694 100644 --- a/agentic-healthcare-booking-app/voice_agent/agntcy/session/session.py +++ b/agentic-healthcare-booking-app/voice_agent/agntcy/session/session.py @@ -1,18 +1,12 @@ -""" -Session Management -""" -import json +# Session Management import os +import json import uuid from datetime import datetime - class Session: - """Manages session state and conversation history""" - - def __init__(self, session_dir='sessions'): + def __init__(self): self.id = str(uuid.uuid4())[:8] - self.session_dir = session_dir self.data = {} self.triage_complete = False self.triage_attempts = 0 @@ -22,11 +16,8 @@ def __init__(self, session_dir='sessions'): self.triage_context_id = None self.triage_results = {} self.in_triage_mode = False - - print(f"SESSION: Created session {self.id}") def add_interaction(self, role, message, extra_data=None): - """Log a conversation interaction""" interaction = { "timestamp": datetime.now().isoformat(), "role": role, @@ -35,15 +26,13 @@ def add_interaction(self, role, message, extra_data=None): } if extra_data: interaction["extra_data"] = extra_data - self.conversation_log.append(interaction) print(f"SESSION-LOG: {role.upper()} - {message[:100]}...") def save_to_file(self): - """Save session data to file""" try: - os.makedirs(self.session_dir, exist_ok=True) - filename = f"{self.session_dir}/session_{datetime.now().strftime('%Y%m%d_%H%M%S')}_{self.id}.json" + os.makedirs("sessions", exist_ok=True) + filename = f"sessions/session_{datetime.now().strftime('%Y%m%d_%H%M%S')}_{self.id}.json" session_data = { "session_id": self.id, @@ -53,7 +42,6 @@ def save_to_file(self): "final_data": self.data, "triage_complete": self.triage_complete, "triage_attempts": self.triage_attempts, - "triage_results": self.triage_results, "conversation_log": self.conversation_log, "data_fields_collected": list(self.data.keys()), "total_interactions": len(self.conversation_log)