from __future__ import annotations import logging from app.clients.llm_router import chat_completion from app.config import settings from app.services.model_picker import pick_general_purpose_model LOG = logging.getLogger(__name__) _ROLE_WRITER_SYSTEM = ( "You are a helpful assistant that creates role prompts for LLM participants. " "Respond ONLY with the finished role prompt text — no reasoning, analysis, " "draft notes, questions to the user, or meta-commentary.\n\n" "STRICT RULES:\n" "- Do NOT invent a personal name unless one is explicitly provided in the name field.\n" "- Do NOT invent hobbies, side interests, employers, blogs, websites, or domains " "not stated in the user's input.\n" "- Do NOT add conversational filler or questions (e.g. 'What do you think?').\n" "- Write in second person ('You are…') so another LLM can embody the persona." ) _ANTI_INVENTION_AI = ( "\n\nIMPORTANT: You may elaborate on tone, speech patterns, and professional style " "implied by the stated identity, but stay strictly within the user's described domain. " "Do NOT add unrelated interests, fictional backstory, or new subject areas." ) _ANTI_INVENTION_EXACT = ( "\n\nIMPORTANT: Use ONLY the information explicitly provided. Do not invent names " "(unless given in the name field), hobbies, employers, blogs, or other facts." ) # --------------------------------------------------------------------------- # Structured input prompts # --------------------------------------------------------------------------- STRUCTURED_AI_COMPLETED_PROMPT = ( "You will receive structured information about a character or persona: a name, an identity " "statement, a profile, and optionally writing/speech samples. Some fields may be sparse or " "missing. Write a complete, vivid 3-5 sentence role prompt that an LLM can use to " "convincingly embody this persona in a conversation.\n\n" "If any fields are sparse, infer plausible personality traits, speech patterns, and " "conversational style from whatever clues are available — but only within the professional " "or personal domain the user described. Fill in realistic detail so the role prompt is " "rich and actionable — never produce a vague or skeletal prompt.\n\n" "Cover: personality, tone and speech patterns, background/expertise, and how they would " "naturally interact in a group discussion.\n\n" "The name is: {name}\n" "The identity statement is: {identity}\n" "The profile is: {profile}\n" "Here are the writing and/or speech samples: {samples}" + _ANTI_INVENTION_AI ) STRUCTURED_EXACT_PROMPT = ( "You will receive structured information about a character or persona: a name, an identity " "statement, a profile, and optionally writing/speech samples. Combine this information into " "a coherent 3-5 sentence role prompt that an LLM can use to embody this persona in a " "conversation.\n\n" "IMPORTANT: Use ONLY the information explicitly provided. Do not invent, assume, or infer " "any traits, background, opinions, or speech patterns beyond what is stated. Your job is " "purely to organize and lightly rephrase the provided facts into a smooth, usable role " "prompt — add linking words and natural sentence flow, but no new content. If a field is " "empty or says '(not provided)', simply omit it.\n\n" "The name is: {name}\n" "The identity statement is: {identity}\n" "The profile is: {profile}\n" "Here are the writing and/or speech samples: {samples}" + _ANTI_INVENTION_EXACT ) # --------------------------------------------------------------------------- # Freeform input prompts # --------------------------------------------------------------------------- FREEFORM_AI_COMPLETED_PROMPT = ( "You will receive freeform information about a character or persona. The input may be " "detailed (with writing samples, background, etc.) or very brief (just a name or a short " "description). Regardless of how much is provided, write a complete, vivid 3-5 sentence " "role prompt that an LLM can use to convincingly embody this persona in a conversation.\n\n" "If the input is sparse, infer plausible personality traits, speech patterns, and " "conversational style from whatever clues are available (the name, any title or " "occupation, context, etc.) — but only within the domain the user described. Fill in " "realistic detail so the role prompt is rich and actionable — never produce a vague or " "skeletal prompt.\n\n" "Cover: personality, tone and speech patterns, background/expertise, and how they would " "naturally interact in a group discussion.\n\n" "The persona's name is: {name}\n\n" "Here is everything provided about this persona:\n" "---\n{text}\n---" + _ANTI_INVENTION_AI ) FREEFORM_EXACT_PROMPT = ( "You will receive freeform information about a character or persona. Combine this " "information into a coherent 3-5 sentence role prompt that an LLM can use to embody " "this persona in a conversation.\n\n" "IMPORTANT: Use ONLY the information explicitly provided. Do not invent, assume, or infer " "any traits, background, opinions, or speech patterns beyond what is stated. Your job is " "purely to organize and lightly rephrase the user's text into a smooth, usable role " "prompt — add linking words and natural sentence flow, but no new content. If very little " "was provided, the role prompt should be correspondingly brief.\n\n" "The persona's name is: {name}\n\n" "Here is everything provided about this persona:\n" "---\n{text}\n---" + _ANTI_INVENTION_EXACT ) async def _call_llm(model_id: str, prompt_text: str) -> dict: resolved = settings.resolve_model(model_id) if not resolved: return { "role_prompt": "", "error": f"No neutral model available to generate the role prompt.", } messages = [ {"role": "system", "content": _ROLE_WRITER_SYSTEM}, {"role": "user", "content": prompt_text}, ] result = await chat_completion( resolved=resolved, messages=messages, temperature=0.7, max_tokens=512, timeout=45, ) if result.get("error"): return {"role_prompt": "", "error": result["response"]} return { "role_prompt": result["response"], "elapsed_seconds": result["elapsed_seconds"], "writer_model_id": model_id, } async def _resolve_writer_model( orchestrator_model_id: str | None, extra_model_ids: list[str] | None, ) -> str | None: return pick_general_purpose_model( orchestrator_model_id, extra_model_ids=extra_model_ids, ) async def generate_role_prompt( name: str, profile: str, identity: str, samples: str, role_style: str = "exact", orchestrator_model_id: str | None = None, extra_model_ids: list[str] | None = None, ) -> dict: """Distill structured persona inputs into a role prompt via a neutral writer model.""" writer_id = await _resolve_writer_model(orchestrator_model_id, extra_model_ids) if not writer_id: return {"role_prompt": "", "error": "No model available to generate the role prompt."} template = STRUCTURED_AI_COMPLETED_PROMPT if role_style == "ai_completed" else STRUCTURED_EXACT_PROMPT prompt_text = template.format( name=name or "(not provided)", identity=identity or "(not provided)", profile=profile or "(not provided)", samples=samples or "(not provided)", ) return await _call_llm(writer_id, prompt_text) async def generate_role_prompt_freeform( name: str, text: str, role_style: str = "ai_completed", orchestrator_model_id: str | None = None, extra_model_ids: list[str] | None = None, ) -> dict: """Distill a freeform text block into a role prompt via a neutral writer model.""" writer_id = await _resolve_writer_model(orchestrator_model_id, extra_model_ids) if not writer_id: return {"role_prompt": "", "error": "No model available to generate the role prompt."} template = FREEFORM_AI_COMPLETED_PROMPT if role_style == "ai_completed" else FREEFORM_EXACT_PROMPT prompt_text = template.format( name=name or "(not provided)", text=text or "(not provided)", ) return await _call_llm(writer_id, prompt_text)