non-building checkpoint 1

This commit is contained in:
2026-01-03 11:18:56 -07:00
parent 9c64cb0c2f
commit 81ea22eae9
37 changed files with 4804 additions and 275 deletions

30
backend/Dockerfile Normal file
View File

@@ -0,0 +1,30 @@
FROM python:3.11-slim
WORKDIR /app
# Install system dependencies
RUN apt-get update && apt-get install -y \
gcc \
&& rm -rf /var/lib/apt/lists/*
# Copy requirements first for better caching
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy application code
COPY . .
# Create non-root user
RUN useradd -m -u 1000 appuser && chown -R appuser:appuser /app
USER appuser
# Expose port
EXPOSE 8000
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')" || exit 1
# Run the application
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

15
backend/app/api/v1/api.py Normal file
View File

@@ -0,0 +1,15 @@
"""
API router for version 1 endpoints.
"""
from fastapi import APIRouter
from app.api.v1.endpoints import prompts, feedback
# Create main API router
api_router = APIRouter()
# Include endpoint routers
api_router.include_router(prompts.router, prefix="/prompts", tags=["prompts"])
api_router.include_router(feedback.router, prefix="/feedback", tags=["feedback"])

View File

@@ -0,0 +1,131 @@
"""
Feedback-related API endpoints.
"""
from typing import List, Dict
from fastapi import APIRouter, HTTPException, Depends, status
from pydantic import BaseModel
from app.services.prompt_service import PromptService
from app.models.prompt import FeedbackWord, RateFeedbackWordsRequest, RateFeedbackWordsResponse
# Create router
router = APIRouter()
# Response models
class GenerateFeedbackWordsResponse(BaseModel):
"""Response model for generating feedback words."""
theme_words: List[str]
count: int = 6
# Service dependency
async def get_prompt_service() -> PromptService:
"""Dependency to get PromptService instance."""
return PromptService()
@router.get("/generate", response_model=GenerateFeedbackWordsResponse)
async def generate_feedback_words(
prompt_service: PromptService = Depends(get_prompt_service)
):
"""
Generate 6 theme feedback words using AI.
Returns:
List of 6 theme words for feedback
"""
try:
theme_words = await prompt_service.generate_theme_feedback_words()
if len(theme_words) != 6:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Expected 6 theme words, got {len(theme_words)}"
)
return GenerateFeedbackWordsResponse(
theme_words=theme_words,
count=len(theme_words)
)
except ValueError as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=str(e)
)
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error generating feedback words: {str(e)}"
)
@router.post("/rate", response_model=RateFeedbackWordsResponse)
async def rate_feedback_words(
request: RateFeedbackWordsRequest,
prompt_service: PromptService = Depends(get_prompt_service)
):
"""
Rate feedback words and update feedback system.
Args:
request: Dictionary of word to rating (0-6)
Returns:
Updated feedback words
"""
try:
feedback_words = await prompt_service.update_feedback_words(request.ratings)
return RateFeedbackWordsResponse(
feedback_words=feedback_words,
added_to_history=True
)
except ValueError as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=str(e)
)
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error rating feedback words: {str(e)}"
)
@router.get("/current", response_model=List[FeedbackWord])
async def get_current_feedback_words(
prompt_service: PromptService = Depends(get_prompt_service)
):
"""
Get current feedback words with weights.
Returns:
List of current feedback words with weights
"""
try:
# This would need to be implemented in PromptService
# For now, return empty list
return []
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error getting current feedback words: {str(e)}"
)
@router.get("/history")
async def get_feedback_history(
prompt_service: PromptService = Depends(get_prompt_service)
):
"""
Get feedback word history.
Returns:
List of historic feedback words
"""
try:
# This would need to be implemented in PromptService
# For now, return empty list
return []
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error getting feedback history: {str(e)}"
)

View File

@@ -0,0 +1,186 @@
"""
Prompt-related API endpoints.
"""
from typing import List, Optional
from fastapi import APIRouter, HTTPException, Depends, status
from pydantic import BaseModel
from app.services.prompt_service import PromptService
from app.models.prompt import PromptResponse, PoolStatsResponse, HistoryStatsResponse
# Create router
router = APIRouter()
# Response models
class DrawPromptsResponse(BaseModel):
"""Response model for drawing prompts."""
prompts: List[str]
count: int
remaining_in_pool: int
class FillPoolResponse(BaseModel):
"""Response model for filling prompt pool."""
added: int
total_in_pool: int
target_volume: int
class SelectPromptResponse(BaseModel):
"""Response model for selecting a prompt."""
selected_prompt: str
position_in_history: str # e.g., "prompt00"
history_size: int
# Service dependency
async def get_prompt_service() -> PromptService:
"""Dependency to get PromptService instance."""
return PromptService()
@router.get("/draw", response_model=DrawPromptsResponse)
async def draw_prompts(
count: Optional[int] = None,
prompt_service: PromptService = Depends(get_prompt_service)
):
"""
Draw prompts from the pool.
Args:
count: Number of prompts to draw (defaults to settings.NUM_PROMPTS_PER_SESSION)
prompt_service: PromptService instance
Returns:
List of prompts drawn from pool
"""
try:
prompts = await prompt_service.draw_prompts_from_pool(count)
pool_size = prompt_service.get_pool_size()
return DrawPromptsResponse(
prompts=prompts,
count=len(prompts),
remaining_in_pool=pool_size
)
except ValueError as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=str(e)
)
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error drawing prompts: {str(e)}"
)
@router.post("/fill-pool", response_model=FillPoolResponse)
async def fill_prompt_pool(
prompt_service: PromptService = Depends(get_prompt_service)
):
"""
Fill the prompt pool to target volume using AI.
Returns:
Information about added prompts
"""
try:
added_count = await prompt_service.fill_pool_to_target()
pool_size = prompt_service.get_pool_size()
target_volume = prompt_service.get_target_volume()
return FillPoolResponse(
added=added_count,
total_in_pool=pool_size,
target_volume=target_volume
)
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error filling prompt pool: {str(e)}"
)
@router.get("/stats", response_model=PoolStatsResponse)
async def get_pool_stats(
prompt_service: PromptService = Depends(get_prompt_service)
):
"""
Get statistics about the prompt pool.
Returns:
Pool statistics
"""
try:
return await prompt_service.get_pool_stats()
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error getting pool stats: {str(e)}"
)
@router.get("/history/stats", response_model=HistoryStatsResponse)
async def get_history_stats(
prompt_service: PromptService = Depends(get_prompt_service)
):
"""
Get statistics about prompt history.
Returns:
History statistics
"""
try:
return await prompt_service.get_history_stats()
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error getting history stats: {str(e)}"
)
@router.get("/history", response_model=List[PromptResponse])
async def get_prompt_history(
limit: Optional[int] = None,
prompt_service: PromptService = Depends(get_prompt_service)
):
"""
Get prompt history.
Args:
limit: Maximum number of history items to return
Returns:
List of historical prompts
"""
try:
return await prompt_service.get_prompt_history(limit)
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error getting prompt history: {str(e)}"
)
@router.post("/select/{prompt_index}")
async def select_prompt(
prompt_index: int,
prompt_service: PromptService = Depends(get_prompt_service)
):
"""
Select a prompt from drawn prompts to add to history.
Args:
prompt_index: Index of the prompt to select (0-based)
Returns:
Confirmation of prompt selection
"""
try:
# This endpoint would need to track drawn prompts in session
# For now, we'll implement a simplified version
raise HTTPException(
status_code=status.HTTP_501_NOT_IMPLEMENTED,
detail="Prompt selection not yet implemented"
)
except HTTPException:
raise
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error selecting prompt: {str(e)}"
)

View File

@@ -0,0 +1,76 @@
"""
Configuration settings for the application.
Uses Pydantic settings management with environment variable support.
"""
import os
from typing import List, Optional
from pydantic_settings import BaseSettings
from pydantic import AnyHttpUrl, validator
class Settings(BaseSettings):
"""Application settings."""
# API Settings
API_V1_STR: str = "/api/v1"
PROJECT_NAME: str = "Daily Journal Prompt Generator API"
VERSION: str = "1.0.0"
DEBUG: bool = False
ENVIRONMENT: str = "development"
# Server Settings
HOST: str = "0.0.0.0"
PORT: int = 8000
# CORS Settings
BACKEND_CORS_ORIGINS: List[AnyHttpUrl] = [
"http://localhost:3000", # Frontend dev server
"http://localhost:80", # Frontend production
]
# API Keys
DEEPSEEK_API_KEY: Optional[str] = None
OPENAI_API_KEY: Optional[str] = None
API_BASE_URL: str = "https://api.deepseek.com"
MODEL: str = "deepseek-chat"
# Application Settings
MIN_PROMPT_LENGTH: int = 500
MAX_PROMPT_LENGTH: int = 1000
NUM_PROMPTS_PER_SESSION: int = 6
CACHED_POOL_VOLUME: int = 20
HISTORY_BUFFER_SIZE: int = 60
FEEDBACK_HISTORY_SIZE: int = 30
# File Paths (relative to project root)
DATA_DIR: str = "data"
PROMPT_TEMPLATE_PATH: str = "data/ds_prompt.txt"
FEEDBACK_TEMPLATE_PATH: str = "data/ds_feedback.txt"
SETTINGS_CONFIG_PATH: str = "data/settings.cfg"
# Data File Names (relative to DATA_DIR)
PROMPTS_HISTORIC_FILE: str = "prompts_historic.json"
PROMPTS_POOL_FILE: str = "prompts_pool.json"
FEEDBACK_WORDS_FILE: str = "feedback_words.json"
FEEDBACK_HISTORIC_FILE: str = "feedback_historic.json"
@validator("BACKEND_CORS_ORIGINS", pre=True)
def assemble_cors_origins(cls, v: str | List[str]) -> List[str] | str:
"""Parse CORS origins from string or list."""
if isinstance(v, str) and not v.startswith("["):
return [i.strip() for i in v.split(",")]
elif isinstance(v, (list, str)):
return v
raise ValueError(v)
class Config:
"""Pydantic configuration."""
env_file = ".env"
case_sensitive = True
extra = "ignore"
# Create global settings instance
settings = Settings()

View File

@@ -0,0 +1,130 @@
"""
Exception handlers for the application.
"""
import logging
from typing import Any, Dict
from fastapi import FastAPI, Request, status
from fastapi.responses import JSONResponse
from fastapi.exceptions import RequestValidationError
from pydantic import ValidationError as PydanticValidationError
from app.core.exceptions import DailyJournalPromptException
from app.core.logging import setup_logging
logger = setup_logging()
def setup_exception_handlers(app: FastAPI) -> None:
"""Set up exception handlers for the FastAPI application."""
@app.exception_handler(DailyJournalPromptException)
async def daily_journal_prompt_exception_handler(
request: Request,
exc: DailyJournalPromptException,
) -> JSONResponse:
"""Handle DailyJournalPromptException."""
logger.error(f"DailyJournalPromptException: {exc.detail}")
return JSONResponse(
status_code=exc.status_code,
content={
"error": {
"type": exc.__class__.__name__,
"message": str(exc.detail),
"status_code": exc.status_code,
}
},
)
@app.exception_handler(RequestValidationError)
async def request_validation_exception_handler(
request: Request,
exc: RequestValidationError,
) -> JSONResponse:
"""Handle request validation errors."""
logger.warning(f"RequestValidationError: {exc.errors()}")
return JSONResponse(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
content={
"error": {
"type": "ValidationError",
"message": "Invalid request data",
"details": exc.errors(),
"status_code": status.HTTP_422_UNPROCESSABLE_ENTITY,
}
},
)
@app.exception_handler(PydanticValidationError)
async def pydantic_validation_exception_handler(
request: Request,
exc: PydanticValidationError,
) -> JSONResponse:
"""Handle Pydantic validation errors."""
logger.warning(f"PydanticValidationError: {exc.errors()}")
return JSONResponse(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
content={
"error": {
"type": "ValidationError",
"message": "Invalid data format",
"details": exc.errors(),
"status_code": status.HTTP_422_UNPROCESSABLE_ENTITY,
}
},
)
@app.exception_handler(Exception)
async def generic_exception_handler(
request: Request,
exc: Exception,
) -> JSONResponse:
"""Handle all other exceptions."""
logger.exception(f"Unhandled exception: {exc}")
return JSONResponse(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
content={
"error": {
"type": "InternalServerError",
"message": "An unexpected error occurred",
"status_code": status.HTTP_500_INTERNAL_SERVER_ERROR,
}
},
)
@app.exception_handler(404)
async def not_found_exception_handler(
request: Request,
exc: Exception,
) -> JSONResponse:
"""Handle 404 Not Found errors."""
logger.warning(f"404 Not Found: {request.url}")
return JSONResponse(
status_code=status.HTTP_404_NOT_FOUND,
content={
"error": {
"type": "NotFoundError",
"message": f"Resource not found: {request.url}",
"status_code": status.HTTP_404_NOT_FOUND,
}
},
)
@app.exception_handler(405)
async def method_not_allowed_exception_handler(
request: Request,
exc: Exception,
) -> JSONResponse:
"""Handle 405 Method Not Allowed errors."""
logger.warning(f"405 Method Not Allowed: {request.method} {request.url}")
return JSONResponse(
status_code=status.HTTP_405_METHOD_NOT_ALLOWED,
content={
"error": {
"type": "MethodNotAllowedError",
"message": f"Method {request.method} not allowed for {request.url}",
"status_code": status.HTTP_405_METHOD_NOT_ALLOWED,
}
},
)

View File

@@ -0,0 +1,172 @@
"""
Custom exceptions for the application.
"""
from typing import Any, Dict, Optional
from fastapi import HTTPException, status
class DailyJournalPromptException(HTTPException):
"""Base exception for Daily Journal Prompt application."""
def __init__(
self,
status_code: int = status.HTTP_500_INTERNAL_SERVER_ERROR,
detail: Any = None,
headers: Optional[Dict[str, str]] = None,
) -> None:
super().__init__(status_code=status_code, detail=detail, headers=headers)
class ValidationError(DailyJournalPromptException):
"""Exception for validation errors."""
def __init__(
self,
detail: Any = "Validation error",
headers: Optional[Dict[str, str]] = None,
) -> None:
super().__init__(
status_code=status.HTTP_400_BAD_REQUEST,
detail=detail,
headers=headers,
)
class NotFoundError(DailyJournalPromptException):
"""Exception for resource not found errors."""
def __init__(
self,
detail: Any = "Resource not found",
headers: Optional[Dict[str, str]] = None,
) -> None:
super().__init__(
status_code=status.HTTP_404_NOT_FOUND,
detail=detail,
headers=headers,
)
class UnauthorizedError(DailyJournalPromptException):
"""Exception for unauthorized access errors."""
def __init__(
self,
detail: Any = "Unauthorized access",
headers: Optional[Dict[str, str]] = None,
) -> None:
super().__init__(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=detail,
headers=headers,
)
class ForbiddenError(DailyJournalPromptException):
"""Exception for forbidden access errors."""
def __init__(
self,
detail: Any = "Forbidden access",
headers: Optional[Dict[str, str]] = None,
) -> None:
super().__init__(
status_code=status.HTTP_403_FORBIDDEN,
detail=detail,
headers=headers,
)
class AIServiceError(DailyJournalPromptException):
"""Exception for AI service errors."""
def __init__(
self,
detail: Any = "AI service error",
headers: Optional[Dict[str, str]] = None,
) -> None:
super().__init__(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail=detail,
headers=headers,
)
class DataServiceError(DailyJournalPromptException):
"""Exception for data service errors."""
def __init__(
self,
detail: Any = "Data service error",
headers: Optional[Dict[str, str]] = None,
) -> None:
super().__init__(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=detail,
headers=headers,
)
class ConfigurationError(DailyJournalPromptException):
"""Exception for configuration errors."""
def __init__(
self,
detail: Any = "Configuration error",
headers: Optional[Dict[str, str]] = None,
) -> None:
super().__init__(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=detail,
headers=headers,
)
class PromptPoolEmptyError(DailyJournalPromptException):
"""Exception for empty prompt pool."""
def __init__(
self,
detail: Any = "Prompt pool is empty",
headers: Optional[Dict[str, str]] = None,
) -> None:
super().__init__(
status_code=status.HTTP_400_BAD_REQUEST,
detail=detail,
headers=headers,
)
class InsufficientPoolSizeError(DailyJournalPromptException):
"""Exception for insufficient pool size."""
def __init__(
self,
current_size: int,
requested: int,
headers: Optional[Dict[str, str]] = None,
) -> None:
detail = f"Pool only has {current_size} prompts, requested {requested}"
super().__init__(
status_code=status.HTTP_400_BAD_REQUEST,
detail=detail,
headers=headers,
)
class TemplateNotFoundError(DailyJournalPromptException):
"""Exception for missing template files."""
def __init__(
self,
template_name: str,
headers: Optional[Dict[str, str]] = None,
) -> None:
detail = f"Template not found: {template_name}"
super().__init__(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=detail,
headers=headers,
)

View File

@@ -0,0 +1,54 @@
"""
Logging configuration for the application.
"""
import logging
import sys
from typing import Optional
from app.core.config import settings
def setup_logging(
logger_name: str = "daily_journal_prompt",
log_level: Optional[str] = None,
) -> logging.Logger:
"""
Set up logging configuration.
Args:
logger_name: Name of the logger
log_level: Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
Returns:
Configured logger instance
"""
if log_level is None:
log_level = "DEBUG" if settings.DEBUG else "INFO"
# Create logger
logger = logging.getLogger(logger_name)
logger.setLevel(getattr(logging, log_level.upper()))
# Remove existing handlers to avoid duplicates
logger.handlers.clear()
# Create console handler
console_handler = logging.StreamHandler(sys.stdout)
console_handler.setLevel(getattr(logging, log_level.upper()))
# Create formatter
formatter = logging.Formatter(
"%(asctime)s - %(name)s - %(levelname)s - %(message)s",
datefmt="%Y-%m-%d %H:%M:%S"
)
console_handler.setFormatter(formatter)
# Add handler to logger
logger.addHandler(console_handler)
# Prevent propagation to root logger
logger.propagate = False
return logger

View File

@@ -0,0 +1,88 @@
"""
Pydantic models for prompt-related data.
"""
from typing import List, Optional, Dict, Any
from pydantic import BaseModel, Field
class PromptResponse(BaseModel):
"""Response model for a single prompt."""
key: str = Field(..., description="Prompt key (e.g., 'prompt00')")
text: str = Field(..., description="Prompt text content")
position: int = Field(..., description="Position in history (0 = most recent)")
class Config:
"""Pydantic configuration."""
from_attributes = True
class PoolStatsResponse(BaseModel):
"""Response model for pool statistics."""
total_prompts: int = Field(..., description="Total prompts in pool")
prompts_per_session: int = Field(..., description="Prompts drawn per session")
target_pool_size: int = Field(..., description="Target pool volume")
available_sessions: int = Field(..., description="Available sessions in pool")
needs_refill: bool = Field(..., description="Whether pool needs refilling")
class HistoryStatsResponse(BaseModel):
"""Response model for history statistics."""
total_prompts: int = Field(..., description="Total prompts in history")
history_capacity: int = Field(..., description="Maximum history capacity")
available_slots: int = Field(..., description="Available slots in history")
is_full: bool = Field(..., description="Whether history is full")
class FeedbackWord(BaseModel):
"""Model for a feedback word with weight."""
key: str = Field(..., description="Feedback key (e.g., 'feedback00')")
word: str = Field(..., description="Feedback word")
weight: int = Field(..., ge=0, le=6, description="Weight from 0-6")
class FeedbackHistoryItem(BaseModel):
"""Model for a feedback history item (word only, no weight)."""
key: str = Field(..., description="Feedback key (e.g., 'feedback00')")
word: str = Field(..., description="Feedback word")
class GeneratePromptsRequest(BaseModel):
"""Request model for generating prompts."""
count: Optional[int] = Field(
None,
ge=1,
le=20,
description="Number of prompts to generate (defaults to settings)"
)
use_history: bool = Field(
True,
description="Whether to use historic prompts as context"
)
use_feedback: bool = Field(
True,
description="Whether to use feedback words as context"
)
class GeneratePromptsResponse(BaseModel):
"""Response model for generated prompts."""
prompts: List[str] = Field(..., description="Generated prompts")
count: int = Field(..., description="Number of prompts generated")
used_history: bool = Field(..., description="Whether history was used")
used_feedback: bool = Field(..., description="Whether feedback was used")
class RateFeedbackWordsRequest(BaseModel):
"""Request model for rating feedback words."""
ratings: Dict[str, int] = Field(
...,
description="Dictionary of word to rating (0-6)"
)
class RateFeedbackWordsResponse(BaseModel):
"""Response model for rated feedback words."""
feedback_words: List[FeedbackWord] = Field(..., description="Rated feedback words")
added_to_history: bool = Field(..., description="Whether added to history")

View File

@@ -0,0 +1,337 @@
"""
AI service for handling OpenAI/DeepSeek API calls.
"""
import json
from typing import List, Dict, Any, Optional
from openai import OpenAI, AsyncOpenAI
from app.core.config import settings
from app.core.logging import setup_logging
logger = setup_logging()
class AIService:
"""Service for handling AI API calls."""
def __init__(self):
"""Initialize AI service."""
api_key = settings.DEEPSEEK_API_KEY or settings.OPENAI_API_KEY
if not api_key:
raise ValueError("No API key found. Set DEEPSEEK_API_KEY or OPENAI_API_KEY in environment.")
self.client = AsyncOpenAI(
api_key=api_key,
base_url=settings.API_BASE_URL
)
self.model = settings.MODEL
def _clean_ai_response(self, response_content: str) -> str:
"""
Clean up AI response content to handle common formatting issues.
Handles:
1. Leading/trailing backticks (```json ... ```)
2. Leading "json" string on its own line
3. Extra whitespace and newlines
"""
content = response_content.strip()
# Remove leading/trailing backticks (```json ... ```)
if content.startswith('```'):
lines = content.split('\n')
if len(lines) > 1:
first_line = lines[0].strip()
if 'json' in first_line.lower() or first_line == '```':
content = '\n'.join(lines[1:])
# Remove trailing backticks if present
if content.endswith('```'):
content = content[:-3].rstrip()
# Remove leading "json" string on its own line (case-insensitive)
lines = content.split('\n')
if len(lines) > 0:
first_line = lines[0].strip().lower()
if first_line == 'json':
content = '\n'.join(lines[1:])
# Also handle the case where "json" might be at the beginning of the first line
content = content.strip()
if content.lower().startswith('json\n'):
content = content[4:].strip()
return content.strip()
async def generate_prompts(
self,
prompt_template: str,
historic_prompts: List[Dict[str, str]],
feedback_words: Optional[List[Dict[str, Any]]] = None,
count: Optional[int] = None,
min_length: Optional[int] = None,
max_length: Optional[int] = None
) -> List[str]:
"""
Generate journal prompts using AI.
Args:
prompt_template: Base prompt template
historic_prompts: List of historic prompts for context
feedback_words: List of feedback words with weights
count: Number of prompts to generate
min_length: Minimum prompt length
max_length: Maximum prompt length
Returns:
List of generated prompts
"""
if count is None:
count = settings.NUM_PROMPTS_PER_SESSION
if min_length is None:
min_length = settings.MIN_PROMPT_LENGTH
if max_length is None:
max_length = settings.MAX_PROMPT_LENGTH
# Prepare the full prompt
full_prompt = self._prepare_prompt(
prompt_template,
historic_prompts,
feedback_words,
count,
min_length,
max_length
)
logger.info(f"Generating {count} prompts with AI")
try:
# Call the AI API
response = await self.client.chat.completions.create(
model=self.model,
messages=[
{
"role": "system",
"content": "You are a creative writing assistant that generates journal prompts. Always respond with valid JSON."
},
{
"role": "user",
"content": full_prompt
}
],
temperature=0.7,
max_tokens=2000
)
response_content = response.choices[0].message.content
logger.debug(f"AI response received: {len(response_content)} characters")
# Parse the response
prompts = self._parse_prompt_response(response_content, count)
logger.info(f"Successfully parsed {len(prompts)} prompts from AI response")
return prompts
except Exception as e:
logger.error(f"Error calling AI API: {e}")
logger.debug(f"Full prompt sent to API: {full_prompt[:500]}...")
raise
def _prepare_prompt(
self,
template: str,
historic_prompts: List[Dict[str, str]],
feedback_words: Optional[List[Dict[str, Any]]],
count: int,
min_length: int,
max_length: int
) -> str:
"""Prepare the full prompt with all context."""
# Add the instruction for the specific number of prompts
prompt_instruction = f"Please generate {count} writing prompts, each between {min_length} and {max_length} characters."
# Start with template and instruction
full_prompt = f"{template}\n\n{prompt_instruction}"
# Add historic prompts if available
if historic_prompts:
historic_context = json.dumps(historic_prompts, indent=2)
full_prompt = f"{full_prompt}\n\nPrevious prompts:\n{historic_context}"
# Add feedback words if available
if feedback_words:
feedback_context = json.dumps(feedback_words, indent=2)
full_prompt = f"{full_prompt}\n\nFeedback words:\n{feedback_context}"
return full_prompt
def _parse_prompt_response(self, response_content: str, expected_count: int) -> List[str]:
"""Parse AI response to extract prompts."""
cleaned_content = self._clean_ai_response(response_content)
try:
data = json.loads(cleaned_content)
if isinstance(data, list):
if len(data) >= expected_count:
return data[:expected_count]
else:
logger.warning(f"AI returned {len(data)} prompts, expected {expected_count}")
return data
elif isinstance(data, dict):
logger.warning("AI returned dictionary format, expected list format")
prompts = []
for i in range(expected_count):
key = f"newprompt{i}"
if key in data:
prompts.append(data[key])
return prompts
else:
logger.warning(f"AI returned unexpected data type: {type(data)}")
return []
except json.JSONDecodeError:
logger.warning("AI response is not valid JSON, attempting to extract prompts...")
return self._extract_prompts_from_text(response_content, expected_count)
def _extract_prompts_from_text(self, text: str, expected_count: int) -> List[str]:
"""Extract prompts from plain text response."""
lines = text.strip().split('\n')
prompts = []
for line in lines[:expected_count]:
line = line.strip()
if line and len(line) > 50: # Reasonable minimum length for a prompt
prompts.append(line)
return prompts
async def generate_theme_feedback_words(
self,
feedback_template: str,
historic_prompts: List[Dict[str, str]],
current_feedback_words: Optional[List[Dict[str, Any]]] = None,
historic_feedback_words: Optional[List[Dict[str, str]]] = None
) -> List[str]:
"""
Generate theme feedback words using AI.
Args:
feedback_template: Feedback analysis template
historic_prompts: List of historic prompts for context
current_feedback_words: Current feedback words with weights
historic_feedback_words: Historic feedback words (just words)
Returns:
List of 6 theme words
"""
# Prepare the full prompt
full_prompt = self._prepare_feedback_prompt(
feedback_template,
historic_prompts,
current_feedback_words,
historic_feedback_words
)
logger.info("Generating theme feedback words with AI")
try:
# Call the AI API
response = await self.client.chat.completions.create(
model=self.model,
messages=[
{
"role": "system",
"content": "You are a creative writing assistant that analyzes writing prompts. Always respond with valid JSON."
},
{
"role": "user",
"content": full_prompt
}
],
temperature=0.7,
max_tokens=1000
)
response_content = response.choices[0].message.content
logger.debug(f"AI feedback response received: {len(response_content)} characters")
# Parse the response
theme_words = self._parse_feedback_response(response_content)
logger.info(f"Successfully parsed {len(theme_words)} theme words from AI response")
if len(theme_words) != 6:
logger.warning(f"Expected 6 theme words, got {len(theme_words)}")
return theme_words
except Exception as e:
logger.error(f"Error calling AI API for feedback: {e}")
logger.debug(f"Full feedback prompt sent to API: {full_prompt[:500]}...")
raise
def _prepare_feedback_prompt(
self,
template: str,
historic_prompts: List[Dict[str, str]],
current_feedback_words: Optional[List[Dict[str, Any]]],
historic_feedback_words: Optional[List[Dict[str, str]]]
) -> str:
"""Prepare the full feedback prompt."""
if not historic_prompts:
raise ValueError("No historic prompts available for feedback analysis")
full_prompt = f"{template}\n\nPrevious prompts:\n{json.dumps(historic_prompts, indent=2)}"
# Add current feedback words if available
if current_feedback_words:
feedback_context = json.dumps(current_feedback_words, indent=2)
full_prompt = f"{full_prompt}\n\nCurrent feedback themes (with weights):\n{feedback_context}"
# Add historic feedback words if available
if historic_feedback_words:
feedback_historic_context = json.dumps(historic_feedback_words, indent=2)
full_prompt = f"{full_prompt}\n\nHistoric feedback themes (just words):\n{feedback_historic_context}"
return full_prompt
def _parse_feedback_response(self, response_content: str) -> List[str]:
"""Parse AI response to extract theme words."""
cleaned_content = self._clean_ai_response(response_content)
try:
data = json.loads(cleaned_content)
if isinstance(data, list):
theme_words = []
for word in data:
if isinstance(word, str):
theme_words.append(word.lower().strip())
else:
theme_words.append(str(word).lower().strip())
return theme_words
else:
logger.warning(f"AI returned unexpected data type for feedback: {type(data)}")
return []
except json.JSONDecodeError:
logger.warning("AI feedback response is not valid JSON, attempting to extract theme words...")
return self._extract_theme_words_from_text(response_content)
def _extract_theme_words_from_text(self, text: str) -> List[str]:
"""Extract theme words from plain text response."""
lines = text.strip().split('\n')
theme_words = []
for line in lines:
line = line.strip()
if line and len(line) < 50: # Theme words should be short
words = [w.lower().strip('.,;:!?()[]{}\"\'') for w in line.split()]
theme_words.extend(words)
if len(theme_words) >= 6:
break
return theme_words[:6]

View File

@@ -0,0 +1,187 @@
"""
Data service for handling JSON file operations.
"""
import json
import os
import aiofiles
from typing import Any, List, Dict, Optional
from pathlib import Path
from app.core.config import settings
from app.core.logging import setup_logging
logger = setup_logging()
class DataService:
"""Service for handling data persistence in JSON files."""
def __init__(self):
"""Initialize data service."""
self.data_dir = Path(settings.DATA_DIR)
self.data_dir.mkdir(exist_ok=True)
def _get_file_path(self, filename: str) -> Path:
"""Get full path for a data file."""
return self.data_dir / filename
async def load_json(self, filename: str, default: Any = None) -> Any:
"""
Load JSON data from file.
Args:
filename: Name of the JSON file
default: Default value if file doesn't exist or is invalid
Returns:
Loaded data or default value
"""
file_path = self._get_file_path(filename)
if not file_path.exists():
logger.warning(f"File {filename} not found, returning default")
return default if default is not None else []
try:
async with aiofiles.open(file_path, 'r', encoding='utf-8') as f:
content = await f.read()
return json.loads(content)
except json.JSONDecodeError as e:
logger.error(f"Error decoding JSON from {filename}: {e}")
return default if default is not None else []
except Exception as e:
logger.error(f"Error loading {filename}: {e}")
return default if default is not None else []
async def save_json(self, filename: str, data: Any) -> bool:
"""
Save data to JSON file.
Args:
filename: Name of the JSON file
data: Data to save
Returns:
True if successful, False otherwise
"""
file_path = self._get_file_path(filename)
try:
# Create backup of existing file if it exists
if file_path.exists():
backup_path = file_path.with_suffix('.json.bak')
async with aiofiles.open(file_path, 'r', encoding='utf-8') as src:
async with aiofiles.open(backup_path, 'w', encoding='utf-8') as dst:
await dst.write(await src.read())
# Save new data
async with aiofiles.open(file_path, 'w', encoding='utf-8') as f:
await f.write(json.dumps(data, indent=2, ensure_ascii=False))
logger.info(f"Saved data to {filename}")
return True
except Exception as e:
logger.error(f"Error saving {filename}: {e}")
return False
async def load_prompts_historic(self) -> List[Dict[str, str]]:
"""Load historic prompts from JSON file."""
return await self.load_json(
settings.PROMPTS_HISTORIC_FILE,
default=[]
)
async def save_prompts_historic(self, prompts: List[Dict[str, str]]) -> bool:
"""Save historic prompts to JSON file."""
return await self.save_json(settings.PROMPTS_HISTORIC_FILE, prompts)
async def load_prompts_pool(self) -> List[str]:
"""Load prompt pool from JSON file."""
return await self.load_json(
settings.PROMPTS_POOL_FILE,
default=[]
)
async def save_prompts_pool(self, prompts: List[str]) -> bool:
"""Save prompt pool to JSON file."""
return await self.save_json(settings.PROMPTS_POOL_FILE, prompts)
async def load_feedback_words(self) -> List[Dict[str, Any]]:
"""Load feedback words from JSON file."""
return await self.load_json(
settings.FEEDBACK_WORDS_FILE,
default=[]
)
async def save_feedback_words(self, feedback_words: List[Dict[str, Any]]) -> bool:
"""Save feedback words to JSON file."""
return await self.save_json(settings.FEEDBACK_WORDS_FILE, feedback_words)
async def load_feedback_historic(self) -> List[Dict[str, str]]:
"""Load historic feedback words from JSON file."""
return await self.load_json(
settings.FEEDBACK_HISTORIC_FILE,
default=[]
)
async def save_feedback_historic(self, feedback_words: List[Dict[str, str]]) -> bool:
"""Save historic feedback words to JSON file."""
return await self.save_json(settings.FEEDBACK_HISTORIC_FILE, feedback_words)
async def load_prompt_template(self) -> str:
"""Load prompt template from file."""
template_path = Path(settings.PROMPT_TEMPLATE_PATH)
if not template_path.exists():
logger.error(f"Prompt template not found at {template_path}")
return ""
try:
async with aiofiles.open(template_path, 'r', encoding='utf-8') as f:
return await f.read()
except Exception as e:
logger.error(f"Error loading prompt template: {e}")
return ""
async def load_feedback_template(self) -> str:
"""Load feedback template from file."""
template_path = Path(settings.FEEDBACK_TEMPLATE_PATH)
if not template_path.exists():
logger.error(f"Feedback template not found at {template_path}")
return ""
try:
async with aiofiles.open(template_path, 'r', encoding='utf-8') as f:
return await f.read()
except Exception as e:
logger.error(f"Error loading feedback template: {e}")
return ""
async def load_settings_config(self) -> Dict[str, Any]:
"""Load settings from config file."""
config_path = Path(settings.SETTINGS_CONFIG_PATH)
if not config_path.exists():
logger.warning(f"Settings config not found at {config_path}")
return {}
try:
import configparser
config = configparser.ConfigParser()
config.read(config_path)
settings_dict = {}
if 'prompts' in config:
prompts_section = config['prompts']
settings_dict['min_length'] = int(prompts_section.get('min_length', settings.MIN_PROMPT_LENGTH))
settings_dict['max_length'] = int(prompts_section.get('max_length', settings.MAX_PROMPT_LENGTH))
settings_dict['num_prompts'] = int(prompts_section.get('num_prompts', settings.NUM_PROMPTS_PER_SESSION))
if 'prefetch' in config:
prefetch_section = config['prefetch']
settings_dict['cached_pool_volume'] = int(prefetch_section.get('cached_pool_volume', settings.CACHED_POOL_VOLUME))
return settings_dict
except Exception as e:
logger.error(f"Error loading settings config: {e}")
return {}

View File

@@ -0,0 +1,416 @@
"""
Main prompt service that orchestrates prompt generation and management.
"""
from typing import List, Dict, Any, Optional
from datetime import datetime
from app.core.config import settings
from app.core.logging import setup_logging
from app.services.data_service import DataService
from app.services.ai_service import AIService
from app.models.prompt import (
PromptResponse,
PoolStatsResponse,
HistoryStatsResponse,
FeedbackWord,
FeedbackHistoryItem
)
logger = setup_logging()
class PromptService:
"""Main service for prompt generation and management."""
def __init__(self):
"""Initialize prompt service with dependencies."""
self.data_service = DataService()
self.ai_service = AIService()
# Load settings from config file
self.settings_config = {}
# Cache for loaded data
self._prompts_historic_cache = None
self._prompts_pool_cache = None
self._feedback_words_cache = None
self._feedback_historic_cache = None
self._prompt_template_cache = None
self._feedback_template_cache = None
async def _load_settings_config(self):
"""Load settings from config file if not already loaded."""
if not self.settings_config:
self.settings_config = await self.data_service.load_settings_config()
async def _get_setting(self, key: str, default: Any) -> Any:
"""Get setting value, preferring config file over environment."""
await self._load_settings_config()
return self.settings_config.get(key, default)
# Data loading methods with caching
async def get_prompts_historic(self) -> List[Dict[str, str]]:
"""Get historic prompts with caching."""
if self._prompts_historic_cache is None:
self._prompts_historic_cache = await self.data_service.load_prompts_historic()
return self._prompts_historic_cache
async def get_prompts_pool(self) -> List[str]:
"""Get prompt pool with caching."""
if self._prompts_pool_cache is None:
self._prompts_pool_cache = await self.data_service.load_prompts_pool()
return self._prompts_pool_cache
async def get_feedback_words(self) -> List[Dict[str, Any]]:
"""Get feedback words with caching."""
if self._feedback_words_cache is None:
self._feedback_words_cache = await self.data_service.load_feedback_words()
return self._feedback_words_cache
async def get_feedback_historic(self) -> List[Dict[str, str]]:
"""Get historic feedback words with caching."""
if self._feedback_historic_cache is None:
self._feedback_historic_cache = await self.data_service.load_feedback_historic()
return self._feedback_historic_cache
async def get_prompt_template(self) -> str:
"""Get prompt template with caching."""
if self._prompt_template_cache is None:
self._prompt_template_cache = await self.data_service.load_prompt_template()
return self._prompt_template_cache
async def get_feedback_template(self) -> str:
"""Get feedback template with caching."""
if self._feedback_template_cache is None:
self._feedback_template_cache = await self.data_service.load_feedback_template()
return self._feedback_template_cache
# Core prompt operations
async def draw_prompts_from_pool(self, count: Optional[int] = None) -> List[str]:
"""
Draw prompts from the pool.
Args:
count: Number of prompts to draw
Returns:
List of drawn prompts
"""
if count is None:
count = await self._get_setting('num_prompts', settings.NUM_PROMPTS_PER_SESSION)
pool = await self.get_prompts_pool()
if len(pool) < count:
raise ValueError(
f"Pool only has {len(pool)} prompts, requested {count}. "
f"Use fill-pool endpoint to add more prompts."
)
# Draw prompts from the beginning of the pool
drawn_prompts = pool[:count]
remaining_pool = pool[count:]
# Update cache and save
self._prompts_pool_cache = remaining_pool
await self.data_service.save_prompts_pool(remaining_pool)
logger.info(f"Drew {len(drawn_prompts)} prompts from pool, {len(remaining_pool)} remaining")
return drawn_prompts
async def fill_pool_to_target(self) -> int:
"""
Fill the prompt pool to target volume.
Returns:
Number of prompts added
"""
target_volume = await self._get_setting('cached_pool_volume', settings.CACHED_POOL_VOLUME)
current_pool = await self.get_prompts_pool()
current_size = len(current_pool)
if current_size >= target_volume:
logger.info(f"Pool already at target volume: {current_size}/{target_volume}")
return 0
prompts_needed = target_volume - current_size
logger.info(f"Generating {prompts_needed} prompts to fill pool")
# Generate prompts
new_prompts = await self.generate_prompts(
count=prompts_needed,
use_history=True,
use_feedback=True
)
if not new_prompts:
logger.error("Failed to generate prompts for pool")
return 0
# Add to pool
updated_pool = current_pool + new_prompts
self._prompts_pool_cache = updated_pool
await self.data_service.save_prompts_pool(updated_pool)
added_count = len(new_prompts)
logger.info(f"Added {added_count} prompts to pool, new size: {len(updated_pool)}")
return added_count
async def generate_prompts(
self,
count: Optional[int] = None,
use_history: bool = True,
use_feedback: bool = True
) -> List[str]:
"""
Generate new prompts using AI.
Args:
count: Number of prompts to generate
use_history: Whether to use historic prompts as context
use_feedback: Whether to use feedback words as context
Returns:
List of generated prompts
"""
if count is None:
count = await self._get_setting('num_prompts', settings.NUM_PROMPTS_PER_SESSION)
min_length = await self._get_setting('min_length', settings.MIN_PROMPT_LENGTH)
max_length = await self._get_setting('max_length', settings.MAX_PROMPT_LENGTH)
# Load templates and data
prompt_template = await self.get_prompt_template()
if not prompt_template:
raise ValueError("Prompt template not found")
historic_prompts = await self.get_prompts_historic() if use_history else []
feedback_words = await self.get_feedback_words() if use_feedback else None
# Generate prompts using AI
new_prompts = await self.ai_service.generate_prompts(
prompt_template=prompt_template,
historic_prompts=historic_prompts,
feedback_words=feedback_words,
count=count,
min_length=min_length,
max_length=max_length
)
return new_prompts
async def add_prompt_to_history(self, prompt_text: str) -> str:
"""
Add a prompt to the historic prompts cyclic buffer.
Args:
prompt_text: Prompt text to add
Returns:
Position key of the added prompt (e.g., "prompt00")
"""
historic_prompts = await self.get_prompts_historic()
# Create the new prompt object
new_prompt = {"prompt00": prompt_text}
# Shift all existing prompts down by one position
updated_prompts = [new_prompt]
# Add all existing prompts, shifting their numbers down by one
for i, prompt_dict in enumerate(historic_prompts):
if i >= settings.HISTORY_BUFFER_SIZE - 1: # Keep only HISTORY_BUFFER_SIZE prompts
break
# Get the prompt text
prompt_key = list(prompt_dict.keys())[0]
prompt_text = prompt_dict[prompt_key]
# Create prompt with new number (shifted down by one)
new_prompt_key = f"prompt{i+1:02d}"
updated_prompts.append({new_prompt_key: prompt_text})
# Update cache and save
self._prompts_historic_cache = updated_prompts
await self.data_service.save_prompts_historic(updated_prompts)
logger.info(f"Added prompt to history as prompt00, history size: {len(updated_prompts)}")
return "prompt00"
# Statistics methods
async def get_pool_stats(self) -> PoolStatsResponse:
"""Get statistics about the prompt pool."""
pool = await self.get_prompts_pool()
total_prompts = len(pool)
prompts_per_session = await self._get_setting('num_prompts', settings.NUM_PROMPTS_PER_SESSION)
target_pool_size = await self._get_setting('cached_pool_volume', settings.CACHED_POOL_VOLUME)
available_sessions = total_prompts // prompts_per_session if prompts_per_session > 0 else 0
needs_refill = total_prompts < target_pool_size
return PoolStatsResponse(
total_prompts=total_prompts,
prompts_per_session=prompts_per_session,
target_pool_size=target_pool_size,
available_sessions=available_sessions,
needs_refill=needs_refill
)
async def get_history_stats(self) -> HistoryStatsResponse:
"""Get statistics about prompt history."""
historic_prompts = await self.get_prompts_historic()
total_prompts = len(historic_prompts)
history_capacity = settings.HISTORY_BUFFER_SIZE
available_slots = max(0, history_capacity - total_prompts)
is_full = total_prompts >= history_capacity
return HistoryStatsResponse(
total_prompts=total_prompts,
history_capacity=history_capacity,
available_slots=available_slots,
is_full=is_full
)
async def get_prompt_history(self, limit: Optional[int] = None) -> List[PromptResponse]:
"""
Get prompt history.
Args:
limit: Maximum number of history items to return
Returns:
List of historical prompts
"""
historic_prompts = await self.get_prompts_historic()
if limit is not None:
historic_prompts = historic_prompts[:limit]
prompts = []
for i, prompt_dict in enumerate(historic_prompts):
prompt_key = list(prompt_dict.keys())[0]
prompt_text = prompt_dict[prompt_key]
prompts.append(PromptResponse(
key=prompt_key,
text=prompt_text,
position=i
))
return prompts
# Feedback operations
async def generate_theme_feedback_words(self) -> List[str]:
"""Generate 6 theme feedback words using AI."""
feedback_template = await self.get_feedback_template()
if not feedback_template:
raise ValueError("Feedback template not found")
historic_prompts = await self.get_prompts_historic()
if not historic_prompts:
raise ValueError("No historic prompts available for feedback analysis")
current_feedback_words = await self.get_feedback_words()
historic_feedback_words = await self.get_feedback_historic()
theme_words = await self.ai_service.generate_theme_feedback_words(
feedback_template=feedback_template,
historic_prompts=historic_prompts,
current_feedback_words=current_feedback_words,
historic_feedback_words=historic_feedback_words
)
return theme_words
async def update_feedback_words(self, ratings: Dict[str, int]) -> List[FeedbackWord]:
"""
Update feedback words with new ratings.
Args:
ratings: Dictionary of word to rating (0-6)
Returns:
Updated feedback words
"""
if len(ratings) != 6:
raise ValueError(f"Expected 6 ratings, got {len(ratings)}")
feedback_items = []
for i, (word, rating) in enumerate(ratings.items()):
if not 0 <= rating <= 6:
raise ValueError(f"Rating for '{word}' must be between 0 and 6, got {rating}")
feedback_key = f"feedback{i:02d}"
feedback_items.append({
feedback_key: word,
"weight": rating
})
# Update cache and save
self._feedback_words_cache = feedback_items
await self.data_service.save_feedback_words(feedback_items)
# Also add to historic feedback
await self._add_feedback_words_to_history(feedback_items)
# Convert to FeedbackWord models
feedback_words = []
for item in feedback_items:
key = list(item.keys())[0]
word = item[key]
weight = item["weight"]
feedback_words.append(FeedbackWord(key=key, word=word, weight=weight))
logger.info(f"Updated feedback words with {len(feedback_words)} items")
return feedback_words
async def _add_feedback_words_to_history(self, feedback_items: List[Dict[str, Any]]) -> None:
"""Add feedback words to historic buffer."""
historic_feedback = await self.get_feedback_historic()
# Extract just the words from current feedback
new_feedback_words = []
for i, item in enumerate(feedback_items):
feedback_key = f"feedback{i:02d}"
if feedback_key in item:
word = item[feedback_key]
new_feedback_words.append({feedback_key: word})
if len(new_feedback_words) != 6:
logger.warning(f"Expected 6 feedback words, got {len(new_feedback_words)}. Not adding to history.")
return
# Shift all existing feedback words down by 6 positions
updated_feedback_historic = new_feedback_words
# Add all existing feedback words, shifting their numbers down by 6
for i, feedback_dict in enumerate(historic_feedback):
if i >= settings.FEEDBACK_HISTORY_SIZE - 6: # Keep only FEEDBACK_HISTORY_SIZE items
break
feedback_key = list(feedback_dict.keys())[0]
word = feedback_dict[feedback_key]
new_feedback_key = f"feedback{i+6:02d}"
updated_feedback_historic.append({new_feedback_key: word})
# Update cache and save
self._feedback_historic_cache = updated_feedback_historic
await self.data_service.save_feedback_historic(updated_feedback_historic)
logger.info(f"Added 6 feedback words to history, history size: {len(updated_feedback_historic)}")
# Utility methods for API endpoints
def get_pool_size(self) -> int:
"""Get current pool size (synchronous for API endpoints)."""
if self._prompts_pool_cache is None:
raise RuntimeError("Pool cache not initialized")
return len(self._prompts_pool_cache)
def get_target_volume(self) -> int:
"""Get target pool volume (synchronous for API endpoints)."""
return settings.CACHED_POOL_VOLUME

88
backend/main.py Normal file
View File

@@ -0,0 +1,88 @@
"""
Daily Journal Prompt Generator - FastAPI Backend
Main application entry point
"""
import os
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from contextlib import asynccontextmanager
from app.api.v1.api import api_router
from app.core.config import settings
from app.core.logging import setup_logging
from app.core.exception_handlers import setup_exception_handlers
# Setup logging
logger = setup_logging()
@asynccontextmanager
async def lifespan(app: FastAPI):
"""Lifespan context manager for startup and shutdown events."""
# Startup
logger.info("Starting Daily Journal Prompt Generator API")
logger.info(f"Environment: {settings.ENVIRONMENT}")
logger.info(f"Debug mode: {settings.DEBUG}")
# Create data directory if it doesn't exist
data_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), "data")
os.makedirs(data_dir, exist_ok=True)
logger.info(f"Data directory: {data_dir}")
yield
# Shutdown
logger.info("Shutting down Daily Journal Prompt Generator API")
# Create FastAPI app
app = FastAPI(
title="Daily Journal Prompt Generator API",
description="API for generating and managing journal writing prompts",
version="1.0.0",
docs_url="/docs" if settings.DEBUG else None,
redoc_url="/redoc" if settings.DEBUG else None,
lifespan=lifespan
)
# Setup exception handlers
setup_exception_handlers(app)
# Configure CORS
if settings.BACKEND_CORS_ORIGINS:
app.add_middleware(
CORSMiddleware,
allow_origins=[str(origin) for origin in settings.BACKEND_CORS_ORIGINS],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Include API router
app.include_router(api_router, prefix="/api/v1")
@app.get("/")
async def root():
"""Root endpoint with API information."""
return {
"name": "Daily Journal Prompt Generator API",
"version": "1.0.0",
"description": "API for generating and managing journal writing prompts",
"docs": "/docs" if settings.DEBUG else None,
"health": "/health"
}
@app.get("/health")
async def health_check():
"""Health check endpoint."""
return {"status": "healthy", "service": "daily-journal-prompt-api"}
if __name__ == "__main__":
import uvicorn
uvicorn.run(
"main:app",
host=settings.HOST,
port=settings.PORT,
reload=settings.DEBUG,
log_level="info"
)

8
backend/requirements.txt Normal file
View File

@@ -0,0 +1,8 @@
fastapi>=0.104.0
uvicorn[standard]>=0.24.0
pydantic>=2.0.0
pydantic-settings>=2.0.0
python-dotenv>=1.0.0
openai>=1.0.0
aiofiles>=23.0.0