Spaces:
Running
Running
File size: 10,573 Bytes
e29c7ce addec5c e29c7ce ab0a14d e29c7ce 6afc342 31404a1 c57e87b e29c7ce bdc8662 e29c7ce ab0a14d e29c7ce 773e767 17ba573 e29c7ce bdc8662 addec5c bdc8662 addec5c e29c7ce addec5c e29c7ce ab0a14d 31c3c78 e29c7ce 31c3c78 e29c7ce 773e767 832430f ab0a14d 773e767 ab0a14d 832430f ab0a14d 832430f ab0a14d addec5c 6afc342 addec5c e29c7ce bdc8662 31c3c78 17ba573 31c3c78 bdc8662 832430f 17ba573 832430f bdc8662 e29c7ce bdc8662 e29c7ce c57e87b 6afc342 addec5c 832430f e29c7ce ab0a14d 832430f 773e767 e29c7ce ab0a14d bdc8662 17ba573 bdc8662 556ebc5 bdc8662 17ba573 bdc8662 556ebc5 bdc8662 ab0a14d 832430f ab0a14d e29c7ce 832430f addec5c e29c7ce bdc8662 832430f 17ba573 bdc8662 832430f e29c7ce 832430f addec5c 31404a1 addec5c c57e87b | 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 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 | """
Chat engine β core conversation loop.
Connects prompt_builder + memory + parser into a single streaming generator
that app.py (or any UI layer) can consume with `yield from`.
Separation of concerns:
- app.py : Gradio UI, OAuth token, slider values
- chat.py : LLM call, streaming, post-stream memory save
- prompt_builder.py : system prompt assembly
- memory.py : session-scoped snapshots and periodic memory extraction
- parser.py : raw response β typed segments β markdown
"""
import logging
from huggingface_hub import InferenceClient
from engine.memory import sync_history, append_exchange, bounded_recent_context
from engine.memory_jobs import schedule_memory_update
from engine.relationship import relationship_to_prompt
from engine.parser import parse_response, segments_to_markdown
from engine.prompt_builder import build_messages, load_character
from engine.translator import translate_from_english, translate_to_english
logger = logging.getLogger(__name__)
def _chat_completion(client, messages, **kwargs):
"""Adapt chat completion calls for HF InferenceClient or OpenAI HF Router."""
if isinstance(kwargs.get("model"), str) and kwargs["model"].endswith(":featherless-ai"):
model = kwargs.pop("model")
return client.chat.completions.create(model=model, messages=messages, **kwargs)
return client.chat_completion(messages, **kwargs)
def generate_response(
message: str,
character_id: str,
client: InferenceClient,
session_id: str,
history: list[dict],
persona: dict | None = None,
model: str = "Qwen/Qwen3-235B-A22B-Instruct-2507",
max_tokens: int = 512,
temperature: float = 0.7,
top_p: float = 0.95,
language: str = "English",
translation_model: str | None = None,
use_translator: bool = False,
):
"""
Core streaming generator.
Yields the accumulated raw response after each token arrives so Gradio
can display it in real time. After the stream ends, yields one final
formatted version (actions italic, thoughts em-dashed) and saves
the exchange to session-scoped memory.
All errors are caught and surfaced as in-chat messages so the UI
never silently breaks.
Translation modes (when language β English):
use_translator=True β dedicated translation_model translates user input
to English and response back to user's language.
Main model always operates in English.
use_translator=False β no separate model. Main model receives the language
instruction directly and responds in user's language.
Args:
message: Current user input.
character_id: Which character to respond as.
client: Initialised HuggingFace InferenceClient.
persona: Optional user persona dict.
model: HuggingFace model string.
max_tokens: Maximum tokens to generate.
temperature: Sampling temperature.
top_p: Nucleus sampling probability.
language: UI language selected by the user.
translation_model: Model used when use_translator=True.
use_translator: True = separate translator, False = main model handles language.
"""
logger.info(f"[Chat] βΆ character='{character_id}' | language='{language}' | model='{model}'")
# ββ Load character βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
try:
character = load_character(character_id)
logger.debug(f"[Chat] Character loaded: '{character.get('name', character_id)}'")
except FileNotFoundError:
logger.error(f"[Chat] Character not found: {character_id}")
yield f"*(Character '{character_id}' could not be loaded.)*"
return
except Exception as e:
logger.error(f"[Chat] Unexpected error loading character: {e}", exc_info=True)
yield "*(Something went wrong loading the character. Check the logs.)*"
return
# Synchronize the visible transcript before assembling model context.
try:
conversation = sync_history(session_id, character_id, history)
except Exception:
logger.exception("Could not synchronize conversation memory.")
yield "*(Conversation history could not be loaded. Please try again.)*"
return
recent_messages, context_omitted = bounded_recent_context(conversation)
summary = conversation['summary']
long_term_memory = conversation['long_term_memory']
# ββ Translation mode? ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
is_non_english = language.strip().lower() != "english"
use_translation = use_translator and is_non_english and bool(translation_model)
translation_client = client
if use_translation and isinstance(model, str) and model.endswith(":featherless-ai"):
from huggingface_hub import InferenceClient
translation_client = InferenceClient(token=getattr(client, "api_key", None), timeout=60)
if is_non_english and not use_translator:
logger.info(f"[Chat] Translation: OFF β main model will respond in '{language}' directly")
elif use_translation:
logger.info(f"[Chat] Translation: ON β model='{translation_model}'")
else:
logger.info("[Chat] Translation: not needed (language is English)")
if use_translation:
# Translate user message β English before sending to main model
logger.debug(f"[Chat] Translating user message: {message[:80]!r}")
en_message = translate_to_english(message, language, translation_client, translation_model)
logger.debug(f"[Chat] User message (EN): {en_message[:80]!r}")
prompt_language = "English" # main model always responds in English
else:
en_message = message
prompt_language = language
# ββ Build prompt βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
messages = build_messages(
character=character,
persona=persona,
summary=summary,
long_term_memory=long_term_memory,
recent_messages=recent_messages,
current_message=en_message,
language=prompt_language,
)
messages[0]['content'] += '\n\n' + relationship_to_prompt(conversation['relationship'])
if context_omitted:
messages[0]['content'] += ('\n\nSome unsummarized history is temporarily omitted to keep context bounded. '
'Do not invent details from missing turns.')
if use_translation:
messages[0]['content'] += "\n\nReply in English, even if earlier messages use another language."
logger.debug(
f"[Chat] Prompt built: {len(messages[0]['content'])} chars system | "
f"{len(messages)} total messages"
)
# ββ LLM call ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
logger.info(f"[Chat] LLM call: model='{model}' max_tokens={max_tokens} temp={temperature:.2f} top_p={top_p:.2f}")
raw_response = ""
try:
if use_translation:
# Collect silently β yield placeholder while model generates
yield "*(β¦)*"
for chunk in _chat_completion(
client,
messages,
model=model,
max_tokens=max_tokens,
stream=True,
temperature=temperature,
top_p=top_p,
frequency_penalty=0.3,
):
if chunk.choices and chunk.choices[0].delta.content:
raw_response += chunk.choices[0].delta.content
else:
# Stream tokens in real time
for chunk in _chat_completion(
client,
messages,
model=model,
max_tokens=max_tokens,
stream=True,
temperature=temperature,
top_p=top_p,
frequency_penalty=0.3,
):
if chunk.choices and chunk.choices[0].delta.content:
raw_response += chunk.choices[0].delta.content
yield raw_response
except Exception as e:
logger.error(f"[Chat] LLM error: {e}", exc_info=True)
error_note = "\n\n*(The model did not respond. Please try again.)*"
yield (raw_response + error_note) if raw_response else error_note.strip()
return
logger.info(f"[Chat] Stream complete: {len(raw_response)} chars generated")
if not raw_response.strip():
yield "*(The model returned an empty response. Please try again.)*"
return
# ββ Translate response β user's language, format, yield βββββββββββββββββββ
if use_translation:
logger.debug(f"[Chat] Translating response β {language} ({len(raw_response)} chars)")
final_text = translate_from_english(raw_response, language, translation_client, translation_model)
else:
final_text = raw_response
segments = parse_response(final_text)
logger.debug(f"[Chat] Parser: {len(segments)} segments β {[s['type'] for s in segments]}")
formatted = segments_to_markdown(segments)
logger.info(f"[Chat] β Response ready: {len(formatted)} chars, {len(segments)} segments")
# Store exactly what the user sees, including translated text.
try:
saved = append_exchange(session_id, character_id, conversation, message, formatted)
except Exception:
logger.exception("Could not save conversation memory.")
yield formatted + "\n\n*(This response could not be saved. Please try again.)*"
return
if not saved:
yield "*(Conversation changed while generating. Please try again.)*"
return
yield formatted
schedule_memory_update(session_id, character_id, saved, client, model, persona=persona)
|