From 976b345d72d7854975383679ea1fb011a20ed9d3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 21 Oct 2025 14:15:37 +0000 Subject: [PATCH 1/3] Initial plan From 0a00dd68ca8276b0d4594584487083212b9ae301 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 21 Oct 2025 14:23:26 +0000 Subject: [PATCH 2/3] Implement FastAPI endpoint with fitness validation and AI response generation Co-authored-by: lightningcell <117159961+lightningcell@users.noreply.github.com> --- .env.example | 1 + .gitignore | 42 +++++++++++ README.md | 135 ++++++++++++++++++++++++++++++++- example_usage.py | 105 ++++++++++++++++++++++++++ main.py | 189 +++++++++++++++++++++++++++++++++++++++++++++++ models.py | 28 +++++++ requirements.txt | 5 ++ test_app.py | 129 ++++++++++++++++++++++++++++++++ 8 files changed, 633 insertions(+), 1 deletion(-) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 example_usage.py create mode 100644 main.py create mode 100644 models.py create mode 100644 requirements.txt create mode 100644 test_app.py diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..3cac181 --- /dev/null +++ b/.env.example @@ -0,0 +1 @@ +OPENAI_API_KEY=your_openai_api_key_here diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e150c3a --- /dev/null +++ b/.gitignore @@ -0,0 +1,42 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# Virtual Environment +venv/ +ENV/ +env/ + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# Environment variables +.env +.env.local + +# Testing +.pytest_cache/ +.coverage +htmlcov/ diff --git a/README.md b/README.md index 0757a49..180696a 100644 --- a/README.md +++ b/README.md @@ -1 +1,134 @@ -# demo-structured-output \ No newline at end of file +# demo-structured-output + +FastAPI application for validating fitness-related user inputs and generating personalized AI responses. + +## Features + +- **Fitness Input Validation**: Uses OpenAI's structured output to determine if user input is fitness-related +- **Personalized AI Responses**: Generates tailored fitness advice based on user information +- **Two-step Processing**: + 1. Validates input relevance (fitness-related or not) + 2. Generates personalized response using user data + +## Requirements + +- Python 3.8+ +- OpenAI API key + +## Installation + +1. Clone the repository: +```bash +git clone https://github.com/lightningcell/demo-structured-output.git +cd demo-structured-output +``` + +2. Install dependencies: +```bash +pip install -r requirements.txt +``` + +3. Set up environment variables: +```bash +cp .env.example .env +# Edit .env and add your OpenAI API key +``` + +## Usage + +### Start the server + +```bash +python main.py +``` + +Or with uvicorn: +```bash +uvicorn main:app --reload +``` + +The API will be available at `http://localhost:8000` + +### API Endpoints + +#### POST /process-fitness-input + +Processes user fitness input with validation. + +**Request Body:** +```json +{ + "user_name": "John Doe", + "user_goals": ["lose weight", "build muscle", "improve stamina"], + "user_body_info": { + "weight": 76.8, + "height": 180, + "age": 30 + }, + "user_input": "I want to start a workout routine" +} +``` + +**Success Response (200):** +```json +{ + "response": "Based on your goals and current metrics...", + "fitness_score": 0.95 +} +``` + +**Error Response (400):** +```json +{ + "detail": { + "error": "Input is not fitness-related", + "details": "The provided input does not appear to be related to fitness, health, or wellness. Confidence score: 0.25" + } +} +``` + +### Example with curl + +```bash +curl -X POST "http://localhost:8000/process-fitness-input" \ + -H "Content-Type: application/json" \ + -d '{ + "user_name": "John Doe", + "user_goals": ["lose weight", "build muscle"], + "user_body_info": {"weight": 76.8, "height": 180}, + "user_input": "What exercises should I do to lose weight?" + }' +``` + +## API Documentation + +Once the server is running, visit: +- Swagger UI: `http://localhost:8000/docs` +- ReDoc: `http://localhost:8000/redoc` + +## Project Structure + +``` +. +├── main.py # FastAPI application with endpoints +├── models.py # Pydantic models for request/response +├── requirements.txt # Python dependencies +├── .env.example # Example environment variables +└── README.md # This file +``` + +## Implementation Details + +### Step 1: Fitness Validation +Uses OpenAI's structured output feature to parse responses into a `FitnessValidationResponse` model with: +- `is_fitness_related`: Boolean indicating if input is fitness-related +- `score`: Confidence score (0-1) for the classification + +### Step 2: Response Generation +If validation passes (score >= 0.5): +- User information (name, goals, body info) is included in the prompt +- OpenAI generates a personalized fitness response +- Response is returned to the client + +If validation fails: +- HTTP 400 error with details about why the input was rejected \ No newline at end of file diff --git a/example_usage.py b/example_usage.py new file mode 100644 index 0000000..8a15352 --- /dev/null +++ b/example_usage.py @@ -0,0 +1,105 @@ +""" +Example usage of the Fitness AI API + +This file demonstrates how to interact with the API endpoints. +""" + +import requests +import json + +# API base URL +BASE_URL = "http://localhost:8000" + + +def test_root_endpoint(): + """Test the root endpoint""" + print("Testing root endpoint...") + response = requests.get(f"{BASE_URL}/") + print(f"Status: {response.status_code}") + print(f"Response: {json.dumps(response.json(), indent=2)}\n") + + +def test_health_endpoint(): + """Test the health check endpoint""" + print("Testing health endpoint...") + response = requests.get(f"{BASE_URL}/health") + print(f"Status: {response.status_code}") + print(f"Response: {json.dumps(response.json(), indent=2)}\n") + + +def test_process_fitness_input_success(): + """Test the main endpoint with a fitness-related input""" + print("Testing process-fitness-input with fitness-related input...") + + payload = { + "user_name": "John Doe", + "user_goals": ["lose weight", "build muscle", "improve stamina"], + "user_body_info": { + "weight": 76.8, + "height": 180, + "age": 30, + "gender": "male" + }, + "user_input": "I want to start a workout routine to lose weight and build muscle" + } + + response = requests.post( + f"{BASE_URL}/process-fitness-input", + json=payload + ) + + print(f"Status: {response.status_code}") + print(f"Response: {json.dumps(response.json(), indent=2)}\n") + + +def test_process_fitness_input_failure(): + """Test the main endpoint with a non-fitness-related input""" + print("Testing process-fitness-input with non-fitness-related input...") + + payload = { + "user_name": "Jane Smith", + "user_goals": ["learn programming", "read more books"], + "user_body_info": { + "weight": 65.0, + "height": 170 + }, + "user_input": "What's the weather like today?" + } + + response = requests.post( + f"{BASE_URL}/process-fitness-input", + json=payload + ) + + print(f"Status: {response.status_code}") + print(f"Response: {json.dumps(response.json(), indent=2)}\n") + + +def main(): + """Run all example tests""" + print("=" * 70) + print("Fitness AI API - Example Usage") + print("=" * 70 + "\n") + + try: + test_root_endpoint() + test_health_endpoint() + test_process_fitness_input_success() + test_process_fitness_input_failure() + except requests.exceptions.ConnectionError: + print("ERROR: Could not connect to the API.") + print("Make sure the server is running with: python main.py") + return 1 + except Exception as e: + print(f"ERROR: {e}") + return 1 + + print("=" * 70) + print("Examples completed!") + print("=" * 70) + return 0 + + +if __name__ == "__main__": + import sys + sys.exit(main()) diff --git a/main.py b/main.py new file mode 100644 index 0000000..2bbe870 --- /dev/null +++ b/main.py @@ -0,0 +1,189 @@ +import os +from fastapi import FastAPI, HTTPException +from openai import OpenAI +from dotenv import load_dotenv +from models import ( + UserInput, + FitnessValidationResponse, + ErrorResponse, + SuccessResponse +) + +# Load environment variables +load_dotenv() + +app = FastAPI( + title="Fitness AI API", + description="API for validating fitness-related inputs and generating personalized responses", + version="1.0.0" +) + +# Initialize OpenAI client (lazy initialization to allow testing without API key) +client = None + + +def get_openai_client(): + """Get or initialize the OpenAI client""" + global client + if client is None: + api_key = os.getenv("OPENAI_API_KEY") + if not api_key: + raise HTTPException( + status_code=500, + detail="OpenAI API key is not configured. Please set OPENAI_API_KEY environment variable." + ) + client = OpenAI(api_key=api_key) + return client + + +def validate_fitness_input(user_input: str) -> FitnessValidationResponse: + """ + Step 1: Validate if the user input is fitness-related using OpenAI structured output. + + Args: + user_input: The user's text input to validate + + Returns: + FitnessValidationResponse with is_fitness_related boolean and confidence score + """ + try: + client = get_openai_client() + completion = client.beta.chat.completions.parse( + model="gpt-4o-2024-08-06", + messages=[ + { + "role": "system", + "content": """You are a fitness expert validator. Analyze the user input and determine if it's related to fitness, health, exercise, nutrition, or wellness. + +Return a boolean indicating if it's fitness-related and a confidence score between 0 and 1. +- Score 0.0-0.3: Definitely not fitness-related +- Score 0.3-0.6: Somewhat related or ambiguous +- Score 0.6-1.0: Clearly fitness-related""" + }, + { + "role": "user", + "content": f"Is this input fitness-related? Input: '{user_input}'" + } + ], + response_format=FitnessValidationResponse, + ) + + return completion.choices[0].message.parsed + except Exception as e: + # Default to considering it not fitness-related if there's an error + return FitnessValidationResponse(is_fitness_related=False, score=0.0) + + +def generate_ai_response(user_name: str, user_goals: list, user_body_info: dict, user_input: str) -> str: + """ + Step 2: Generate AI response using user information. + + Args: + user_name: User's name + user_goals: List of user goals + user_body_info: Dictionary of user body information + user_input: The validated user input + + Returns: + AI-generated response string + """ + # Construct the prompt with user information + user_context = f""" +User Information: +- Name: {user_name} +- Goals: {', '.join(user_goals)} +- Body Info: {', '.join([f'{k}: {v}' for k, v in user_body_info.items()])} + +User Input: {user_input} +""" + + try: + client = get_openai_client() + completion = client.chat.completions.create( + model="gpt-4o-2024-08-06", + messages=[ + { + "role": "system", + "content": """You are a professional fitness and wellness coach. Provide personalized advice based on the user's information, goals, and current body metrics. Be encouraging, specific, and actionable in your recommendations.""" + }, + { + "role": "user", + "content": user_context + } + ], + temperature=0.7, + max_tokens=500 + ) + + return completion.choices[0].message.content + except Exception as e: + raise HTTPException(status_code=500, detail=f"Error generating AI response: {str(e)}") + + +@app.post("/process-fitness-input", response_model=SuccessResponse) +async def process_fitness_input(user_data: UserInput): + """ + Main endpoint that processes user fitness input. + + Flow: + 1. Validate if the input is fitness-related + 2. If not fitness-related, return error + 3. If fitness-related, generate personalized AI response using user data + + Args: + user_data: UserInput model containing user name, goals, body info, and input + + Returns: + SuccessResponse with AI-generated response and fitness score + + Raises: + HTTPException: If input is not fitness-related or other errors occur + """ + # Step 1: Validate fitness input + validation_result = validate_fitness_input(user_data.user_input) + + # Step 2: Check validation result + if not validation_result.is_fitness_related or validation_result.score < 0.5: + raise HTTPException( + status_code=400, + detail={ + "error": "Input is not fitness-related", + "details": f"The provided input does not appear to be related to fitness, health, or wellness. Confidence score: {validation_result.score:.2f}" + } + ) + + # Step 3: Generate AI response with user information + ai_response = generate_ai_response( + user_name=user_data.user_name, + user_goals=user_data.user_goals, + user_body_info=user_data.user_body_info, + user_input=user_data.user_input + ) + + return SuccessResponse( + response=ai_response, + fitness_score=validation_result.score + ) + + +@app.get("/") +async def root(): + """Root endpoint with API information""" + return { + "message": "Fitness AI API", + "version": "1.0.0", + "endpoints": { + "POST /process-fitness-input": "Process and validate fitness-related user input" + } + } + + +@app.get("/health") +async def health_check(): + """Health check endpoint""" + return {"status": "healthy"} + + +if __name__ == "__main__": + import uvicorn + uvicorn.run(app, host="0.0.0.0", port=8000) diff --git a/models.py b/models.py new file mode 100644 index 0000000..4a0ee6b --- /dev/null +++ b/models.py @@ -0,0 +1,28 @@ +from pydantic import BaseModel, Field +from typing import List, Dict, Any + + +class UserInput(BaseModel): + """User input model for the fitness endpoint""" + user_name: str = Field(..., description="User's name") + user_goals: List[str] = Field(..., description="List of user goals") + user_body_info: Dict[str, Any] = Field(..., description="User body information (e.g., {weight: 76.8, height: 180, ...})") + user_input: str = Field(..., description="User's text input to validate and process") + + +class FitnessValidationResponse(BaseModel): + """Response model for fitness validation""" + is_fitness_related: bool = Field(..., description="Whether the input is fitness-related") + score: float = Field(..., description="Confidence score (0-1) for fitness relevance") + + +class ErrorResponse(BaseModel): + """Error response model""" + error: str = Field(..., description="Error message") + details: str = Field(default="", description="Additional error details") + + +class SuccessResponse(BaseModel): + """Success response model""" + response: str = Field(..., description="AI-generated response based on user data") + fitness_score: float = Field(..., description="Fitness relevance score") diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..00c9bd8 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +fastapi==0.104.1 +uvicorn[standard]==0.24.0 +pydantic==2.5.0 +openai==1.3.0 +python-dotenv==1.0.0 diff --git a/test_app.py b/test_app.py new file mode 100644 index 0000000..cb7c5e3 --- /dev/null +++ b/test_app.py @@ -0,0 +1,129 @@ +#!/usr/bin/env python3 +""" +Test script for the Fitness AI API +This script starts the server and tests the endpoints without requiring an OpenAI API key. +""" +import sys +import json + + +def test_models(): + """Test that models can be imported and instantiated""" + print("Testing models...") + try: + from models import UserInput, FitnessValidationResponse, ErrorResponse, SuccessResponse + + # Test UserInput + user_input = UserInput( + user_name="John Doe", + user_goals=["lose weight", "build muscle"], + user_body_info={"weight": 76.8, "height": 180}, + user_input="I want to start working out" + ) + print(f"✓ UserInput model works: {user_input.user_name}") + + # Test FitnessValidationResponse + validation = FitnessValidationResponse(is_fitness_related=True, score=0.95) + print(f"✓ FitnessValidationResponse model works: {validation.score}") + + # Test ErrorResponse + error = ErrorResponse(error="Test error", details="Test details") + print(f"✓ ErrorResponse model works: {error.error}") + + # Test SuccessResponse + success = SuccessResponse(response="Test response", fitness_score=0.95) + print(f"✓ SuccessResponse model works: {success.fitness_score}") + + print("All models work correctly!\n") + return True + except Exception as e: + print(f"✗ Error testing models: {e}") + return False + + +def test_app_structure(): + """Test that the FastAPI app can be imported and has expected structure""" + print("Testing app structure...") + try: + from main import app + + # Check routes + routes = [route.path for route in app.routes] + print(f"✓ App has {len(routes)} routes") + + expected_routes = ["/", "/health", "/process-fitness-input"] + for route in expected_routes: + if route in routes: + print(f"✓ Route '{route}' exists") + else: + print(f"✗ Route '{route}' is missing") + return False + + print("App structure is correct!\n") + return True + except Exception as e: + print(f"✗ Error testing app structure: {e}") + return False + + +def test_validation_and_response_functions(): + """Test that the validation and response functions exist and have correct signatures""" + print("Testing function signatures...") + try: + from main import validate_fitness_input, generate_ai_response + import inspect + + # Check validate_fitness_input + sig = inspect.signature(validate_fitness_input) + params = list(sig.parameters.keys()) + if params == ['user_input']: + print("✓ validate_fitness_input has correct signature") + else: + print(f"✗ validate_fitness_input has incorrect signature: {params}") + return False + + # Check generate_ai_response + sig = inspect.signature(generate_ai_response) + params = list(sig.parameters.keys()) + if params == ['user_name', 'user_goals', 'user_body_info', 'user_input']: + print("✓ generate_ai_response has correct signature") + else: + print(f"✗ generate_ai_response has incorrect signature: {params}") + return False + + print("All function signatures are correct!\n") + return True + except Exception as e: + print(f"✗ Error testing function signatures: {e}") + return False + + +def main(): + """Run all tests""" + print("=" * 60) + print("Fitness AI API - Static Tests") + print("=" * 60 + "\n") + + tests = [ + test_models, + test_app_structure, + test_validation_and_response_functions + ] + + results = [] + for test in tests: + results.append(test()) + + print("=" * 60) + if all(results): + print("✓ All tests passed!") + print("=" * 60) + return 0 + else: + print("✗ Some tests failed") + print("=" * 60) + return 1 + + +if __name__ == "__main__": + sys.exit(main()) From caf90cc50d4a3c99f30ba118dbe27e983eab8f59 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 21 Oct 2025 14:26:48 +0000 Subject: [PATCH 3/3] Add comprehensive implementation documentation Co-authored-by: lightningcell <117159961+lightningcell@users.noreply.github.com> --- IMPLEMENTATION.md | 156 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 156 insertions(+) create mode 100644 IMPLEMENTATION.md diff --git a/IMPLEMENTATION.md b/IMPLEMENTATION.md new file mode 100644 index 0000000..c9ecd63 --- /dev/null +++ b/IMPLEMENTATION.md @@ -0,0 +1,156 @@ +# Implementation Summary + +## Problem Statement (Turkish) +The requirement was to create a FastAPI endpoint with the following specifications: + +1. **Input Parameters:** + - user adı (user name) + - user hedefleri ama liste şeklinde (user goals as a list) + - user vücut bilgileri (dict olarak, örneğin {weight: 76.8,..}) (user body info as dict) + - user input'u (user input) + +2. **Two-Step Process:** + + **Step 1:** Use the user input to ask the model if it's fitness-related, returning a boolean or score value. + + **Step 2:** Based on the value: + - If failed: return error response + - If successful: include user information in prompt and send to AI, return AI response + +## Implementation + +### Files Created + +1. **main.py** - Main FastAPI application + - `/process-fitness-input` endpoint (POST) - Main endpoint for processing user input + - `/` endpoint (GET) - Root endpoint with API info + - `/health` endpoint (GET) - Health check + - `validate_fitness_input()` - Step 1: Validates if input is fitness-related + - `generate_ai_response()` - Step 2: Generates personalized AI response + +2. **models.py** - Pydantic data models + - `UserInput` - Request model with user_name, user_goals, user_body_info, user_input + - `FitnessValidationResponse` - Internal model for validation results + - `ErrorResponse` - Error response model + - `SuccessResponse` - Success response model with AI response and fitness score + +3. **requirements.txt** - Project dependencies + - fastapi==0.104.1 + - uvicorn[standard]==0.24.0 + - pydantic==2.5.0 + - openai==1.3.0 + - python-dotenv==1.0.0 + +4. **test_app.py** - Test script to validate implementation +5. **example_usage.py** - Example script showing API usage +6. **.gitignore** - Python gitignore file +7. **.env.example** - Example environment variables file +8. **README.md** - Comprehensive documentation + +### Implementation Details + +#### Step 1: Fitness Validation +Uses OpenAI's **structured output** feature (gpt-4o-2024-08-06 model) with the `response_format` parameter: + +```python +completion = client.beta.chat.completions.parse( + model="gpt-4o-2024-08-06", + messages=[...], + response_format=FitnessValidationResponse, # Structured output +) +``` + +Returns a `FitnessValidationResponse` object with: +- `is_fitness_related`: Boolean +- `score`: Float (0-1) confidence score + +#### Step 2: Conditional Processing + +**If validation fails (score < 0.5 or is_fitness_related = False):** +```json +HTTP 400 Bad Request +{ + "detail": { + "error": "Input is not fitness-related", + "details": "The provided input does not appear to be related to fitness..." + } +} +``` + +**If validation succeeds:** +- User information (name, goals, body info) is included in the prompt +- OpenAI generates a personalized fitness response +- Returns success response: +```json +HTTP 200 OK +{ + "response": "AI-generated personalized fitness advice...", + "fitness_score": 0.95 +} +``` + +### API Usage + +**Request:** +```bash +curl -X POST "http://localhost:8000/process-fitness-input" \ + -H "Content-Type: application/json" \ + -d '{ + "user_name": "John Doe", + "user_goals": ["lose weight", "build muscle"], + "user_body_info": {"weight": 76.8, "height": 180}, + "user_input": "I want to start a workout routine" + }' +``` + +**Success Response (200):** +```json +{ + "response": "Based on your goals of losing weight and building muscle, and your current weight of 76.8kg and height of 180cm...", + "fitness_score": 0.95 +} +``` + +**Error Response (400):** +```json +{ + "detail": { + "error": "Input is not fitness-related", + "details": "The provided input does not appear to be related to fitness, health, or wellness. Confidence score: 0.25" + } +} +``` + +## Configuration + +1. Copy `.env.example` to `.env` +2. Add your OpenAI API key: + ``` + OPENAI_API_KEY=sk-... + ``` + +## Testing + +Run tests: +```bash +python test_app.py +``` + +Start the server: +```bash +python main.py +# or +uvicorn main:app --reload +``` + +## Features Implemented + +✅ FastAPI endpoint accepting user name, goals (list), body info (dict), and input (string) +✅ Step 1: Fitness validation using OpenAI structured output (boolean + score) +✅ Step 2: Conditional processing based on validation +✅ Error response for non-fitness inputs +✅ Success response with personalized AI-generated content +✅ Comprehensive documentation +✅ Example scripts and tests +✅ Proper error handling +✅ OpenAPI/Swagger documentation (auto-generated at /docs)