Deliver Step 9: agent-card.json, SKILL.md, USDA lookup, integrations dynamic config, Welcome/Integrations/MealPlan pages, API methods, README, routes

This commit is contained in:
Claude 2026-06-15 14:40:34 +00:00
parent 6fb5d759ef
commit c133a463ec
10 changed files with 751 additions and 33 deletions

View file

@ -153,3 +153,117 @@ async def disconnect(
if token:
await db.delete(token)
return {"status": "disconnected", "provider": provider}
# --- Dynamic Integration Config (v3) ---
from pydantic import BaseModel
from app.models.integration_config import IntegrationConfig
from app.services.provider_registry import PROVIDER_REGISTRY
from app.services.crypto import encrypt_value, decrypt_value
from app.config import settings
class ConfigureRequest(BaseModel):
provider: str
credentials: dict[str, str]
@router.get("/status")
async def full_integration_status(
user: Annotated[User, Depends(get_current_user)],
db: Annotated[AsyncSession, Depends(get_db)],
):
"""Return connection status for all providers. Never returns actual credentials."""
# OAuth tokens (Strava, Polar)
oauth_result = await db.execute(select(OAuthToken).where(OAuthToken.user_id == user.id))
oauth_tokens = {t.provider: True for t in oauth_result.scalars().all()}
# Dynamic config entries
config_result = await db.execute(
select(IntegrationConfig.provider)
.where(IntegrationConfig.user_id == user.id)
.distinct()
)
configured_providers = {row[0] for row in config_result.all()}
# Also check env vars for backward compatibility
env_providers = set()
if settings.strava_client_id:
env_providers.add("strava")
if settings.polar_client_id:
env_providers.add("polar")
if settings.telegram_bot_token:
env_providers.add("telegram")
if settings.groq_api_key:
env_providers.add("groq")
status = {}
for key, info in PROVIDER_REGISTRY.items():
if key in oauth_tokens:
status[key] = "connected"
elif key in configured_providers or key in env_providers:
status[key] = "configured"
else:
status[key] = "not_configured"
return status
@router.get("/providers")
async def list_providers():
"""Return the provider registry with setup instructions."""
result = {}
for key, info in PROVIDER_REGISTRY.items():
result[key] = {
"name": info["name"],
"type": info["type"],
"fields": info["fields"],
"help_url": info.get("help_url", ""),
"help_text": info.get("help_text", "").replace("{BASE_URL}", settings.base_url),
}
return result
@router.post("/configure")
async def configure_integration(
body: ConfigureRequest,
user: Annotated[User, Depends(get_current_user)],
db: Annotated[AsyncSession, Depends(get_db)],
):
"""Store encrypted integration credentials. Write-only — values can never be read back."""
if body.provider not in PROVIDER_REGISTRY:
raise HTTPException(400, f"Unknown provider: {body.provider}")
info = PROVIDER_REGISTRY[body.provider]
expected_fields = set(info["fields"])
provided_fields = set(body.credentials.keys())
missing = expected_fields - provided_fields
if missing:
raise HTTPException(400, f"Missing required fields: {', '.join(missing)}")
for key, value in body.credentials.items():
encrypted = encrypt_value(settings.secret_key, value)
# Upsert
existing = await db.execute(
select(IntegrationConfig).where(
IntegrationConfig.user_id == user.id,
IntegrationConfig.provider == body.provider,
IntegrationConfig.config_key == key,
)
)
row = existing.scalar_one_or_none()
if row:
row.config_value = encrypted
row.updated_at = datetime.now(timezone.utc)
else:
db.add(IntegrationConfig(
user_id=user.id,
provider=body.provider,
config_key=key,
config_value=encrypted,
))
return {"status": "configured", "provider": body.provider}

View file

@ -0,0 +1,59 @@
"""USDA FoodData Central lookup service.
Free API with 400K+ foods and research-grade nutrition data.
API key required (free, instant signup at https://fdc.nal.usda.gov/api-key-signup).
"""
from __future__ import annotations
import logging
import httpx
logger = logging.getLogger(__name__)
USDA_BASE = "https://api.nal.usda.gov/fdc/v1"
async def search_usda(query: str, api_key: str, page_size: int = 10) -> list[dict]:
"""Search USDA FoodData Central for food items.
Returns a list of dicts with: fdcId, description, brandName, calories,
protein_g, carbs_g, fat_g, serving_size.
"""
if not api_key:
return []
try:
async with httpx.AsyncClient(timeout=15) as client:
resp = await client.get(
f"{USDA_BASE}/foods/search",
params={
"api_key": api_key,
"query": query,
"pageSize": page_size,
"dataType": "Foundation,SR Legacy,Branded",
},
)
resp.raise_for_status()
data = resp.json()
results = []
for food in data.get("foods", []):
nutrients = {n["nutrientName"]: n["value"] for n in food.get("foodNutrients", [])}
results.append({
"fdcId": food.get("fdcId"),
"description": food.get("description", ""),
"brandName": food.get("brandName"),
"source": "usda",
"calories": nutrients.get("Energy"),
"protein_g": nutrients.get("Protein"),
"carbs_g": nutrients.get("Carbohydrate, by difference"),
"fat_g": nutrients.get("Total lipid (fat)"),
"fiber_g": nutrients.get("Fiber, total dietary"),
"serving_size": food.get("servingSize"),
"serving_unit": food.get("servingSizeUnit"),
})
return results
except Exception as e:
logger.warning(f"USDA search failed: {e}")
return []