diff --git a/app/services/ai/config.py b/app/services/ai/config.py index 7810b6f5..4b5e5bf6 100644 --- a/app/services/ai/config.py +++ b/app/services/ai/config.py @@ -471,6 +471,27 @@ def get_best_embedding_provider(self) -> Optional['EmbeddingProvider']: _config: Optional[AIConfig] = None +# Symbolic tiers a prompt YAML can name instead of a concrete model id. +# Hardcoding ids in ~20 prompt files made retiring a model a sweep across all of +# them; naming a tier keeps the id in AIConfig, where one edit moves everything. +MODEL_TIERS = ('default', 'quality', 'fast') + + +def resolve_model_name(name: str) -> 'AIModel': + """Resolve a prompt's ``model`` field to an AIModel. + + Accepts a tier (``default`` / ``quality`` / ``fast``), which is looked up on + the active config, or a concrete model id / alias. + + Raises ValueError or KeyError for anything unrecognised, matching + ``AIModel.from_string`` so callers keep one error path. + """ + key = (name or '').strip().lower() + if key in MODEL_TIERS: + return getattr(get_ai_config(), f'{key}_model') + return AIModel.from_string(name) + + def get_ai_config() -> AIConfig: """ Get the singleton AI configuration instance. diff --git a/app/services/ai/prompt_service.py b/app/services/ai/prompt_service.py index 176b2f8c..c40fd2fe 100644 --- a/app/services/ai/prompt_service.py +++ b/app/services/ai/prompt_service.py @@ -45,12 +45,28 @@ import logging from typing import Dict, Any, Optional, List, Tuple from pathlib import Path -from app.services.ai.config import AIModel, AIProvider as AIProviderEnum, get_ai_config +from app.services.ai.config import (AIModel, AIProvider as AIProviderEnum, + get_ai_config, resolve_model_name) from app.models.user_ai_preferences import UserAIPreference logger = logging.getLogger(__name__) +def _provider_for_display(prompt_data: Dict[str, Any]) -> Optional[str]: + """Provider name for the settings UI, derived from the prompt's model. + + Prompts no longer declare a provider — the model carries it — but the + settings screens still want to show one. + """ + model_str = prompt_data.get('model') + if model_str: + try: + return resolve_model_name(model_str).provider.value + except (ValueError, KeyError): + pass + return prompt_data.get('preferred_provider') + + class PromptService: """ Central service for managing and loading LLM prompts. @@ -206,7 +222,7 @@ def get_prompt_info(self, category: str, name: str) -> Dict[str, Any]: 'category': category, 'max_tokens': prompt_data.get('max_tokens', 2000), 'temperature': prompt_data.get('temperature', 0.7), - 'preferred_provider': prompt_data.get('preferred_provider'), + 'preferred_provider': _provider_for_display(prompt_data), 'model': prompt_data.get('model'), 'required_variables': self._extract_variables(template), 'has_system_context': 'system_context' in prompt_data, @@ -373,10 +389,19 @@ def resolve_model_provider( model_str = metadata.get('model') if model_str: try: - model_enum = AIModel.from_string(model_str) + # Tier names ('quality', 'fast', ...) resolve through AIConfig, so a + # prompt does not have to name a concrete model id. + model_enum = resolve_model_name(model_str) except (ValueError, KeyError): logger.warning(f"Unknown model in prompt metadata: {model_str}") + # The provider comes from the model, which already carries it. A prompt that + # also declared `preferred_provider` could contradict its own model, and once + # prompts name a tier the YAML cannot know what that tier resolves to. + if model_enum is not None: + return model_enum, model_enum.provider + + # No model named: a bare provider still selects one, using its default model. provider_str = metadata.get('preferred_provider') if provider_str: try: @@ -445,7 +470,7 @@ def get_effective_model_display( info = prompt_service.get_prompt_info(prompt_category, prompts[0]) model_str = info.get('model') if model_str: - model = AIModel.from_string(model_str) + model = resolve_model_name(model_str) return _MODEL_DISPLAY_NAMES.get(model.model_id, model.model_id) except (ValueError, KeyError): pass diff --git a/app/services/ai/prompts/argos/explanation.yaml b/app/services/ai/prompts/argos/explanation.yaml index e71af215..2f832f3d 100644 --- a/app/services/ai/prompts/argos/explanation.yaml +++ b/app/services/ai/prompts/argos/explanation.yaml @@ -6,8 +6,7 @@ description: "Generates detailed explanation for a specific Argos insight" version: "1.0" category: "argos" -preferred_provider: "gemini" -model: "gemini-3-flash-preview" +model: "quality" max_tokens: 5500 temperature: 0.5 diff --git a/app/services/ai/prompts/argos/relevance_scoring.yaml b/app/services/ai/prompts/argos/relevance_scoring.yaml index bff51a78..db1c5f6a 100644 --- a/app/services/ai/prompts/argos/relevance_scoring.yaml +++ b/app/services/ai/prompts/argos/relevance_scoring.yaml @@ -6,8 +6,7 @@ description: "Scores insight candidates for relevance to current research" version: "1.0" category: "argos" -preferred_provider: "gemini" -model: "gemini-3-flash-preview" +model: "quality" max_tokens: 7000 temperature: 0.3 diff --git a/app/services/ai/prompts/checkpoint/checkpoint_analysis.yaml b/app/services/ai/prompts/checkpoint/checkpoint_analysis.yaml index 54f589cf..f2fb3c1a 100644 --- a/app/services/ai/prompts/checkpoint/checkpoint_analysis.yaml +++ b/app/services/ai/prompts/checkpoint/checkpoint_analysis.yaml @@ -7,8 +7,7 @@ version: "1.0" category: "checkpoint" # AI Provider Configuration -preferred_provider: "gemini" -model: "gemini-3-flash-preview" +model: "quality" max_tokens: 1500 temperature: 0.4 diff --git a/app/services/ai/prompts/companion/companion_agent.yaml b/app/services/ai/prompts/companion/companion_agent.yaml index 55de9f90..98526c0e 100644 --- a/app/services/ai/prompts/companion/companion_agent.yaml +++ b/app/services/ai/prompts/companion/companion_agent.yaml @@ -3,8 +3,7 @@ description: "Global agentic companion — answers grounded in the user's own ac version: "1.0" category: "companion" -preferred_provider: "gemini" -model: "gemini-3-flash-preview" +model: "quality" # Per-turn output budget. Tool-call turns are naturally short; this headroom is for # the final answer when a question spans several sources and needs real synthesis. max_tokens: 4096 diff --git a/app/services/ai/prompts/companion/counter_evidence.yaml b/app/services/ai/prompts/companion/counter_evidence.yaml index 977bc6ed..64393321 100644 --- a/app/services/ai/prompts/companion/counter_evidence.yaml +++ b/app/services/ai/prompts/companion/counter_evidence.yaml @@ -3,8 +3,7 @@ description: "Surface disconfirming evidence and questions that challenge a rese version: "1.0" category: "companion" -preferred_provider: "gemini" -model: "gemini-3-flash-preview" +model: "quality" max_tokens: 6000 temperature: 0.3 diff --git a/app/services/ai/prompts/companion/knowledge_summary.yaml b/app/services/ai/prompts/companion/knowledge_summary.yaml index c82a6206..c53d946a 100644 --- a/app/services/ai/prompts/companion/knowledge_summary.yaml +++ b/app/services/ai/prompts/companion/knowledge_summary.yaml @@ -3,8 +3,7 @@ description: "Compress one piece of a user's research knowledge into a short, em version: "1.0" category: "companion" -preferred_provider: "gemini" -model: "gemini-3-flash-preview" +model: "fast" max_tokens: 120 temperature: 0.2 diff --git a/app/services/ai/prompts/companion/live_companion.yaml b/app/services/ai/prompts/companion/live_companion.yaml index f0b91cec..e4224bc6 100644 --- a/app/services/ai/prompts/companion/live_companion.yaml +++ b/app/services/ai/prompts/companion/live_companion.yaml @@ -3,8 +3,7 @@ description: "Answer research questions during a session, surfacing facts not op version: "1.0" category: "companion" -preferred_provider: "gemini" -model: "gemini-3-flash-preview" +model: "quality" max_tokens: 6000 temperature: 0.3 diff --git a/app/services/ai/prompts/companion/research_brief.yaml b/app/services/ai/prompts/companion/research_brief.yaml index f9d657f1..d398a203 100644 --- a/app/services/ai/prompts/companion/research_brief.yaml +++ b/app/services/ai/prompts/companion/research_brief.yaml @@ -3,8 +3,7 @@ description: "Generate a pre-session research brief for a research step" version: "1.0" category: "companion" -preferred_provider: "gemini" -model: "gemini-3-flash-preview" +model: "quality" max_tokens: 8000 temperature: 0.3 diff --git a/app/services/ai/prompts/companion/session_wrapup.yaml b/app/services/ai/prompts/companion/session_wrapup.yaml index 0dfb3c8a..084600b4 100644 --- a/app/services/ai/prompts/companion/session_wrapup.yaml +++ b/app/services/ai/prompts/companion/session_wrapup.yaml @@ -3,8 +3,7 @@ description: "Summarize a research session: accomplishments, gaps, next steps" version: "1.0" category: "companion" -preferred_provider: "gemini" -model: "gemini-3-flash-preview" +model: "quality" max_tokens: 6000 temperature: 0.3 diff --git a/app/services/ai/prompts/intelligence/decision_review.yaml b/app/services/ai/prompts/intelligence/decision_review.yaml index 09d684b0..607a5230 100644 --- a/app/services/ai/prompts/intelligence/decision_review.yaml +++ b/app/services/ai/prompts/intelligence/decision_review.yaml @@ -7,7 +7,6 @@ version: "1.0" category: "intelligence" # AI Provider Configuration -preferred_provider: "claude" model: "claude-sonnet-4-20250514" max_tokens: 1000 temperature: 0.6 diff --git a/app/services/ai/prompts/intelligence/pattern_explanation.yaml b/app/services/ai/prompts/intelligence/pattern_explanation.yaml index 43c4c4f2..4a6b5824 100644 --- a/app/services/ai/prompts/intelligence/pattern_explanation.yaml +++ b/app/services/ai/prompts/intelligence/pattern_explanation.yaml @@ -7,7 +7,6 @@ version: "1.0" category: "intelligence" # AI Provider Configuration -preferred_provider: "claude" model: "claude-sonnet-4-20250514" max_tokens: 500 temperature: 0.5 diff --git a/app/services/ai/prompts/intelligence/research_qa.yaml b/app/services/ai/prompts/intelligence/research_qa.yaml index c118877b..3b8615e7 100644 --- a/app/services/ai/prompts/intelligence/research_qa.yaml +++ b/app/services/ai/prompts/intelligence/research_qa.yaml @@ -7,8 +7,7 @@ version: "1.0" category: "intelligence" # AI Provider Configuration -preferred_provider: "gemini" # Cost-effective for document Q&A -model: "gemini-2.5-flash" +model: "fast" max_tokens: 1000 temperature: 0.2 diff --git a/app/services/ai/prompts/intelligence/text_summarization.yaml b/app/services/ai/prompts/intelligence/text_summarization.yaml index 1d132e0a..3b27bf53 100644 --- a/app/services/ai/prompts/intelligence/text_summarization.yaml +++ b/app/services/ai/prompts/intelligence/text_summarization.yaml @@ -7,8 +7,7 @@ version: "1.0" category: "intelligence" # AI Provider Configuration -preferred_provider: "gemini" # Cost-effective for summarization -model: "gemini-2.5-flash" +model: "fast" max_tokens: 800 temperature: 0.3 diff --git a/app/services/ai/prompts/intelligence/thesis_analysis.yaml b/app/services/ai/prompts/intelligence/thesis_analysis.yaml index 2fddbc3a..217c5a50 100644 --- a/app/services/ai/prompts/intelligence/thesis_analysis.yaml +++ b/app/services/ai/prompts/intelligence/thesis_analysis.yaml @@ -7,7 +7,6 @@ version: "1.0" category: "intelligence" # AI Provider Configuration -preferred_provider: "claude" # Can be overridden in ai_routing.yaml model: "claude-sonnet-4-20250514" max_tokens: 2000 temperature: 0.3 diff --git a/app/services/ai/prompts/intelligence/thesis_analysis_simple.yaml b/app/services/ai/prompts/intelligence/thesis_analysis_simple.yaml index 8f2328fd..4e90e3bd 100644 --- a/app/services/ai/prompts/intelligence/thesis_analysis_simple.yaml +++ b/app/services/ai/prompts/intelligence/thesis_analysis_simple.yaml @@ -7,7 +7,6 @@ version: "1.0" category: "intelligence" # AI Provider Configuration -preferred_provider: "claude" model: "claude-sonnet-4-20250514" max_tokens: 2000 temperature: 0.3 diff --git a/app/services/ai/prompts/intelligence/warning_generation.yaml b/app/services/ai/prompts/intelligence/warning_generation.yaml index c1d2a1cd..6182dbab 100644 --- a/app/services/ai/prompts/intelligence/warning_generation.yaml +++ b/app/services/ai/prompts/intelligence/warning_generation.yaml @@ -7,7 +7,6 @@ version: "1.0" category: "intelligence" # AI Provider Configuration -preferred_provider: "claude" model: "claude-sonnet-4-20250514" max_tokens: 800 temperature: 0.5 diff --git a/app/services/ai/prompts/portfolio/portfolio_complete_analysis.yaml b/app/services/ai/prompts/portfolio/portfolio_complete_analysis.yaml index eea4f5b3..552f5423 100644 --- a/app/services/ai/prompts/portfolio/portfolio_complete_analysis.yaml +++ b/app/services/ai/prompts/portfolio/portfolio_complete_analysis.yaml @@ -6,8 +6,7 @@ description: "AI-powered analysis of portfolio data to extract behavioral insigh version: "1.0" category: "intelligence" -preferred_provider: "gemini" -model: "gemini-3-flash-preview" +model: "quality" max_tokens: 10000 temperature: 0.5 diff --git a/app/services/ai/prompts/portfolio/portfolio_raw_trade_analysis.yaml b/app/services/ai/prompts/portfolio/portfolio_raw_trade_analysis.yaml index 4be9cfe4..d0305439 100644 --- a/app/services/ai/prompts/portfolio/portfolio_raw_trade_analysis.yaml +++ b/app/services/ai/prompts/portfolio/portfolio_raw_trade_analysis.yaml @@ -6,8 +6,7 @@ description: "Deep behavioral analysis from raw transactions with tax context, F version: "1.0" category: "portfolio" -preferred_provider: "gemini" -model: "gemini-3-flash-preview" +model: "quality" max_tokens: 65000 temperature: 0.3 diff --git a/app/services/ai/prompts/portfolio/sector_momentum_analysis.yaml b/app/services/ai/prompts/portfolio/sector_momentum_analysis.yaml index 0a23cee2..40fadf37 100644 --- a/app/services/ai/prompts/portfolio/sector_momentum_analysis.yaml +++ b/app/services/ai/prompts/portfolio/sector_momentum_analysis.yaml @@ -6,8 +6,7 @@ description: "Detect sector momentum chasing patterns using AI knowledge of hist version: "1.0" category: "portfolio" -preferred_provider: "gemini" -model: "gemini-3-flash-preview" +model: "quality" max_tokens: 10000 temperature: 0.3 diff --git a/app/services/ai/prompts/portfolio/tax_optimization_analysis.yaml b/app/services/ai/prompts/portfolio/tax_optimization_analysis.yaml index 370c182d..0e006b95 100644 --- a/app/services/ai/prompts/portfolio/tax_optimization_analysis.yaml +++ b/app/services/ai/prompts/portfolio/tax_optimization_analysis.yaml @@ -7,8 +7,7 @@ description: "Tax optimization analysis adapted to investor's tax residence, wit version: "1.0" category: "portfolio" -preferred_provider: "gemini" -model: "gemini-3-flash-preview" +model: "quality" max_tokens: 10000 temperature: 0.3 diff --git a/app/services/ai/prompts/research/bias_check.yaml b/app/services/ai/prompts/research/bias_check.yaml index bfc74973..02d48042 100644 --- a/app/services/ai/prompts/research/bias_check.yaml +++ b/app/services/ai/prompts/research/bias_check.yaml @@ -6,8 +6,7 @@ description: "Analyze investment thesis for cognitive biases (Munger framework)" version: "1.0" category: "research" -preferred_provider: "gemini" -model: "gemini-3-flash-preview" +model: "quality" max_tokens: 10000 temperature: 0.3 diff --git a/app/services/ai/prompts/research/challenge_mode.yaml b/app/services/ai/prompts/research/challenge_mode.yaml index 453fe2a9..755c1c35 100644 --- a/app/services/ai/prompts/research/challenge_mode.yaml +++ b/app/services/ai/prompts/research/challenge_mode.yaml @@ -6,8 +6,7 @@ description: "Generates counter-arguments and critical questions to test user's version: "1.0" category: "research" -preferred_provider: "gemini" -model: "gemini-3-flash-preview" +model: "quality" max_tokens: 10000 temperature: 0.7 diff --git a/app/services/ai/prompts/research/checklist_item_analyze.yaml b/app/services/ai/prompts/research/checklist_item_analyze.yaml index 0ba66696..67ee97e4 100644 --- a/app/services/ai/prompts/research/checklist_item_analyze.yaml +++ b/app/services/ai/prompts/research/checklist_item_analyze.yaml @@ -6,8 +6,7 @@ description: "Analyzes financial documents and available sources to answer a spe version: "1.0" category: "research" -preferred_provider: "gemini" -model: "gemini-3-flash-preview" +model: "quality" max_tokens: 10000 temperature: 0.4 diff --git a/app/services/ai/prompts/research/elaboration_mode.yaml b/app/services/ai/prompts/research/elaboration_mode.yaml index c8a5ff35..03f2a1e3 100644 --- a/app/services/ai/prompts/research/elaboration_mode.yaml +++ b/app/services/ai/prompts/research/elaboration_mode.yaml @@ -6,8 +6,7 @@ description: "Generates follow-up questions and suggests deeper areas to explore version: "1.0" category: "research" -preferred_provider: "gemini" -model: "gemini-3-flash-preview" +model: "quality" max_tokens: 10000 temperature: 0.4 diff --git a/app/services/ai/prompts/research/factcheck_mode.yaml b/app/services/ai/prompts/research/factcheck_mode.yaml index 5837035f..9e04aa40 100644 --- a/app/services/ai/prompts/research/factcheck_mode.yaml +++ b/app/services/ai/prompts/research/factcheck_mode.yaml @@ -6,8 +6,7 @@ description: "Verifies claims in user research with evidence-based verdicts" version: "2.0" category: "research" -preferred_provider: "gemini" -model: "gemini-3-flash-preview" +model: "quality" max_tokens: 10000 temperature: 0.2 diff --git a/app/services/ai/prompts/screening/screening_analysis.yaml b/app/services/ai/prompts/screening/screening_analysis.yaml index c992471d..6bfe84ef 100644 --- a/app/services/ai/prompts/screening/screening_analysis.yaml +++ b/app/services/ai/prompts/screening/screening_analysis.yaml @@ -6,8 +6,7 @@ description: "Analyze kill checklist history to surface patterns and improvement version: "1.0" category: "screening" -preferred_provider: "gemini" -model: "gemini-2.5-flash" +model: "fast" max_tokens: 4000 temperature: 0.3 diff --git a/app/services/ai_research_assistant.py b/app/services/ai_research_assistant.py index 81c5a86a..87cbf4fc 100644 --- a/app/services/ai_research_assistant.py +++ b/app/services/ai_research_assistant.py @@ -59,7 +59,7 @@ from app.services.ai import ai_service from app.services.ai.prompt_service import prompt_service -from app.services.ai.config import AIProvider, AIModel +from app.services.ai.config import AIProvider, AIModel, resolve_model_name logger = logging.getLogger(__name__) @@ -292,21 +292,20 @@ def _generate_response( prompt_text = prompt_data['prompt'] metadata = prompt_data['metadata'] - # Convert provider and model from strings to enums - provider_str = metadata.get('preferred_provider') + # Convert the prompt's model to an enum. The provider is not read + # from the prompt — the model already carries it, and a separately + # declared provider could contradict it. model_str = metadata.get('model') provider_enum = None model_enum = None - if provider_str: - try: - provider_enum = AIProvider(provider_str) - except ValueError: - logger.warning(f"Invalid provider '{provider_str}', using default") - if model_str: - model_enum = AIModel.from_string(model_str) + # Prompts name a tier ('quality'/'fast'), not a model id. + model_enum = resolve_model_name(model_str) + provider_enum = model_enum.provider + + provider_str = provider_enum.value if provider_enum else None logger.info( f"Generating {mode} response for question: {question_text[:50]}... "