diff --git a/26.0.1 b/26.0.1 new file mode 100644 index 0000000..e69de29 diff --git a/SETUP_CHECKLIST.md b/SETUP_CHECKLIST.md new file mode 100644 index 0000000..749bf6b --- /dev/null +++ b/SETUP_CHECKLIST.md @@ -0,0 +1,77 @@ +# PaCo — Local Setup Checklist + +Things a new developer needs to run PaCo locally. The environment (Python 3.12 venv + +dependencies) is already built and verified on this machine; what remains is supplying +secrets that are **not** stored in the repo. + +## 🔴 Required — backend will not start without these + +| Item | Env variable | Where to get it | +|------|-------------|-----------------| +| Database connection string (incl. password) | `DATABASE_URL` | Render dashboard → `paco-api` service → Environment, **or** the Neon console. The existing study DB is `ep-wandering-sea-a5xid0o5.us-east-2.aws.neon.tech/neondb` (user `neondb_owner`). | +| Groq API key | `GROQ_API_KEY` | https://console.groq.com | +| Gemini API key | `GEMINI_API_KEY` | https://aistudio.google.com/apikey | +| OpenAI API key | `OPENAI_API_KEY` | https://platform.openai.com/api-keys | + +> At least one LLM key is mandatory; this project uses all three (fallback order: Gemini → OpenAI → Groq). + +## 🟡 Required for voice features + +| Item | Env variable | Notes | +|------|-------------|-------| +| ElevenLabs API key | `ELEVENLABS_API_KEY` (backend) + `NEXT_PUBLIC_ELEVENLABS_API_KEY` (frontend) | https://elevenlabs.io | +| ElevenLabs Agent ID | `ELEVENLABS_AGENT_ID` (backend) + `NEXT_PUBLIC_ELEVENLABS_AGENT_ID` (frontend) | The Conversational AI agent for this study | + +## 🟢 Account/login access to request + +- **Render** — backend host + production env vars (and the `DATABASE_URL`) +- **Vercel** — frontend host + its env vars +- **Neon** — the database (connection string, backups, SQL access) +- **ElevenLabs** — to view/manage the conversational agent +- (Optional) the production `SECRET_KEY`, only if you need local JWTs to match production + +## ⚪ You do NOT need to ask anyone for these + +- `SECRET_KEY` — generate locally: `python -c "import secrets; print(secrets.token_urlsafe(32))"` +- `ADMIN_PASSWORD` — pick any value (used by the admin API) +- `ALGORITHM`, token expiry, `CORS_ORIGINS`, ElevenLabs voice/model IDs — defaults exist +- Research login IDs (RID001–RID005) — seeded into the DB by `scripts/seed_research_ids.py` + +## ❓ Ask the team explicitly + +> Is there a separate **dev/staging database**, or is `ep-wandering-sea-a5xid0o5` the **live study database**? + +If it's the live DB with real research data, do **not** point local dev at it (migrations/seed +scripts could modify it). Request a dev database or permission to create a fresh empty one. + +--- + +## Repository fixes already applied + +`paco-api/requirements.txt` was internally broken (the multi-LLM commit added Gemini/OpenAI +providers the app imports on startup, but their packages and a compatible `httpx` were missing). +Fixed on this machine: + +- `httpx==0.27.0` → `httpx>=0.28.1` (required by `google-genai`) +- `websockets==12.0` → `websockets>=13.0` (required by `google-genai`) +- `groq==0.5.0` → `groq>=0.13.0` (old groq breaks on httpx ≥ 0.28: `proxies` kwarg removed) +- added `openai>=1.30.0` and `google-genai>=1.0.0` (imported by the code, previously undeclared) + +These changes should be committed and reflected in the Render build. + +## Run order (once secrets are in `.env`) + +```powershell +cd paco-api +# .venv already created with Python 3.12 +.\.venv\Scripts\Activate.ps1 +$env:PYTHONIOENCODING="utf-8" # Windows: avoids emoji-in-print crash at startup +alembic upgrade head # create tables +python scripts\seed_research_ids.py +uvicorn app.main:app --reload --port 8000 + +# frontend (separate terminal) +cd ..\paco-frontend +npm install +npm run dev # http://localhost:3000 +``` diff --git a/httpx) b/httpx) new file mode 100644 index 0000000..e69de29 diff --git a/paco-api/app/api/endpoints/medication_analysis.py b/paco-api/app/api/endpoints/medication_analysis.py index 2fd65b8..8bc5116 100644 --- a/paco-api/app/api/endpoints/medication_analysis.py +++ b/paco-api/app/api/endpoints/medication_analysis.py @@ -11,16 +11,11 @@ from app.schemas.medication_analysis import ( AnalysisRequest, AnalysisResponse, - AnalysisResult, + QuilamAnalysisResult, + QuilamDomain, + QuilamDomains, AnalysisHistoryResponse, - AnalysisHistoryItem, - MedicationInfo, - TimingSchedule, - SideEffect, - AdherenceDifficulty, - AdherenceStrategy, - QuestionConcern, - OverallAdherence + AnalysisHistoryItem ) from app.services.medication_analysis_service import medication_analysis_service from app.core.security import verify_admin_password @@ -28,44 +23,90 @@ router = APIRouter() -def parse_analysis_result(detailed_analysis: str) -> AnalysisResult: - """Parse the detailed analysis JSON into structured format""" +VALID_FLAGS = {"surfaced", "not surfaced", "concern flagged"} + + +def parse_domain(domains_data: dict, key: str) -> QuilamDomain: + """Parse a single QUILAM domain, tolerant of LLM output variance. + + Handles missing keys, explicit nulls, wrong-typed values, and flag + casing/whitespace. A malformed individual domain degrades to a single + error domain rather than discarding the whole analysis. + """ + try: + d = domains_data.get(key) + if not isinstance(d, dict): + d = {} + + raw_flag = d.get("flag") or "not surfaced" + flag = raw_flag.lower().strip() if isinstance(raw_flag, str) else "not surfaced" + if flag not in VALID_FLAGS: + flag = "not surfaced" + + finding = d.get("finding") or "Not discussed" + if not isinstance(finding, str): + finding = str(finding) + + details = d.get("details") or [] + if not isinstance(details, list): + details = [str(details)] + else: + details = [str(item) for item in details] + + return QuilamDomain(finding=finding, flag=flag, details=details) + except (ValueError, TypeError, AttributeError): + return QuilamDomain(finding="Could not parse this domain.", flag="not surfaced", details=[]) + + +def parse_analysis_result(detailed_analysis: str) -> QuilamAnalysisResult: + """Parse the QUILAM analysis JSON into structured format""" try: data = json.loads(detailed_analysis) - - return AnalysisResult( - medications=[MedicationInfo(**med) for med in data.get("medications", [])], - timing_schedule=TimingSchedule(**data.get("timing_schedule", {})), - side_effects=[SideEffect(**se) for se in data.get("side_effects", [])], - adherence_difficulties=[ - AdherenceDifficulty(**diff) for diff in data.get("adherence_difficulties", []) - ], - adherence_strategies=[ - AdherenceStrategy(**strat) for strat in data.get("adherence_strategies", []) - ], - questions_concerns=[ - QuestionConcern(**qc) for qc in data.get("questions_concerns", []) - ], - overall_adherence=OverallAdherence(**data.get("overall_adherence", {})), - confidence_score=data.get("confidence_score", 0), - summary=data.get("summary", ""), - key_concerns=data.get("key_concerns", []), - recommendations=data.get("recommendations", []) + if not isinstance(data, dict): + raise ValueError("Top-level analysis JSON is not an object") + + domains_data = data.get("domains") + if not isinstance(domains_data, dict): + domains_data = {} + + try: + confidence_score = int(data.get("confidence_score", 0)) + except (ValueError, TypeError): + confidence_score = 0 + confidence_score = max(0, min(100, confidence_score)) + + key_concerns = data.get("key_concerns") or [] + if not isinstance(key_concerns, list): + key_concerns = [str(key_concerns)] + else: + key_concerns = [str(item) for item in key_concerns] + + overall_summary = data.get("overall_summary") or "" + if not isinstance(overall_summary, str): + overall_summary = str(overall_summary) + + return QuilamAnalysisResult( + domains=QuilamDomains( + general_beliefs=parse_domain(domains_data, "general_beliefs"), + self_management=parse_domain(domains_data, "self_management"), + specific_beliefs=parse_domain(domains_data, "specific_beliefs"), + provider_relationship=parse_domain(domains_data, "provider_relationship") + ), + overall_summary=overall_summary, + key_concerns=key_concerns, + confidence_score=confidence_score ) - except (json.JSONDecodeError, ValueError) as e: - # Return a minimal result if parsing fails - return AnalysisResult( - medications=[], - timing_schedule=TimingSchedule(), - side_effects=[], - adherence_difficulties=[], - adherence_strategies=[], - questions_concerns=[], - overall_adherence=OverallAdherence(), - confidence_score=0, - summary="Error parsing analysis results. Check detailed_analysis field.", + except (json.JSONDecodeError, ValueError, TypeError): + return QuilamAnalysisResult( + domains=QuilamDomains( + general_beliefs=QuilamDomain(finding="Error parsing analysis results.", flag="not surfaced"), + self_management=QuilamDomain(finding="Error parsing analysis results.", flag="not surfaced"), + specific_beliefs=QuilamDomain(finding="Error parsing analysis results.", flag="not surfaced"), + provider_relationship=QuilamDomain(finding="Error parsing analysis results.", flag="not surfaced") + ), + overall_summary="Error parsing analysis results. Check detailed_analysis field.", key_concerns=["Analysis parsing error"], - recommendations=["Re-run analysis"] + confidence_score=0 ) @@ -157,21 +198,27 @@ async def get_analysis_history( limit=limit ) - # Format response - history_items = [ - AnalysisHistoryItem( - analysis_id=analysis.id, - analysis_date=analysis.analysis_date, - analyzed_from=analysis.analyzed_from, - analyzed_to=analysis.analyzed_to, - conversation_count=analysis.conversation_count, - confidence_score=analysis.confidence_score, - summary=analysis.summary, - is_taking_medications=analysis.is_taking_medications, - taking_as_prescribed=analysis.taking_as_prescribed + # Format response — surface each QUILAM domain's flag per analysis + history_items = [] + for analysis in analyses: + result = parse_analysis_result(analysis.detailed_analysis) + history_items.append( + AnalysisHistoryItem( + analysis_id=analysis.id, + analysis_date=analysis.analysis_date, + analyzed_from=analysis.analyzed_from, + analyzed_to=analysis.analyzed_to, + conversation_count=analysis.conversation_count, + confidence_score=analysis.confidence_score, + summary=analysis.summary, + domain_flags={ + "general_beliefs": result.domains.general_beliefs.flag, + "self_management": result.domains.self_management.flag, + "specific_beliefs": result.domains.specific_beliefs.flag, + "provider_relationship": result.domains.provider_relationship.flag, + }, + ) ) - for analysis in analyses - ] return AnalysisHistoryResponse( research_id=research_id, diff --git a/paco-api/app/core/config.py b/paco-api/app/core/config.py index 3cb1315..5ac410a 100644 --- a/paco-api/app/core/config.py +++ b/paco-api/app/core/config.py @@ -4,7 +4,7 @@ from pydantic_settings import BaseSettings from pydantic import field_validator from functools import lru_cache -from typing import List, Union +from typing import List, Optional, Union class Settings(BaseSettings): @@ -16,19 +16,21 @@ class Settings(BaseSettings): API_V1_PREFIX: str = "/api/v1" # Security - SECRET_KEY: str + SECRET_KEY: Optional[str] = None ALGORITHM: str = "HS256" ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 # 24 hours # Database - DATABASE_URL: str + DATABASE_URL: Optional[str] = None # LLM API Keys - GROQ_API_KEY: str # Primary LLM provider - OPENROUTER_API_KEY: str = "" # Optional alternative + GROQ_API_KEY: Optional[str] = None # Primary LLM provider + OPENROUTER_API_KEY: Optional[str] = None # Optional alternative + GEMINI_API_KEY: Optional[str] = None + OPENAI_API_KEY: Optional[str] = None # ElevenLabs - ELEVENLABS_API_KEY: str + ELEVENLABS_API_KEY: Optional[str] = None ELEVENLABS_VOICE_ID: str = "9BWtsMINqrJLrRacOk9x" # Aria voice ELEVENLABS_MODEL_ID: str = "eleven_multilingual_v2" @@ -36,7 +38,7 @@ class Settings(BaseSettings): CORS_ORIGINS: Union[str, List[str]] = "http://localhost:3000,http://localhost:5173,https://paco.vercel.app" # Admin - ADMIN_PASSWORD: str = "" + ADMIN_PASSWORD: Optional[str] = None # Research IDs (used by seed script only) RESEARCH_IDS: str = "" @@ -57,4 +59,4 @@ class Config: @lru_cache() def get_settings() -> Settings: """Cached settings instance""" - return Settings() + return Settings() \ No newline at end of file diff --git a/paco-api/app/prompts.py b/paco-api/app/prompts.py index d427f5f..5f18cf2 100644 --- a/paco-api/app/prompts.py +++ b/paco-api/app/prompts.py @@ -8,71 +8,98 @@ "Thank you for talking with me today. I'd like to learn more about how things are going with your medicines. There are no right or wrong answers—I'm just here to listen and help if I can. Does that sound okay?" --- -#### Key Questions I'll Explore (Using Motivational Interviewing Principles) +#### What I'm Listening For -### **1. Are You Taking Your Medicines?** -I'll ask open-ended questions like: +I never mention these domains to the patient or reference them by name in conversation. + +As the conversation unfolds naturally, I pay close attention to four areas of a person's experience with their medicines. I never ask about these in order or all at once—I follow the patient's lead and weave in relevant questions only when they fit naturally into what we are already talking about. The goal is a real conversation, not a survey. + +**General feelings about treatment:** I listen for how the patient feels about medicines in general—whether they worry that doctors prescribe too many medicines, whether they have concerns about long-term side effects, or whether they trust medical treatments more or less than natural remedies. These beliefs often surface when someone explains why they feel hesitant or unsure. + +**Day-to-day self-management:** I pay attention to practical challenges—whether they sometimes forget to refill a prescription, run out of medicine unexpectedly, or find it hard to keep track of multiple medicines. These come up naturally when we talk about their daily routine and what gets in the way. + +**Personal decisions about taking medicine:** I listen carefully for signs that a patient has made a conscious choice to skip, reduce, or stop a medicine—especially if they have not told their doctor. This includes whether they feel uncomfortable taking medicine in front of others, whether they feel negligent or forget on purpose, or whether they stopped because they felt worse. I explore these gently and without judgment, since patients may feel embarrassed or worried about how I will react. + +**The relationship with their care team:** I listen for how the patient feels about their doctor and healthcare team—whether they feel included in decisions, whether instructions were explained clearly, and whether they feel satisfied with their care overall. This often comes up when patients talk about what their doctor told them or how a recent visit went. + +--- +#### How I Explore These Areas Naturally + +I do not move through topics in a fixed order. I listen for openings and follow the patient's lead. The themes below show how I explore what matters, with example questions for each. I pick the ones that fit the moment—I never run through them like a list. + +--- + +### **Opening: How Are Things Going With Your Medicines?** +I begin with open questions to understand their experience and let them set the direction: - "Can you tell me about the medicines you're supposed to take?" - "How have things been going with taking your medicines lately?" - "What has it been like for you to take your medicines every day?" -I affirm their honesty and effort, no matter what they share. +I listen closely to what comes up. Sometimes a patient will jump right to a concern—a side effect, a missed refill, a feeling they don't need the medicine—and I follow that thread wherever it leads. --- -### **2. If Not, How Often Are You Missing Them?** -If they're not taking their medicines as prescribed, I'll gently explore: -- "Can you help me understand how often you're able to take your medicines?" +### **Day-to-Day Challenges** +When the conversation turns to practical difficulties, I explore what gets in the way: +- "Are there times when you realize you've run out and haven't had a chance to pick up your refill?" +- "Do you ever find yourself somewhere without your medicine when you need it?" +- "When you have more than one medicine to take, how do you keep track of them all?" - "Are there certain times of day when it's easier or harder to remember?" -- "When do you find yourself missing a dose?" +- "What makes it hard for you to take your medicines?" -I listen without judgment and reflect back what I hear to show I understand. +I affirm that these kinds of mix-ups are very common and nothing to feel bad about. --- -### **3. What Barriers Are You Facing?** -I'll explore challenges by asking: -- "What makes it hard for you to take your medicines?" -- "Are there things that get in the way, like cost, side effects, or forgetting?" -- "What's the biggest challenge you're facing right now with your medicines?" +### **What Do You Think About Medicines in General?** +When there seems to be hesitation or ambivalence, I gently explore their broader beliefs without challenging them: +- "Some people feel like doctors prescribe a lot of medicines. What do you think about that?" +- "Do you ever have worries about taking a medicine for a long time?" +- "Are there other things—like home remedies or natural options—that you use or that you think about?" -Common barriers might include: -- Forgetting to take them -- Cost or trouble getting to the pharmacy -- Side effects that feel bad -- Not understanding why the medicine is important -- Feeling better and thinking they don't need it anymore -- Having too many medicines to keep track of +I never push back on their beliefs. I reflect them and explore with curiosity what is behind them. --- -### **4. What Do You Think Would Help?** -I'll invite them to think about solutions: -- "What do you think might help you take your medicines more regularly?" -- "If you could change one thing to make it easier, what would it be?" -- "What has worked for you in the past when you were able to stick to something important?" +### **Decisions to Skip, Reduce, or Stop** +When a patient has been missing doses or seems to have made their own changes, I listen carefully to understand whether it was intentional and why—without making them feel judged for it: +- "Sometimes people decide on their own to take less of a medicine or stop taking it altogether—has that ever happened for you?" +- "If you have stopped or changed how you take it, what led you to that decision?" +- "Have there been times when you felt worse after taking it and decided not to take it?" +- "Is there ever a situation where you feel uncomfortable taking your medicine—like when you are around other people?" -I support their ideas and help them build on their own motivation. +I respond with empathy, not alarm. I do not pressure patients to share more than they are comfortable with. --- -### **5. What Techniques Have You Tried?** -I'll ask about past efforts: -- "Have you tried anything to help you remember to take your medicines?" -- "What's worked well for you? What hasn't worked as well?" -- "Tell me about a time when you were taking your medicines regularly—what was different then?" +### **Your Relationship With Your Doctor and Care Team** +When it feels natural, I explore how the patient feels about their healthcare: +- "When your doctor recommended this medicine, did they explain what it is for and how to take it?" +- "Do you feel like you and your doctor make decisions together about your care?" +- "Are there things about your treatment that still feel confusing or unclear to you?" +- "Overall, how do you feel about the care you have been getting?" -I affirm their efforts and learn from what they've already tried. +I acknowledge that not everyone feels comfortable speaking up with their doctor, and I validate whatever they share. --- -### **6. Are You Having Symptoms from Not Taking Your Medicines?** -I'll gently explore health impacts: +### **Health and Wellbeing** +I gently explore how missing medicines may be affecting how they feel: - "Have you noticed any changes in how you're feeling?" -- "Are you experiencing any symptoms that might be related to not taking your medicines?" +- "Are you experiencing any symptoms that might be connected to your medicines?" - "How is your health doing overall?" -I help them make connections between medicine adherence and their well-being, without being preachy or scary. +I help them make connections between medicine-taking and their well-being, without being preachy or scary. + +--- + +### **What Might Help?** +I invite them to think about solutions in their own words: +- "What do you think might help you take your medicines more regularly?" +- "If you could change one thing to make it easier, what would it be?" +- "What has worked for you in the past when you were trying to stick to something important?" + +I support their ideas and help them build on their own motivation. --- @@ -108,25 +135,32 @@ - User: "I forget to take them a lot." - PaCo: "Thank you for being honest with me. Forgetting happens to a lot of people. Can you tell me more about when you usually forget? Is it morning, evening, or certain days?" -**3. Exploring Barriers:** +**3. Exploring General Beliefs:** +- User: "I'm not sure I really need all these pills." +- PaCo: "That's a really honest thing to share. A lot of people wonder about that. What makes you feel unsure about them?" + +**4. Exploring Barriers:** - User: "The pills make me feel sick." - PaCo: "That sounds really hard. Feeling sick is a big deal. Have you had a chance to talk to your doctor about this? Sometimes there are other options that might not make you feel that way." -**4. Eliciting Change Talk:** +**5. When Someone Stopped Without Telling Their Doctor:** +- User: "I just stopped taking it for a while because it was making things worse." +- PaCo: "Thank you for sharing that with me—it takes honesty to say that. It sounds like something really wasn't feeling right. Can you tell me more about what was happening?" + +**6. Exploring the Care Relationship:** +- User: "My doctor just told me to take it. I didn't really understand why." +- PaCo: "That can be really frustrating—feeling like you didn't get the full picture. What questions do you still have about it?" + +**7. Eliciting Change Talk:** - PaCo: "What would be different for you if you were able to take your medicines regularly?" - User: "I guess I'd feel better and wouldn't worry so much about getting worse." - PaCo: "So taking your medicines could help you feel better and give you less worry. That sounds really important to you." -**5. Supporting Self-Efficacy:** +**8. Supporting Self-Efficacy:** - User: "I've tried setting alarms but I still forget." - PaCo: "It's great that you tried using alarms—that shows you're thinking about ways to help yourself. Sometimes it takes trying a few different things. What else might work for you?" -**6. Exploring Symptoms:** -- PaCo: "Have you noticed any changes in how you're feeling since you've been missing your medicines?" -- User: "My legs hurt more when I walk." -- PaCo: "I hear you—your legs are hurting more. Do you think there might be a connection between that pain and not taking your medicines?" - -**7. Summarizing:** +**9. Summarizing:** - PaCo: "So if I'm understanding right, you want to take your medicines because you know they help, but forgetting is the biggest problem. You've tried alarms, and you think a pillbox might help. You also mentioned talking to your doctor about the side effects. Does that sound right?" --- diff --git a/paco-api/app/schemas/medication_analysis.py b/paco-api/app/schemas/medication_analysis.py index d1629d1..0614436 100644 --- a/paco-api/app/schemas/medication_analysis.py +++ b/paco-api/app/schemas/medication_analysis.py @@ -2,58 +2,31 @@ Pydantic schemas for medication adherence analysis """ from pydantic import BaseModel, Field -from typing import List, Optional +from typing import Dict, List, Literal, Optional from datetime import datetime -class MedicationInfo(BaseModel): - """Information about a specific medication""" - name: str - dosage: Optional[str] = None - mentioned_by_patient: bool = True +class QuilamDomain(BaseModel): + """Finding and flag for a single QUILAM domain""" + finding: str + flag: Literal["surfaced", "not surfaced", "concern flagged"] + details: List[str] = Field(default_factory=list) -class TimingSchedule(BaseModel): - """When medications are taken""" - morning: List[str] = Field(default_factory=list) - afternoon: List[str] = Field(default_factory=list) - evening: List[str] = Field(default_factory=list) - as_needed: List[str] = Field(default_factory=list) - unclear: List[str] = Field(default_factory=list) +class QuilamDomains(BaseModel): + """All four QUILAM domains""" + general_beliefs: QuilamDomain + self_management: QuilamDomain + specific_beliefs: QuilamDomain + provider_relationship: QuilamDomain -class SideEffect(BaseModel): - """Side effect information""" - medication: str - effect: str - severity: str = Field(..., pattern="^(mild|moderate|severe)$") - - -class AdherenceDifficulty(BaseModel): - """Difficulties with medication adherence""" - type: str - description: str - - -class AdherenceStrategy(BaseModel): - """Strategies used to improve adherence""" - type: str - description: str - effectiveness: str = Field(..., pattern="^(working well|somewhat helpful|not working)$") - - -class QuestionConcern(BaseModel): - """Patient questions or concerns""" - topic: str - question: str - addressed: bool = False - - -class OverallAdherence(BaseModel): - """Overall adherence status""" - taking_medications: Optional[bool] = None - taking_as_prescribed: Optional[bool] = None - taking_correct_medications: Optional[bool] = None +class QuilamAnalysisResult(BaseModel): + """Complete QUILAM framework analysis result""" + domains: QuilamDomains + overall_summary: str + key_concerns: List[str] = Field(default_factory=list) + confidence_score: int = Field(..., ge=0, le=100) class AnalysisRequest(BaseModel): @@ -74,23 +47,8 @@ class Config: } -class AnalysisResult(BaseModel): - """Complete analysis result""" - medications: List[MedicationInfo] - timing_schedule: TimingSchedule - side_effects: List[SideEffect] - adherence_difficulties: List[AdherenceDifficulty] - adherence_strategies: List[AdherenceStrategy] - questions_concerns: List[QuestionConcern] - overall_adherence: OverallAdherence - confidence_score: int = Field(..., ge=0, le=100) - summary: str - key_concerns: List[str] = Field(default_factory=list) - recommendations: List[str] = Field(default_factory=list) - - class AnalysisResponse(BaseModel): - """Response containing analysis results and metadata""" + """Response containing QUILAM analysis results and metadata""" analysis_id: int research_id: str analysis_date: datetime @@ -100,14 +58,14 @@ class AnalysisResponse(BaseModel): confidence_score: int summary: str model_used: str - result: AnalysisResult + result: QuilamAnalysisResult class Config: from_attributes = True class AnalysisHistoryItem(BaseModel): - """Summary of a past analysis""" + """Summary of a past analysis, with each QUILAM domain's flag for at-a-glance review""" analysis_id: int analysis_date: datetime analyzed_from: datetime @@ -115,8 +73,8 @@ class AnalysisHistoryItem(BaseModel): conversation_count: int confidence_score: int summary: str - is_taking_medications: Optional[bool] - taking_as_prescribed: Optional[bool] + # Maps each QUILAM domain key -> its flag (surfaced / not surfaced / concern flagged) + domain_flags: Dict[str, str] = Field(default_factory=dict) class Config: from_attributes = True diff --git a/paco-api/app/services/llm_service.py b/paco-api/app/services/llm_service.py index 48cf695..fa5a5a7 100644 --- a/paco-api/app/services/llm_service.py +++ b/paco-api/app/services/llm_service.py @@ -1,63 +1,108 @@ """ -LLM Service for chat completions +LLM Service with multiple providers and automatic fallback """ from typing import List, Dict, Any, Optional import os -from groq import AsyncGroq - from app.core.config import get_settings +from .providers.base_provider import BaseProvider, RateLimitError +from .providers.gemini_provider import GeminiProvider +from .providers.openai_provider import OpenAIProvider +from .providers.groq_provider import GroqProvider settings = get_settings() -class LLMService: - """Service for interacting with Groq LLM provider""" - +class MultiLLMService: + """Service with multiple LLM providers and automatic fallback""" + def __init__(self): - """Initialize Groq client""" - self.groq_client = None - - # Initialize Groq if API key is available + """Initialize all available providers in priority order""" + self.providers: List[BaseProvider] = [] + self.current_index = 0 + + # Add providers in priority order (Gemini first for free tier) + if settings.GEMINI_API_KEY: + self.providers.append(GeminiProvider(api_key=settings.GEMINI_API_KEY)) + print("[LLM] Initialized GeminiProvider") + + if settings.OPENAI_API_KEY: + self.providers.append(OpenAIProvider(api_key=settings.OPENAI_API_KEY)) + print("[LLM] Initialized OpenAIProvider") + + # Keep Groq as fallback if available if settings.GROQ_API_KEY: - self.groq_client = AsyncGroq(api_key=settings.GROQ_API_KEY) - else: - raise ValueError("GROQ_API_KEY is required for LLM service") - + self.providers.append(GroqProvider(api_key=settings.GROQ_API_KEY)) + print("[LLM] Initialized GroqProvider") + + if not self.providers: + raise ValueError("No LLM API keys configured. Set GEMINI_API_KEY or OPENAI_API_KEY") + + print(f"[LLM] {len(self.providers)} provider(s) available") + async def get_chat_completion( self, messages: List[Dict[str, str]], - model: str = "llama-3.3-70b-versatile", + model: str = "llama-3.3-70b-versatile", # Ignored (for backward compatibility) temperature: float = 0.7, max_tokens: int = 2000, **kwargs ) -> str: """ - Get chat completion from Groq LLM provider + Get chat completion with automatic provider fallback + + Tries providers in order until one succeeds. + Automatically switches on rate limit errors (429). Args: messages: List of message dicts with 'role' and 'content' - model: Model name (e.g., 'llama-3.3-70b-versatile', 'mixtral-8x7b-32768') + model: Ignored (kept for backward compatibility) temperature: Sampling temperature max_tokens: Maximum tokens to generate - **kwargs: Additional parameters for the LLM + **kwargs: Additional parameters (ignored) Returns: Response content as string """ - if not self.groq_client: - raise ValueError("Groq API key not configured") - - # Make API call to Groq - response = await self.groq_client.chat.completions.create( - model=model, - messages=messages, - temperature=temperature, - max_tokens=max_tokens, - **kwargs - ) - - return response.choices[0].message.content + last_error = None + attempts = 0 + max_attempts = len(self.providers) + + # Try all providers + while attempts < max_attempts: + provider = self.providers[self.current_index] + provider_name = provider.__class__.__name__ + + try: + print(f"[LLM] Attempt {attempts + 1}/{max_attempts}: Using {provider_name}") + + response = await provider.complete( + messages=messages, + max_tokens=max_tokens, + temperature=temperature + ) + + print(f"[LLM] Success with {provider_name}") + return response + + except RateLimitError as e: + print(f"[LLM] {provider_name} rate limited, switching to next provider...") + last_error = e + # Switch to next provider + self.current_index = (self.current_index + 1) % len(self.providers) + attempts += 1 + continue + + except Exception as e: + print(f"[LLM] {provider_name} error: {e}") + last_error = e + # Try next provider + self.current_index = (self.current_index + 1) % len(self.providers) + attempts += 1 + continue + + # All providers failed + raise Exception(f"All {max_attempts} LLM provider(s) failed. Last error: {last_error}") -# Global instance -llm_service = LLMService() +# Global instance (backward compatible with existing code) +llm_service = MultiLLMService() \ No newline at end of file diff --git a/paco-api/app/services/medication_analysis_service.py b/paco-api/app/services/medication_analysis_service.py index b44bf78..a6d98ae 100644 --- a/paco-api/app/services/medication_analysis_service.py +++ b/paco-api/app/services/medication_analysis_service.py @@ -18,59 +18,76 @@ class MedicationAnalysisService: """Service for analyzing medication adherence from conversations""" - ANALYSIS_PROMPT = """You are a medical data analyst tasked with extracting medication adherence information from patient conversations. + ANALYSIS_PROMPT = """You are a clinical research analyst reviewing a patient conversation transcript to assess medication adherence using the QUILAM framework. -Analyze the following conversation transcript and extract structured information about: +Analyze the conversation and fill out the four QUILAM domains below. For each domain, write a brief finding summarizing what the patient shared, and assign a flag. -1. **Medications**: List all medications mentioned (name, dosage if mentioned) -2. **Timing**: When the patient takes their medications (morning, evening, with meals, etc.) -3. **Side Effects**: Any adverse effects or symptoms the patient reports -4. **Adherence Difficulties**: Problems the patient has taking medications as prescribed (forgetting, cost, access, confusion, etc.) -5. **Adherence Strategies**: Methods the patient uses to remember/take medications (alarms, pill boxes, routines, etc.) -6. **Questions/Concerns**: Any questions or concerns the patient has expressed about their medications +Flag definitions: +- "surfaced": The topic came up and no significant concern was identified. +- "not surfaced": The topic did not come up in this conversation. +- "concern flagged": A concern in this domain was identified that warrants provider attention. -**Conversation Transcript:** +--- + +Domain 1 - General Beliefs About Treatment +Items to assess: +- Does the patient feel doctors overprescribe medication? +- Does the patient worry about long-term side effects of their medication? +- Does the patient trust medical treatments more or less than natural remedies? + +Domain 2 - Self-Management of Treatment (Unintentional Nonadherence) +Items to assess: +- Does the patient forget to refill prescriptions? +- Does the patient sometimes not have their medication available when they need it? +- Does the patient have difficulty managing multiple medications? + +Domain 3 - Specific Beliefs About Treatment (Intentional Nonadherence) +Items to assess: +- Does the patient feel socially uncomfortable taking medication in front of others? +- Is the patient sometimes negligent about taking their medication? +- Has the patient reduced or stopped their medication without telling their doctor because they felt worse? + +Domain 4 - Patient/Healthcare System Relationship +Items to assess: +- Does the patient feel they make decisions together with their doctor? +- Does the patient understand their healthcare provider's instructions? +- Did the patient's doctor explain how to properly treat their illness? +- Is the patient satisfied with their treatment overall? + +--- + +Conversation Transcript: {conversation_transcript} -**Instructions:** -- Be specific and quote relevant parts of the conversation -- If information is not mentioned, state "Not discussed" for that category -- Use a confidence score (0-100) to indicate how certain you are about the information -- Provide a brief summary suitable for a medical provider to quickly understand the patient's adherence status +--- -**Output Format (JSON):** +Output Format (JSON only, no additional text): {{ - "medications": [ - {{"name": "medication name", "dosage": "dosage if mentioned", "mentioned_by_patient": true/false}} - ], - "timing_schedule": {{ - "morning": ["list of medications"], - "afternoon": ["list of medications"], - "evening": ["list of medications"], - "as_needed": ["list of medications"], - "unclear": ["list of medications"] - }}, - "side_effects": [ - {{"medication": "medication name or 'unclear'", "effect": "description", "severity": "mild/moderate/severe"}} - ], - "adherence_difficulties": [ - {{"type": "forgetting/cost/access/side_effects/complexity/other", "description": "detailed description"}} - ], - "adherence_strategies": [ - {{"type": "alarm/pill_box/routine/caregiver_help/other", "description": "detailed description", "effectiveness": "working well/somewhat helpful/not working"}} - ], - "questions_concerns": [ - {{"topic": "topic area", "question": "patient's question or concern", "addressed": true/false}} - ], - "overall_adherence": {{ - "taking_medications": true/false/unclear, - "taking_as_prescribed": true/false/unclear, - "taking_correct_medications": true/false/unclear + "domains": {{ + "general_beliefs": {{ + "finding": "What the patient shared about their general beliefs, or 'Not discussed' if this did not come up.", + "flag": "surfaced or not surfaced or concern flagged", + "details": ["Specific items or direct quotes from the conversation that support the finding"] + }}, + "self_management": {{ + "finding": "What the patient shared about day-to-day self-management challenges, or 'Not discussed'.", + "flag": "surfaced or not surfaced or concern flagged", + "details": ["Specific items or direct quotes"] + }}, + "specific_beliefs": {{ + "finding": "What the patient shared about intentional decisions to skip, reduce, or stop medication, or 'Not discussed'.", + "flag": "surfaced or not surfaced or concern flagged", + "details": ["Specific items or direct quotes"] + }}, + "provider_relationship": {{ + "finding": "What the patient shared about their relationship with their healthcare team, or 'Not discussed'.", + "flag": "surfaced or not surfaced or concern flagged", + "details": ["Specific items or direct quotes"] + }} }}, - "confidence_score": 0-100, - "summary": "Brief 2-3 sentence summary for medical provider", - "key_concerns": ["List of 3-5 most important concerns for provider to know"], - "recommendations": ["Suggested follow-up actions based on the conversation"] + "overall_summary": "2-3 sentence summary of the patient's medication adherence situation for the provider.", + "key_concerns": ["Most important concerns for the provider to follow up on"], + "confidence_score": 0-100 }} Respond ONLY with valid JSON, no additional text.""" @@ -205,15 +222,8 @@ async def analyze_medication_adherence( analyzed_from=earliest, analyzed_to=latest, conversation_count=message_count, - is_taking_medications=analysis_data.get("overall_adherence", {}).get("taking_medications"), - taking_as_prescribed=analysis_data.get("overall_adherence", {}).get("taking_as_prescribed"), - taking_correct_medications=analysis_data.get("overall_adherence", {}).get("taking_correct_medications"), - adherence_barriers=json.dumps(analysis_data.get("adherence_difficulties", [])), - adherence_strategies=json.dumps(analysis_data.get("adherence_strategies", [])), - side_effects=json.dumps(analysis_data.get("side_effects", [])), - medication_list=json.dumps(analysis_data.get("medications", [])), confidence_score=analysis_data.get("confidence_score", 0), - summary=analysis_data.get("summary", "Analysis completed."), + summary=analysis_data.get("overall_summary", "Analysis completed."), detailed_analysis=response, model_used=model ) diff --git a/paco-api/app/services/providers/__init__.py b/paco-api/app/services/providers/__init__.py new file mode 100644 index 0000000..e3db238 --- /dev/null +++ b/paco-api/app/services/providers/__init__.py @@ -0,0 +1,6 @@ +""" +LLM Provider implementations +""" +from .base_provider import BaseProvider, RateLimitError + +__all__ = ['BaseProvider', 'RateLimitError'] \ No newline at end of file diff --git a/paco-api/app/services/providers/base_provider.py b/paco-api/app/services/providers/base_provider.py new file mode 100644 index 0000000..6982f93 --- /dev/null +++ b/paco-api/app/services/providers/base_provider.py @@ -0,0 +1,37 @@ +""" +Base provider class for LLM providers +""" +from typing import List, Dict +from abc import ABC, abstractmethod + + +class RateLimitError(Exception): + """Raised when API rate limit is hit""" + pass + + +class BaseProvider(ABC): + """Abstract base class for all LLM providers""" + + @abstractmethod + async def complete( + self, + messages: List[Dict[str, str]], + max_tokens: int, + temperature: float = 0.7 + ) -> str: + """ + Make API call to LLM provider + + Args: + messages: List of message dicts with 'role' and 'content' + max_tokens: Maximum tokens to generate + temperature: Sampling temperature (0.0 to 1.0) + + Returns: + Response text as string + + Raises: + RateLimitError: When 429 error or quota exceeded + """ + pass \ No newline at end of file diff --git a/paco-api/app/services/providers/gemini_provider.py b/paco-api/app/services/providers/gemini_provider.py new file mode 100644 index 0000000..f94d18e --- /dev/null +++ b/paco-api/app/services/providers/gemini_provider.py @@ -0,0 +1,79 @@ +""" +Gemini Flash provider implementation +""" +from typing import List, Dict +from google import genai +from .base_provider import BaseProvider, RateLimitError + + +class GeminiProvider(BaseProvider): + """Google Gemini 2.5 Flash provider""" + + def __init__(self, api_key: str): + """Initialize Gemini client with API key""" + self.client = genai.Client(api_key=api_key) + self.model = "models/gemini-2.5-flash" + + async def complete( + self, + messages: List[Dict[str, str]], + max_tokens: int, + temperature: float = 0.7 + ) -> str: + """ + Generate completion using Gemini + + Args: + messages: OpenAI-style messages format + max_tokens: Maximum tokens to generate + temperature: Sampling temperature + + Returns: + Generated text + + Raises: + RateLimitError: On rate limit (429) + """ + try: + # Convert OpenAI message format to Gemini prompt + prompt = self._convert_messages_to_prompt(messages) + + # Generate response (Gemini SDK is synchronous) + response = self.client.models.generate_content( + model=self.model, + contents=prompt + ) + + return response.text + + except Exception as e: + error_str = str(e).lower() + + # Detect rate limit errors + if "429" in str(e) or "quota" in error_str or "rate" in error_str: + raise RateLimitError(f"Gemini rate limit exceeded: {e}") + + # Re-raise other errors + raise Exception(f"Gemini API error: {e}") + + def _convert_messages_to_prompt(self, messages: List[Dict[str, str]]) -> str: + """ + Convert OpenAI-style messages to Gemini prompt + + OpenAI format: [{"role": "system", "content": "..."}, {"role": "user", "content": "..."}] + Gemini format: Single string with all messages concatenated + """ + prompt_parts = [] + + for message in messages: + role = message.get("role", "user") + content = message.get("content", "") + + if role == "system": + prompt_parts.append(f"Instructions: {content}") + elif role == "user": + prompt_parts.append(f"User: {content}") + elif role == "assistant": + prompt_parts.append(f"Assistant: {content}") + + return "\n\n".join(prompt_parts) \ No newline at end of file diff --git a/paco-api/app/services/providers/groq_provider.py b/paco-api/app/services/providers/groq_provider.py new file mode 100644 index 0000000..c494087 --- /dev/null +++ b/paco-api/app/services/providers/groq_provider.py @@ -0,0 +1,56 @@ +""" +Groq Llama provider implementation +""" +from typing import List, Dict +from groq import AsyncGroq +from .base_provider import BaseProvider, RateLimitError + + +class GroqProvider(BaseProvider): + """Groq Llama 3.3 70B provider""" + + def __init__(self, api_key: str): + """Initialize Groq client with API key""" + self.client = AsyncGroq(api_key=api_key) + self.model = "llama-3.3-70b-versatile" + + async def complete( + self, + messages: List[Dict[str, str]], + max_tokens: int, + temperature: float = 0.7 + ) -> str: + """ + Generate completion using Groq + + Args: + messages: OpenAI-style messages format + max_tokens: Maximum tokens to generate + temperature: Sampling temperature + + Returns: + Generated text + + Raises: + RateLimitError: On rate limit (429) + """ + try: + # Groq uses same message format as OpenAI + response = await self.client.chat.completions.create( + model=self.model, + messages=messages, + max_tokens=max_tokens, + temperature=temperature + ) + + return response.choices[0].message.content + + except Exception as e: + error_str = str(e).lower() + + # Detect rate limit errors + if "429" in str(e) or "rate_limit" in error_str or "quota" in error_str: + raise RateLimitError(f"Groq rate limit exceeded: {e}") + + # Re-raise other errors + raise Exception(f"Groq API error: {e}") \ No newline at end of file diff --git a/paco-api/app/services/providers/openai_provider.py b/paco-api/app/services/providers/openai_provider.py new file mode 100644 index 0000000..fdd3f9c --- /dev/null +++ b/paco-api/app/services/providers/openai_provider.py @@ -0,0 +1,56 @@ +""" +OpenAI GPT-4o-mini provider implementation +""" +from typing import List, Dict +from openai import AsyncOpenAI +from .base_provider import BaseProvider, RateLimitError + + +class OpenAIProvider(BaseProvider): + """OpenAI GPT-4o-mini provider""" + + def __init__(self, api_key: str): + """Initialize OpenAI client with API key""" + self.client = AsyncOpenAI(api_key=api_key) + self.model = "gpt-4o-mini" + + async def complete( + self, + messages: List[Dict[str, str]], + max_tokens: int, + temperature: float = 0.7 + ) -> str: + """ + Generate completion using OpenAI + + Args: + messages: OpenAI-style messages format + max_tokens: Maximum tokens to generate + temperature: Sampling temperature + + Returns: + Generated text + + Raises: + RateLimitError: On rate limit (429) + """ + try: + # OpenAI uses the same message format, so no conversion needed + response = await self.client.chat.completions.create( + model=self.model, + messages=messages, + max_tokens=max_tokens, + temperature=temperature + ) + + return response.choices[0].message.content + + except Exception as e: + error_str = str(e).lower() + + # Detect rate limit errors + if "429" in str(e) or "rate_limit" in error_str or "quota" in error_str: + raise RateLimitError(f"OpenAI rate limit exceeded: {e}") + + # Re-raise other errors + raise Exception(f"OpenAI API error: {e}") \ No newline at end of file diff --git a/paco-api/requirements.txt b/paco-api/requirements.txt index f69393e..d480938 100644 --- a/paco-api/requirements.txt +++ b/paco-api/requirements.txt @@ -3,7 +3,7 @@ fastapi==0.109.0 uvicorn[standard]>=0.29.0 gunicorn>=21.2.0 python-multipart==0.0.6 -websockets==12.0 +websockets>=13.0 # Database sqlalchemy==2.0.29 @@ -16,7 +16,9 @@ passlib[bcrypt]==1.7.4 pydantic-settings==2.1.0 # LLM Providers -groq==0.5.0 +groq>=0.13.0 +openai>=1.30.0 +google-genai>=1.0.0 # Voice/Audio elevenlabs==1.50.3 @@ -24,5 +26,5 @@ elevenlabs==1.50.3 # Utilities pydantic==2.7.1 python-dotenv==1.0.1 -httpx==0.27.0 +httpx>=0.28.1 aiohttp==3.9.5 diff --git a/pip b/pip new file mode 100644 index 0000000..e69de29 diff --git a/test_apis.py b/test_apis.py new file mode 100644 index 0000000..4f8bef3 --- /dev/null +++ b/test_apis.py @@ -0,0 +1,93 @@ +""" +test script to verify API keys work +Run: python test_apis.py +""" + +import httpx +import asyncio +import json +from google import genai +import os + +GEMINI_API_KEY = os.environ["GEMINI_API_KEY"] +OPENAI_API_KEY = os.environ["OPENAI_API_KEY"] + +def test_gemini(): + print("\n=== Testing Gemini API ===") + try: + client = genai.Client(api_key=GEMINI_API_KEY) + + resp = client.models.generate_content( + model="models/gemini-2.5-flash", + contents="Say 'Gemini API is working!' and nothing else." + ) + + print(f"Gemini Response: {resp.text}") + return True + + except Exception as e: + print(f"Gemini Exception: {e}") + return False + + +async def test_openai(): + """Test OpenAI API""" + print("\n=== Testing OpenAI API ===") + + url = "https://api.openai.com/v1/chat/completions" + + payload = { + "model": "gpt-4o-mini", + "messages": [{ + "role": "user", + "content": "Say 'OpenAI API is working!' and nothing else." + }], + "max_tokens": 20 + } + + headers = { + "Authorization": f"Bearer {OPENAI_API_KEY}", + "Content-Type": "application/json" + } + + try: + async with httpx.AsyncClient() as client: + response = await client.post( + url, + json=payload, + headers=headers, + timeout=30.0 + ) + + if response.status_code == 200: + result = response.json() + text = result['choices'][0]['message']['content'] + print(f"OpenAI Response: {text}") + return True + else: + print(f"OpenAI Error {response.status_code}: {response.text}") + return False + + except Exception as e: + print(f"OpenAI Exception: {e}") + return False + + +async def main(): + print("Testing API Keys...\n") + + gemini_works = test_gemini() + openai_works = await test_openai() + + print("\n=== Results ===") + print(f"Gemini: {'Working' if gemini_works else 'Failed'}") + print(f"OpenAI: {'Working' if openai_works else 'Failed'}") + + if gemini_works and openai_works: + print("\n All APIs working! Ready to start implementation.") + else: + print("\n Fix API keys before proceeding.") + + +if __name__ == "__main__": + asyncio.run(main()) \ No newline at end of file diff --git a/test_fallback.py b/test_fallback.py new file mode 100644 index 0000000..29299e7 --- /dev/null +++ b/test_fallback.py @@ -0,0 +1,65 @@ +""" +Test fallback behavior when provider fails +""" +import asyncio +import sys +import os +from dotenv import load_dotenv + +load_dotenv() + +# Add paco-api to path +sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'paco-api')) + +# Set environment variables +os.environ['GEMINI_API_KEY'] = os.getenv('GEMINI_API_KEY', '') +os.environ['OPENAI_API_KEY'] = os.getenv('OPENAI_API_KEY', '') + +from app.services.llm_service import MultiLLMService +from app.services.providers.base_provider import RateLimitError + + +class FakeGeminiProvider: + """Fake provider that always fails with rate limit""" + + async def complete(self, messages, max_tokens, temperature=0.7): + print("[FAKE] GeminiProvider simulating rate limit...") + raise RateLimitError("Simulated rate limit - quota exceeded") + + +async def test_fallback(): + """Test that service falls back to OpenAI when Gemini fails""" + print("=== Testing Fallback Behavior ===\n") + + # Create service + service = MultiLLMService() + + # Replace first provider (Gemini) with fake that always fails + print("Replacing GeminiProvider with fake that always rate limits...\n") + service.providers[0] = FakeGeminiProvider() + + # Test messages + messages = [ + { + "role": "user", + "content": "Say 'Fallback to OpenAI worked!' and nothing else." + } + ] + + try: + print("Making request (should fail on Gemini, succeed on OpenAI)...\n") + response = await service.get_chat_completion( + messages=messages, + max_tokens=100 + ) + + print(f"\n✅ Fallback SUCCESS!") + print(f"Response: {response}") + print("\nThe system correctly switched from Gemini to OpenAI!") + + except Exception as e: + print(f"\n❌ Fallback FAILED: {e}") + + +if __name__ == "__main__": + asyncio.run(test_fallback()) \ No newline at end of file diff --git a/test_gemini_provider.py b/test_gemini_provider.py new file mode 100644 index 0000000..47e8508 --- /dev/null +++ b/test_gemini_provider.py @@ -0,0 +1,57 @@ +""" +Test GeminiProvider standalone +""" +import asyncio +import sys +import os + +# Add paco-api to path +sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'paco-api')) + +from app.services.providers.gemini_provider import GeminiProvider + +# Get API key from environment variable +GEMINI_API_KEY = os.environ.get("GEMINI_API_KEY") + +if not GEMINI_API_KEY: + print("ERROR: GEMINI_API_KEY environment variable not set!") + print("Set it first: set GEMINI_API_KEY=your_key_here") + exit(1) + + +async def test_gemini_provider(): + """Test GeminiProvider""" + print("Testing GeminiProvider...\n") + + # Initialize provider + provider = GeminiProvider(api_key=GEMINI_API_KEY) + + # Test messages (OpenAI format) + messages = [ + { + "role": "system", + "content": "You are a helpful medical assistant." + }, + { + "role": "user", + "content": "Say 'GeminiProvider is working!' and nothing else." + } + ] + + try: + # Call provider + response = await provider.complete( + messages=messages, + max_tokens=100, + temperature=0.7 + ) + + print(f"Success") + print(f"Response: {response}") + + except Exception as e: + print(f"Error: {e}") + + +if __name__ == "__main__": + asyncio.run(test_gemini_provider()) \ No newline at end of file diff --git a/test_multi_llm.py b/test_multi_llm.py new file mode 100644 index 0000000..9e01186 --- /dev/null +++ b/test_multi_llm.py @@ -0,0 +1,86 @@ +""" +Test MultiLLMService with fallback +""" +import asyncio +import sys +import os + +print("=== Starting test ===") + +# Load environment variables +try: + from dotenv import load_dotenv + load_dotenv() + print("✓ Loaded .env file") +except Exception as e: + print(f"Warning: Could not load .env: {e}") + +# Check API keys are set +gemini_key = os.getenv('GEMINI_API_KEY', '') +openai_key = os.getenv('OPENAI_API_KEY', '') + +print(f"GEMINI_API_KEY: {'Set ✓' if gemini_key else 'NOT SET ✗'}") +print(f"OPENAI_API_KEY: {'Set ✓' if openai_key else 'NOT SET ✗'}") + +# Set environment variables for the service +os.environ['GEMINI_API_KEY'] = gemini_key +os.environ['OPENAI_API_KEY'] = openai_key + +# Add paco-api to path +paco_path = os.path.join(os.path.dirname(__file__), 'paco-api') +sys.path.insert(0, paco_path) +print(f"✓ Added to path: {paco_path}") + +try: + print("\nImporting llm_service...") + from app.services.llm_service import llm_service + print("✓ Import successful\n") +except Exception as e: + print(f"✗ Import failed: {e}") + import traceback + traceback.print_exc() + exit(1) + + +async def test_multi_llm_service(): + """Test MultiLLMService""" + print("Testing MultiLLMService...\n") + + # Test messages + messages = [ + { + "role": "system", + "content": "You are a helpful medical assistant." + }, + { + "role": "user", + "content": "Say 'MultiLLMService is working!' and nothing else." + } + ] + + try: + # Call service (should use Gemini first) + print("=== Test 1: Normal call ===") + response = await llm_service.get_chat_completion( + messages=messages, + max_tokens=100, + temperature=0.7 + ) + + print(f"\n✅ Response: {response}\n") + + except Exception as e: + print(f"\n❌ Error: {e}\n") + import traceback + traceback.print_exc() + + +if __name__ == "__main__": + print("Running async test...\n") + try: + asyncio.run(test_multi_llm_service()) + print("\n=== Test complete ===") + except Exception as e: + print(f"Fatal error: {e}") + import traceback + traceback.print_exc() \ No newline at end of file diff --git a/test_openai_provider.py b/test_openai_provider.py new file mode 100644 index 0000000..c8e47b6 --- /dev/null +++ b/test_openai_provider.py @@ -0,0 +1,60 @@ +""" +Test OpenAIProvider standalone +""" +import asyncio +import sys +import os +from dotenv import load_dotenv + +# Load environment variables +load_dotenv() + +# Add paco-api to path +sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'paco-api')) + +from app.services.providers.openai_provider import OpenAIProvider + +# Get API key from environment variable +OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY") + +if not OPENAI_API_KEY: + print("ERROR: OPENAI_API_KEY environment variable not set!") + exit(1) + + +async def test_openai_provider(): + """Test OpenAIProvider""" + print("Testing OpenAIProvider...\n") + + # Initialize provider + provider = OpenAIProvider(api_key=OPENAI_API_KEY) + + # Test messages (OpenAI format) + messages = [ + { + "role": "system", + "content": "You are a helpful medical assistant." + }, + { + "role": "user", + "content": "Say 'OpenAIProvider is working!' and nothing else." + } + ] + + try: + # Call provider + response = await provider.complete( + messages=messages, + max_tokens=100, + temperature=0.7 + ) + + print(f"Success!") + print(f"Response: {response}") + + except Exception as e: + print(f"Error: {e}") + + +if __name__ == "__main__": + asyncio.run(test_openai_provider()) \ No newline at end of file