66from ...schema .utils import asUUID
77from ...schema .result import Result
88from ...llm .agent import task as AT
9- from ...llm .complete import llm_complete
109from ...env import LOG
1110from ...schema .config import ProjectConfig
1211from ...telemetry .log import get_wide_event
1312from ...telemetry .get_metrics import get_metrics
1413from ...constants import ExcessMetricTags
15- from ..data import session as SD
16-
17- TITLE_INPUT_MAX_CHARS = 512
18- TITLE_INPUT_MIN_CHARS = 12
19- TITLE_GENERATION_MAX_TOKENS = 24
20- TITLE_OUTPUT_MAX_CHARS = 80
21- NON_INFORMATIVE_TITLE_INPUTS = {
22- "hi" ,
23- "hello" ,
24- "hey" ,
25- "ok" ,
26- "okay" ,
27- "thanks" ,
28- "thank you" ,
29- "test" ,
30- "testing" ,
31- }
32- TITLE_GENERATION_SYSTEM_PROMPT = """You generate concise session titles.
33- Given a user's first message, return one short, informative title.
34- Rules:
35- - 3 to 8 words.
36- - Use plain text only.
37- - Do not use quotes.
38- - Do not include punctuation at the end.
39- """
40-
41-
42- def normalize_title_input_text (text : str , max_chars : int = TITLE_INPUT_MAX_CHARS ) -> str | None :
43- normalized = " " .join (text .strip ().split ())
44- if normalized == "" :
45- return None
46- if len (normalized ) > max_chars :
47- normalized = normalized [:max_chars ].rstrip ()
48- return normalized
4914
5015
5116async def _try_rollback_to_failed (pending_message_ids : list ) -> None :
@@ -59,84 +24,6 @@ async def _try_rollback_to_failed(pending_message_ids: list) -> None:
5924 "session.pending_message_rollback_failed" ,
6025 pending_message_ids = [str (mid ) for mid in pending_message_ids ],
6126 )
62- def check_title_input_quality (text : str | None ) -> tuple [bool , str ]:
63- if text is None :
64- return False , "empty"
65- normalized = normalize_title_input_text (text )
66- if normalized is None :
67- return False , "empty"
68- if len (normalized ) < TITLE_INPUT_MIN_CHARS :
69- return False , "too_short"
70- if normalized .lower () in NON_INFORMATIVE_TITLE_INPUTS :
71- return False , "non_informative"
72- return True , "ok"
73-
74-
75- def extract_first_user_message_text (messages : list [MessageBlob ]) -> str | None :
76- for message in messages :
77- if message .role != "user" :
78- continue
79- text_parts = [
80- part .text .strip ()
81- for part in message .parts
82- if part .type == "text"
83- and isinstance (part .text , str )
84- and part .text .strip () != ""
85- ]
86- if text_parts :
87- return normalize_title_input_text ("\n " .join (text_parts ))
88- return None
89-
90-
91- def sanitize_generated_title (
92- title_candidate : str | None ,
93- fallback_text : str | None ,
94- max_chars : int = TITLE_OUTPUT_MAX_CHARS ,
95- ) -> str | None :
96- def _clean (text : str | None ) -> str | None :
97- if text is None :
98- return None
99- cleaned = " " .join (text .replace ("\n " , " " ).replace ("\r " , " " ).split ())
100- cleaned = cleaned .strip ("`'\" “”‘’ " ).strip ()
101- if cleaned == "" :
102- return None
103- if len (cleaned ) > max_chars :
104- cleaned = cleaned [:max_chars ].rstrip ()
105- cleaned = cleaned .strip ("`'\" “”‘’ " ).strip ()
106- if cleaned == "" :
107- return None
108- if not any (ch .isalnum () for ch in cleaned ):
109- return None
110- return cleaned
111-
112- cleaned_title = _clean (title_candidate )
113- if cleaned_title is not None :
114- return cleaned_title
115-
116- cleaned_fallback = _clean (fallback_text )
117- if cleaned_fallback is None :
118- return None
119- return " " .join (cleaned_fallback .split ()[:8 ])
120-
121-
122- async def generate_session_title_candidate (
123- first_user_message_text : str ,
124- ) -> Result [str | None ]:
125- r = await llm_complete (
126- system_prompt = TITLE_GENERATION_SYSTEM_PROMPT ,
127- history_messages = [{"role" : "user" , "content" : first_user_message_text }],
128- max_tokens = TITLE_GENERATION_MAX_TOKENS ,
129- prompt_kwargs = {"prompt_id" : "session.display_title.first_user" },
130- )
131- llm_response , eil = r .unpack ()
132- if eil :
133- return Result .reject (eil .errmsg )
134- if llm_response .content is None :
135- return Result .resolve (None )
136- title_candidate = llm_response .content .strip ()
137- if title_candidate == "" :
138- return Result .resolve (None )
139- return Result .resolve (title_candidate )
14027
14128
14229async def process_session_pending_message (
@@ -197,79 +84,6 @@ async def process_session_pending_message(
19784 )
19885 for m in messages
19986 ]
200- first_user_message_text = None
201- try :
202- r = await SD .should_generate_session_display_title (
203- db_session , session_id
204- )
205- should_generate_title , eil = r .unpack ()
206- if eil :
207- raise ValueError (eil .errmsg )
208- if not should_generate_title :
209- LOG .debug (
210- f"Session { session_id } already has display_title, "
211- "skip title-input extraction"
212- )
213- else :
214- first_user_message_text = extract_first_user_message_text (
215- messages_data
216- )
217- is_quality_ok , quality_reason = check_title_input_quality (
218- first_user_message_text
219- )
220- if not is_quality_ok :
221- first_user_message_text = None
222- LOG .debug (
223- f"Skip title-input generation for session { session_id } : "
224- f"{ quality_reason } "
225- )
226- else :
227- LOG .debug (
228- f"Extracted first user text from pending session { session_id } , "
229- f"length={ len (first_user_message_text )} "
230- )
231- except Exception as title_gate_err :
232- first_user_message_text = None
233- LOG .warning (
234- f"Skip title extraction for session { session_id } : { title_gate_err } "
235- )
236-
237- if first_user_message_text is not None :
238- try :
239- title_candidate = None
240- title_result = await generate_session_title_candidate (
241- first_user_message_text
242- )
243- title_candidate_raw , eil = title_result .unpack ()
244- if eil :
245- raise ValueError (eil .errmsg )
246-
247- title_candidate = sanitize_generated_title (
248- title_candidate_raw , first_user_message_text
249- )
250- if title_candidate is None :
251- LOG .debug (
252- f"Title generation produced unusable content for session { session_id } "
253- )
254- else :
255- LOG .debug (
256- f"Generated session title candidate for session { session_id } : "
257- f"{ title_candidate [:80 ]} "
258- )
259-
260- if title_candidate is not None :
261- async with DB_CLIENT .get_session_context () as db_session :
262- r = await SD .update_session_display_title (
263- db_session , session_id , title_candidate
264- )
265- _ , eil = r .unpack ()
266- if eil :
267- raise ValueError (eil .errmsg )
268- LOG .debug (f"Persisted display_title for session { session_id } " )
269- except Exception as title_err :
270- LOG .warning (
271- f"Skip title generation/persist for session { session_id } : { title_err } "
272- )
27387
27488 ls_session = None
27589 async with DB_CLIENT .get_session_context () as session :
0 commit comments