Add AI-powered chat-to-tasks feature
Implement a new endpoint that converts natural language input into structured tasks using an LLM. Features include: - LLM service abstraction with support for OpenAI and Google Gemini - Dependency injection pattern for easy provider switching - Robust error handling and response formatting - Integration with existing user authentication and task creation - Fallback to mock LLM service for testing or development
This commit is contained in:
parent
f3dd0afb07
commit
97c002ac88
47
README.md
47
README.md
@ -9,6 +9,7 @@ A RESTful API for managing tasks, built with FastAPI and SQLite.
|
|||||||
- Task CRUD operations with user-based access control
|
- Task CRUD operations with user-based access control
|
||||||
- Task status and priority management
|
- Task status and priority management
|
||||||
- Task completion tracking
|
- Task completion tracking
|
||||||
|
- AI-powered chat-to-tasks conversion
|
||||||
- API documentation with Swagger UI and ReDoc
|
- API documentation with Swagger UI and ReDoc
|
||||||
- Health endpoint for monitoring
|
- Health endpoint for monitoring
|
||||||
|
|
||||||
@ -23,6 +24,8 @@ A RESTful API for managing tasks, built with FastAPI and SQLite.
|
|||||||
- JWT: JSON Web Tokens for authentication
|
- JWT: JSON Web Tokens for authentication
|
||||||
- Passlib: Password hashing and verification
|
- Passlib: Password hashing and verification
|
||||||
- Python-Jose: Python implementation of JWT
|
- Python-Jose: Python implementation of JWT
|
||||||
|
- OpenAI/Google Gemini API: AI models for natural language processing
|
||||||
|
- HTTPX: Async HTTP client for API requests
|
||||||
|
|
||||||
## API Endpoints
|
## API Endpoints
|
||||||
|
|
||||||
@ -46,6 +49,10 @@ A RESTful API for managing tasks, built with FastAPI and SQLite.
|
|||||||
- `DELETE /tasks/{task_id}`: Delete a task for the current user
|
- `DELETE /tasks/{task_id}`: Delete a task for the current user
|
||||||
- `POST /tasks/{task_id}/complete`: Mark a task as completed for the current user
|
- `POST /tasks/{task_id}/complete`: Mark a task as completed for the current user
|
||||||
|
|
||||||
|
### AI-Powered Task Creation (requires authentication)
|
||||||
|
|
||||||
|
- `POST /chat/chat-to-tasks`: Convert natural language input into structured tasks
|
||||||
|
|
||||||
### Health and Diagnostic Endpoints
|
### Health and Diagnostic Endpoints
|
||||||
|
|
||||||
- `GET /health`: Application health check
|
- `GET /health`: Application health check
|
||||||
@ -146,6 +153,19 @@ curl -X 'POST' \
|
|||||||
-H 'Authorization: Bearer YOUR_ACCESS_TOKEN'
|
-H 'Authorization: Bearer YOUR_ACCESS_TOKEN'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Create tasks from natural language (with authentication)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X 'POST' \
|
||||||
|
'https://taskmanagerapi-ttkjqk.backend.im/chat/chat-to-tasks' \
|
||||||
|
-H 'accept: application/json' \
|
||||||
|
-H 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d '{
|
||||||
|
"message": "I need to prepare a presentation for Monday, pick up groceries tomorrow, and call John about the project by Friday."
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
### Get current user information (with authentication)
|
### Get current user information (with authentication)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@ -166,6 +186,7 @@ taskmanagerapi/
|
|||||||
│ │ ├── deps.py # Dependency injection for authentication
|
│ │ ├── deps.py # Dependency injection for authentication
|
||||||
│ │ └── routers/ # API route definitions
|
│ │ └── routers/ # API route definitions
|
||||||
│ │ ├── auth.py # Authentication endpoints
|
│ │ ├── auth.py # Authentication endpoints
|
||||||
|
│ │ ├── chat.py # Chat-to-tasks endpoints
|
||||||
│ │ └── tasks.py # Task management endpoints
|
│ │ └── tasks.py # Task management endpoints
|
||||||
│ ├── core/ # Core application code
|
│ ├── core/ # Core application code
|
||||||
│ │ ├── config.py # Application configuration
|
│ │ ├── config.py # Application configuration
|
||||||
@ -182,9 +203,12 @@ taskmanagerapi/
|
|||||||
│ ├── models/ # SQLAlchemy models
|
│ ├── models/ # SQLAlchemy models
|
||||||
│ │ ├── task.py # Task model
|
│ │ ├── task.py # Task model
|
||||||
│ │ └── user.py # User model
|
│ │ └── user.py # User model
|
||||||
│ └── schemas/ # Pydantic schemas/models
|
│ ├── schemas/ # Pydantic schemas/models
|
||||||
│ ├── task.py # Task schemas
|
│ │ ├── chat.py # Chat-to-tasks schemas
|
||||||
│ └── user.py # User and authentication schemas
|
│ │ ├── task.py # Task schemas
|
||||||
|
│ │ └── user.py # User and authentication schemas
|
||||||
|
│ └── services/ # Service integrations
|
||||||
|
│ └── llm_service.py # LLM service for chat-to-tasks conversion
|
||||||
├── main.py # Application entry point
|
├── main.py # Application entry point
|
||||||
└── requirements.txt # Project dependencies
|
└── requirements.txt # Project dependencies
|
||||||
```
|
```
|
||||||
@ -267,6 +291,23 @@ You can use these credentials to authenticate and get started right away.
|
|||||||
3. **Use token**: Include the token in your requests as a Bearer token in the Authorization header
|
3. **Use token**: Include the token in your requests as a Bearer token in the Authorization header
|
||||||
4. **Access protected endpoints**: Use the token to access protected task management endpoints
|
4. **Access protected endpoints**: Use the token to access protected task management endpoints
|
||||||
|
|
||||||
|
### Chat-to-Tasks Feature
|
||||||
|
|
||||||
|
The application includes an AI-powered feature that converts natural language inputs into structured task objects:
|
||||||
|
|
||||||
|
1. **Environment Configuration**: Set the `LLM_PROVIDER` environment variable to select the AI model provider:
|
||||||
|
- `openai`: Uses OpenAI's GPT models (requires `OPENAI_API_KEY` and optionally `OPENAI_MODEL`)
|
||||||
|
- `gemini`: Uses Google's Gemini models (requires `GEMINI_API_KEY` and optionally `GEMINI_MODEL`)
|
||||||
|
- `mock`: Uses a simple rule-based mock implementation (default, no API keys needed)
|
||||||
|
|
||||||
|
2. **Using the Feature**: Send a POST request to `/chat/chat-to-tasks` with a natural language message describing your tasks. The API will:
|
||||||
|
- Process the message using the configured AI model
|
||||||
|
- Extract task details (title, description, due date, priority)
|
||||||
|
- Create tasks in the database for the authenticated user
|
||||||
|
- Return the created tasks in a structured format
|
||||||
|
|
||||||
|
3. **Example**: "I need to finish the report by Friday, call Susan about the meeting tomorrow morning, and buy groceries tonight" will be converted into three separate tasks with appropriate details.
|
||||||
|
|
||||||
### API Documentation
|
### API Documentation
|
||||||
|
|
||||||
- Swagger UI: http://localhost:8000/docs
|
- Swagger UI: http://localhost:8000/docs
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
|
|
||||||
from app.api.routers import tasks, auth
|
from app.api.routers import tasks, auth, chat
|
||||||
|
|
||||||
api_router = APIRouter()
|
api_router = APIRouter()
|
||||||
api_router.include_router(tasks.router, prefix="/tasks", tags=["tasks"])
|
api_router.include_router(tasks.router, prefix="/tasks", tags=["tasks"])
|
||||||
api_router.include_router(auth.router, prefix="/auth", tags=["auth"])
|
api_router.include_router(auth.router, prefix="/auth", tags=["auth"])
|
||||||
|
api_router.include_router(chat.router, prefix="/chat", tags=["chat"])
|
||||||
|
126
app/api/routers/chat.py
Normal file
126
app/api/routers/chat.py
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
"""
|
||||||
|
Router for the chat-to-tasks functionality.
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from app.api import deps
|
||||||
|
from app.crud import task as task_crud
|
||||||
|
from app.models.user import User
|
||||||
|
from app.schemas.chat import ChatInput, ChatResponse, ChatProcessingError
|
||||||
|
from app.schemas.task import TaskCreate, TaskRead
|
||||||
|
from app.services.llm_service import LLMService, get_llm_service
|
||||||
|
from app.db.session import get_db
|
||||||
|
|
||||||
|
# Set up logger
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/chat-to-tasks", response_model=ChatResponse)
|
||||||
|
async def create_tasks_from_chat(
|
||||||
|
chat_input: ChatInput,
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
current_user: User = Depends(deps.get_current_active_user),
|
||||||
|
llm_service: LLMService = Depends(get_llm_service),
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Convert natural language chat input into one or more task objects.
|
||||||
|
|
||||||
|
This endpoint:
|
||||||
|
1. Takes the user's natural language input
|
||||||
|
2. Sends it to an LLM for processing
|
||||||
|
3. Parses the LLM's response into TaskCreate objects
|
||||||
|
4. Creates the tasks in the database
|
||||||
|
5. Returns the created tasks
|
||||||
|
|
||||||
|
All tasks are associated with the authenticated user.
|
||||||
|
"""
|
||||||
|
if not chat_input.message or len(chat_input.message.strip()) < 3:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail="Message must be at least 3 characters long",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Initialize response
|
||||||
|
response = ChatResponse(original_message=chat_input.message)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Process the chat message with the LLM service
|
||||||
|
logger.info(f"Processing chat input: {chat_input.message[:50]}...")
|
||||||
|
llm_tasks_data = await llm_service.chat_to_tasks(chat_input.message)
|
||||||
|
|
||||||
|
if not llm_tasks_data:
|
||||||
|
logger.warning("LLM returned no tasks")
|
||||||
|
response.processing_successful = False
|
||||||
|
response.error = ChatProcessingError(
|
||||||
|
error_type="parsing_error",
|
||||||
|
error_detail="No tasks could be extracted from your message",
|
||||||
|
)
|
||||||
|
return response
|
||||||
|
|
||||||
|
# Convert LLM response to TaskCreate objects and create in DB
|
||||||
|
created_tasks = []
|
||||||
|
|
||||||
|
for task_data in llm_tasks_data:
|
||||||
|
try:
|
||||||
|
# Map LLM response fields to TaskCreate schema
|
||||||
|
# Handle different field names or formats that might come from the LLM
|
||||||
|
task_create_data = {
|
||||||
|
"title": task_data.get("title", "Untitled Task"),
|
||||||
|
"description": task_data.get("description", ""),
|
||||||
|
"priority": task_data.get("priority", "medium").lower(),
|
||||||
|
}
|
||||||
|
|
||||||
|
# Handle due_date if present
|
||||||
|
if due_date := task_data.get("due_date"):
|
||||||
|
if due_date != "null" and due_date is not None:
|
||||||
|
task_create_data["due_date"] = due_date
|
||||||
|
|
||||||
|
# Map status if present (convert "pending" to "todo" if needed)
|
||||||
|
if status := task_data.get("status"):
|
||||||
|
if status.lower() == "pending":
|
||||||
|
task_create_data["status"] = "todo"
|
||||||
|
else:
|
||||||
|
task_create_data["status"] = status.lower()
|
||||||
|
|
||||||
|
# Create TaskCreate object and validate
|
||||||
|
task_in = TaskCreate(**task_create_data)
|
||||||
|
|
||||||
|
# Create task in database with current user as owner
|
||||||
|
db_task = task_crud.task.create_with_owner(
|
||||||
|
db=db, obj_in=task_in, user_id=current_user.id
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add created task to response
|
||||||
|
created_tasks.append(TaskRead.model_validate(db_task))
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error creating task: {e}")
|
||||||
|
# Continue with other tasks if one fails
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not created_tasks:
|
||||||
|
# If no tasks were successfully created
|
||||||
|
response.processing_successful = False
|
||||||
|
response.error = ChatProcessingError(
|
||||||
|
error_type="creation_error",
|
||||||
|
error_detail="Could not create any tasks from your message",
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Add created tasks to response
|
||||||
|
response.tasks = created_tasks
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(f"Error in chat-to-tasks endpoint: {e}")
|
||||||
|
response.processing_successful = False
|
||||||
|
response.error = ChatProcessingError(
|
||||||
|
error_type="processing_error",
|
||||||
|
error_detail=f"An error occurred while processing your request: {str(e)}",
|
||||||
|
)
|
||||||
|
return response
|
@ -77,12 +77,12 @@ else:
|
|||||||
|
|
||||||
|
|
||||||
class Settings(BaseSettings):
|
class Settings(BaseSettings):
|
||||||
|
# Application settings
|
||||||
PROJECT_NAME: str = "Task Manager API"
|
PROJECT_NAME: str = "Task Manager API"
|
||||||
# No API version prefix - use direct paths
|
|
||||||
API_PREFIX: str = ""
|
API_PREFIX: str = ""
|
||||||
SECRET_KEY: str = secrets.token_urlsafe(32)
|
SECRET_KEY: str = secrets.token_urlsafe(32)
|
||||||
# 60 minutes * 24 hours * 8 days = 8 days
|
ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 8 # 8 days
|
||||||
ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 8
|
ENVIRONMENT: str = "development"
|
||||||
|
|
||||||
# CORS Configuration
|
# CORS Configuration
|
||||||
BACKEND_CORS_ORIGINS: List[AnyHttpUrl] = []
|
BACKEND_CORS_ORIGINS: List[AnyHttpUrl] = []
|
||||||
@ -98,6 +98,18 @@ class Settings(BaseSettings):
|
|||||||
# Database configuration
|
# Database configuration
|
||||||
SQLALCHEMY_DATABASE_URL: str = f"sqlite:///{DB_DIR}/db.sqlite"
|
SQLALCHEMY_DATABASE_URL: str = f"sqlite:///{DB_DIR}/db.sqlite"
|
||||||
|
|
||||||
|
# LLM provider settings - defaults to Mock service if not configured
|
||||||
|
# Options: "openai", "gemini", "mock"
|
||||||
|
LLM_PROVIDER: str = os.environ.get("LLM_PROVIDER", "mock")
|
||||||
|
|
||||||
|
# OpenAI settings
|
||||||
|
OPENAI_API_KEY: str = os.environ.get("OPENAI_API_KEY", "")
|
||||||
|
OPENAI_MODEL: str = os.environ.get("OPENAI_MODEL", "gpt-3.5-turbo")
|
||||||
|
|
||||||
|
# Google Gemini settings
|
||||||
|
GEMINI_API_KEY: str = os.environ.get("GEMINI_API_KEY", "")
|
||||||
|
GEMINI_MODEL: str = os.environ.get("GEMINI_MODEL", "gemini-pro")
|
||||||
|
|
||||||
model_config = {"case_sensitive": True}
|
model_config = {"case_sensitive": True}
|
||||||
|
|
||||||
|
|
||||||
|
44
app/schemas/chat.py
Normal file
44
app/schemas/chat.py
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
"""
|
||||||
|
Pydantic schemas for the Chat-to-Tasks feature.
|
||||||
|
"""
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
from app.schemas.task import TaskRead
|
||||||
|
|
||||||
|
|
||||||
|
class ChatInput(BaseModel):
|
||||||
|
"""Schema for chat input from user."""
|
||||||
|
|
||||||
|
message: str = Field(
|
||||||
|
...,
|
||||||
|
description="Natural language input describing tasks to be created",
|
||||||
|
min_length=3,
|
||||||
|
max_length=2000,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ChatProcessingError(BaseModel):
|
||||||
|
"""Schema for error details when processing chat."""
|
||||||
|
|
||||||
|
error_type: str = Field(..., description="Type of error encountered")
|
||||||
|
error_detail: str = Field(..., description="Detailed error information")
|
||||||
|
|
||||||
|
|
||||||
|
class ChatResponse(BaseModel):
|
||||||
|
"""Schema for chat response with parsed tasks."""
|
||||||
|
|
||||||
|
original_message: str = Field(..., description="Original user message")
|
||||||
|
tasks: List[TaskRead] = Field(
|
||||||
|
default_factory=list,
|
||||||
|
description="Tasks extracted from the message",
|
||||||
|
)
|
||||||
|
processing_successful: bool = Field(
|
||||||
|
default=True,
|
||||||
|
description="Indicates if processing was successful",
|
||||||
|
)
|
||||||
|
error: Optional[ChatProcessingError] = Field(
|
||||||
|
default=None,
|
||||||
|
description="Error details if processing was not successful",
|
||||||
|
)
|
@ -61,4 +61,10 @@ class TaskInDBBase(TaskBase):
|
|||||||
|
|
||||||
|
|
||||||
class Task(TaskInDBBase):
|
class Task(TaskInDBBase):
|
||||||
|
"""Schema for tasks returned from database operations."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class TaskRead(TaskInDBBase):
|
||||||
|
"""Schema for task data returned to clients."""
|
||||||
|
user_id: Optional[int] = None
|
||||||
|
3
app/services/__init__.py
Normal file
3
app/services/__init__.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
"""
|
||||||
|
Service module initialization.
|
||||||
|
"""
|
211
app/services/llm_service.py
Normal file
211
app/services/llm_service.py
Normal file
@ -0,0 +1,211 @@
|
|||||||
|
"""
|
||||||
|
LLM service for converting natural language to structured task data.
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from typing import Dict, List
|
||||||
|
|
||||||
|
|
||||||
|
from app.core.config import settings
|
||||||
|
|
||||||
|
# Configure logger
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class LLMService(ABC):
|
||||||
|
"""Abstract base class for LLM service implementations."""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
async def chat_to_tasks(self, prompt: str) -> List[Dict]:
|
||||||
|
"""
|
||||||
|
Convert natural language input to structured task objects.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
prompt: User's natural language input describing tasks
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of dictionary objects representing tasks
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class OpenAIService(LLMService):
|
||||||
|
"""OpenAI implementation of LLM service."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
"""Initialize the OpenAI service."""
|
||||||
|
try:
|
||||||
|
import openai
|
||||||
|
self.client = openai.AsyncOpenAI(api_key=settings.openai_api_key)
|
||||||
|
self.model = settings.openai_model
|
||||||
|
except (ImportError, AttributeError) as e:
|
||||||
|
logger.error(f"Failed to initialize OpenAI service: {e}")
|
||||||
|
raise RuntimeError(f"OpenAI service initialization failed: {e}")
|
||||||
|
|
||||||
|
async def chat_to_tasks(self, prompt: str) -> List[Dict]:
|
||||||
|
"""
|
||||||
|
Convert natural language to tasks using OpenAI.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
prompt: User's natural language input
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of task dictionaries
|
||||||
|
"""
|
||||||
|
system_prompt = """
|
||||||
|
You are a task extraction assistant. Your job is to convert the user's natural language
|
||||||
|
input into one or more structured task objects. Each task should have these properties:
|
||||||
|
- title: A short, clear title for the task
|
||||||
|
- description: A more detailed description of what needs to be done
|
||||||
|
- due_date: When the task is due (ISO format date string, or null if not specified)
|
||||||
|
- priority: The priority level (high, medium, low)
|
||||||
|
- status: The status (defaults to "pending")
|
||||||
|
|
||||||
|
Return ONLY a JSON array of task objects without any additional text or explanation.
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = await self.client.chat.completions.create(
|
||||||
|
model=self.model,
|
||||||
|
messages=[
|
||||||
|
{"role": "system", "content": system_prompt},
|
||||||
|
{"role": "user", "content": prompt}
|
||||||
|
],
|
||||||
|
response_format={"type": "json_object"},
|
||||||
|
temperature=0.2,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Extract the JSON content from the response
|
||||||
|
content = response.choices[0].message.content
|
||||||
|
result = json.loads(content)
|
||||||
|
|
||||||
|
# Expect a "tasks" key in the response JSON
|
||||||
|
if "tasks" in result:
|
||||||
|
return result["tasks"]
|
||||||
|
return [result] # Return as a single-item list if no "tasks" key
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"OpenAI API error: {e}")
|
||||||
|
raise RuntimeError(f"Failed to process request with OpenAI: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
class GeminiService(LLMService):
|
||||||
|
"""Google Gemini implementation of LLM service."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
"""Initialize the Gemini service."""
|
||||||
|
try:
|
||||||
|
import google.generativeai as genai
|
||||||
|
genai.configure(api_key=settings.gemini_api_key)
|
||||||
|
self.model = genai.GenerativeModel(settings.gemini_model)
|
||||||
|
except (ImportError, AttributeError) as e:
|
||||||
|
logger.error(f"Failed to initialize Gemini service: {e}")
|
||||||
|
raise RuntimeError(f"Gemini service initialization failed: {e}")
|
||||||
|
|
||||||
|
async def chat_to_tasks(self, prompt: str) -> List[Dict]:
|
||||||
|
"""
|
||||||
|
Convert natural language to tasks using Google Gemini.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
prompt: User's natural language input
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of task dictionaries
|
||||||
|
"""
|
||||||
|
system_prompt = """
|
||||||
|
You are a task extraction assistant. Your job is to convert the user's natural language
|
||||||
|
input into one or more structured task objects. Each task should have these properties:
|
||||||
|
- title: A short, clear title for the task
|
||||||
|
- description: A more detailed description of what needs to be done
|
||||||
|
- due_date: When the task is due (ISO format date string, or null if not specified)
|
||||||
|
- priority: The priority level (high, medium, low)
|
||||||
|
- status: The status (defaults to "pending")
|
||||||
|
|
||||||
|
Return ONLY a JSON array of task objects without any additional text or explanation.
|
||||||
|
Format your response as valid JSON with a "tasks" key that contains an array of task objects.
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
chat = self.model.start_chat(history=[
|
||||||
|
{"role": "user", "parts": [system_prompt]},
|
||||||
|
{"role": "model", "parts": ["I understand. I'll convert user inputs into JSON task objects with the specified properties."]}
|
||||||
|
])
|
||||||
|
|
||||||
|
response = await chat.send_message_async(prompt)
|
||||||
|
content = response.text
|
||||||
|
|
||||||
|
# Extract JSON from the response
|
||||||
|
# This handles cases where the model might add markdown code blocks
|
||||||
|
if "```json" in content:
|
||||||
|
json_str = content.split("```json")[1].split("```")[0].strip()
|
||||||
|
elif "```" in content:
|
||||||
|
json_str = content.split("```")[1].strip()
|
||||||
|
else:
|
||||||
|
json_str = content.strip()
|
||||||
|
|
||||||
|
result = json.loads(json_str)
|
||||||
|
|
||||||
|
# Expect a "tasks" key in the response JSON
|
||||||
|
if "tasks" in result:
|
||||||
|
return result["tasks"]
|
||||||
|
return [result] # Return as a single-item list if no "tasks" key
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Gemini API error: {e}")
|
||||||
|
raise RuntimeError(f"Failed to process request with Gemini: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
class MockLLMService(LLMService):
|
||||||
|
"""Mock LLM service for testing."""
|
||||||
|
|
||||||
|
async def chat_to_tasks(self, prompt: str) -> List[Dict]:
|
||||||
|
"""
|
||||||
|
Return mock tasks based on the prompt.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
prompt: User's natural language input
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of task dictionaries
|
||||||
|
"""
|
||||||
|
# Simple parsing logic for testing
|
||||||
|
words = prompt.lower().split()
|
||||||
|
priority = "medium"
|
||||||
|
|
||||||
|
if "urgent" in words or "important" in words:
|
||||||
|
priority = "high"
|
||||||
|
elif "low" in words or "minor" in words:
|
||||||
|
priority = "low"
|
||||||
|
|
||||||
|
# Create a basic task from the prompt
|
||||||
|
return [{
|
||||||
|
"title": prompt[:50] + ("..." if len(prompt) > 50 else ""),
|
||||||
|
"description": prompt,
|
||||||
|
"due_date": None,
|
||||||
|
"priority": priority,
|
||||||
|
"status": "pending"
|
||||||
|
}]
|
||||||
|
|
||||||
|
|
||||||
|
# Factory function to create the appropriate LLM service
|
||||||
|
def get_llm_service() -> LLMService:
|
||||||
|
"""
|
||||||
|
Factory function for LLM service dependency injection.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
An instance of a concrete LLMService implementation
|
||||||
|
"""
|
||||||
|
llm_provider = settings.llm_provider.lower()
|
||||||
|
|
||||||
|
if llm_provider == "openai" and settings.openai_api_key:
|
||||||
|
return OpenAIService()
|
||||||
|
elif llm_provider == "gemini" and settings.gemini_api_key:
|
||||||
|
return GeminiService()
|
||||||
|
elif llm_provider == "mock" or settings.environment == "test":
|
||||||
|
# Use mock service for testing or when configured
|
||||||
|
return MockLLMService()
|
||||||
|
else:
|
||||||
|
# Default to mock service if configuration is incomplete
|
||||||
|
logger.warning(f"LLM provider '{llm_provider}' not properly configured - using mock service")
|
||||||
|
return MockLLMService()
|
22
main.py
22
main.py
@ -1,17 +1,22 @@
|
|||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
|
import traceback
|
||||||
|
import datetime
|
||||||
|
import sqlite3
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from fastapi import FastAPI, Request
|
from fastapi import FastAPI, Request
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from fastapi.responses import JSONResponse
|
from fastapi.responses import JSONResponse
|
||||||
|
from sqlalchemy import text
|
||||||
|
|
||||||
# Add project root to Python path for imports in alembic migrations
|
# Add project root to Python path for imports in alembic migrations
|
||||||
project_root = Path(__file__).parent.absolute()
|
project_root = Path(__file__).parent.absolute()
|
||||||
sys.path.insert(0, str(project_root))
|
sys.path.insert(0, str(project_root))
|
||||||
|
|
||||||
from app.api.routers import api_router
|
# Import app modules after setting up project path
|
||||||
from app.core.config import settings
|
from app.api.routers import api_router # noqa: E402
|
||||||
from app.db import init_db
|
from app.core.config import settings # noqa: E402
|
||||||
|
from app.db import init_db # noqa: E402
|
||||||
|
|
||||||
# Initialize the database on startup
|
# Initialize the database on startup
|
||||||
print("Starting database initialization...")
|
print("Starting database initialization...")
|
||||||
@ -97,7 +102,6 @@ async def global_exception_handler(request: Request, exc: Exception):
|
|||||||
|
|
||||||
# Add SQLite diagnostic check
|
# Add SQLite diagnostic check
|
||||||
try:
|
try:
|
||||||
import sqlite3
|
|
||||||
from app.db.session import db_file
|
from app.db.session import db_file
|
||||||
|
|
||||||
# Try basic SQLite operations
|
# Try basic SQLite operations
|
||||||
@ -113,8 +117,6 @@ async def global_exception_handler(request: Request, exc: Exception):
|
|||||||
task_table_exists = cursor.fetchone() is not None
|
task_table_exists = cursor.fetchone() is not None
|
||||||
|
|
||||||
# Get file info
|
# Get file info
|
||||||
import os
|
|
||||||
|
|
||||||
file_exists = os.path.exists(db_file)
|
file_exists = os.path.exists(db_file)
|
||||||
file_size = os.path.getsize(db_file) if file_exists else 0
|
file_size = os.path.getsize(db_file) if file_exists else 0
|
||||||
|
|
||||||
@ -159,6 +161,9 @@ def api_info():
|
|||||||
"test-token": "/auth/test-token",
|
"test-token": "/auth/test-token",
|
||||||
},
|
},
|
||||||
"tasks": "/tasks",
|
"tasks": "/tasks",
|
||||||
|
"chat": {
|
||||||
|
"chat_to_tasks": "/chat/chat-to-tasks",
|
||||||
|
},
|
||||||
"docs": "/docs",
|
"docs": "/docs",
|
||||||
"redoc": "/redoc",
|
"redoc": "/redoc",
|
||||||
"health": "/health",
|
"health": "/health",
|
||||||
@ -181,11 +186,6 @@ def test_db_connection():
|
|||||||
Test database connection and table creation
|
Test database connection and table creation
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
import os
|
|
||||||
import sqlite3
|
|
||||||
import traceback
|
|
||||||
import datetime
|
|
||||||
from sqlalchemy import text
|
|
||||||
from app.db.session import engine, db_file
|
from app.db.session import engine, db_file
|
||||||
from app.core.config import DB_DIR
|
from app.core.config import DB_DIR
|
||||||
|
|
||||||
|
@ -9,4 +9,8 @@ ruff>=0.1.3
|
|||||||
passlib>=1.7.4
|
passlib>=1.7.4
|
||||||
bcrypt>=4.0.1
|
bcrypt>=4.0.1
|
||||||
python-jose>=3.3.0
|
python-jose>=3.3.0
|
||||||
email-validator>=2.0.0
|
email-validator>=2.0.0
|
||||||
|
# LLM libraries for chat-to-tasks feature
|
||||||
|
openai>=1.6.0
|
||||||
|
google-generativeai>=0.3.0
|
||||||
|
httpx>=0.25.0
|
Loading…
x
Reference in New Issue
Block a user