File size: 8,505 Bytes
c44fab6
 
 
 
 
 
1961f74
c44fab6
 
 
1961f74
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c44fab6
 
 
 
 
 
 
 
 
1961f74
 
 
 
 
 
c44fab6
 
 
 
1961f74
c44fab6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1961f74
c44fab6
 
 
 
 
 
 
 
 
 
 
1961f74
 
 
 
 
 
 
c44fab6
 
 
1961f74
c44fab6
 
 
 
 
 
 
 
 
 
 
 
 
 
1961f74
c44fab6
 
 
 
 
 
1961f74
 
 
 
c44fab6
 
1961f74
c44fab6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1961f74
c44fab6
 
 
1961f74
 
 
 
 
 
 
 
 
 
c44fab6
 
 
 
 
 
1961f74
 
c44fab6
1961f74
 
 
 
 
c44fab6
 
 
 
 
 
 
1961f74
c44fab6
 
 
 
 
 
1961f74
 
c44fab6
1961f74
 
 
 
 
c44fab6
 
 
 
 
1961f74
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
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)