Skip to content

Commit d6df25b

Browse files
committed
Improve chat history retention
1 parent da90097 commit d6df25b

1 file changed

Lines changed: 70 additions & 29 deletions

File tree

backend/main.py

Lines changed: 70 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,9 @@ def _parse_csv_env(name: str, default: str) -> list[str]:
121121
"综合", "综合解读", "解读", "突出", "跨学科", "联动", "整合", "对比", "比较",
122122
"展开", "展开讲讲", "梳理", "串联", "理解", "给出", "提出",
123123
}
124+
CHAT_HISTORY_MAX_MESSAGES = max(2, int(os.getenv("CHAT_HISTORY_MAX_MESSAGES", "6")))
125+
CHAT_HISTORY_TRUNCATED_CHARS = max(200, int(os.getenv("CHAT_HISTORY_TRUNCATED_CHARS", "600")))
126+
CHAT_HISTORY_FULL_TAIL_MESSAGES = max(2, int(os.getenv("CHAT_HISTORY_FULL_TAIL_MESSAGES", "4")))
124127

125128

126129
def _compute_vector_source_fingerprint(text_limit: int) -> tuple[Optional[int], Optional[str]]:
@@ -329,6 +332,11 @@ def _build_token_phrases(tokens: list[str], max_window: int = 4) -> set[str]:
329332
return phrases
330333

331334

335+
def _build_text_match_context(text: str | None) -> tuple[str, set[str]]:
336+
normalized_text = _normalize_match_text(text)
337+
return normalized_text, _build_token_phrases(_segment_text_tokens(normalized_text))
338+
339+
332340
@functools.lru_cache(maxsize=2048)
333341
def _compile_non_cjk_term_pattern(term: str):
334342
return re.compile(rf"(?<![0-9A-Za-z]){re.escape(term)}(?![0-9A-Za-z])", re.IGNORECASE)
@@ -344,9 +352,15 @@ def _concept_matches_text(concept: str, normalized_text: str, phrase_set: set[st
344352
return bool(_compile_non_cjk_term_pattern(concept).search(normalized_text))
345353

346354

347-
def _present_terms_in_text(terms: list[str], text: str) -> list[str]:
348-
normalized_text = _normalize_match_text(text)
349-
phrase_set = _build_token_phrases(_segment_text_tokens(normalized_text))
355+
def _present_terms_in_text(
356+
terms: list[str],
357+
text: str,
358+
*,
359+
normalized_text: str | None = None,
360+
phrase_set: set[str] | None = None,
361+
) -> list[str]:
362+
if normalized_text is None or phrase_set is None:
363+
normalized_text, phrase_set = _build_text_match_context(text)
350364
present = []
351365
for term in terms:
352366
if _concept_matches_text(term, normalized_text, phrase_set):
@@ -462,6 +476,21 @@ def _normalize_text_line(text: str | None) -> str:
462476
return re.sub(r"\s+", " ", text or "").strip()
463477

464478

479+
def _format_chat_history_lines(history: list[dict] | None) -> list[str]:
480+
recent_messages = list((history or [])[-CHAT_HISTORY_MAX_MESSAGES:])
481+
full_tail_start = max(0, len(recent_messages) - CHAT_HISTORY_FULL_TAIL_MESSAGES)
482+
history_lines = []
483+
for idx, msg in enumerate(recent_messages):
484+
role = "用户" if msg.get("role") == "user" else "助手"
485+
content = (msg.get("content") or "").strip()
486+
if not content:
487+
continue
488+
if idx < full_tail_start and len(content) > CHAT_HISTORY_TRUNCATED_CHARS:
489+
content = content[:CHAT_HISTORY_TRUNCATED_CHARS].rstrip() + "…"
490+
history_lines.append(f"{role}: {content}")
491+
return history_lines
492+
493+
465494
def _load_ai_summary(con, chunk_id: int) -> str:
466495
try:
467496
row = con.execute(
@@ -1008,12 +1037,7 @@ def _build_chat_context_payload(con, query: str, user_message: str, history: lis
10081037
for item in relation_hints
10091038
]
10101039

1011-
history_lines = []
1012-
for msg in (history or [])[-6:]:
1013-
role = "用户" if msg.get("role") == "user" else "助手"
1014-
content = (msg.get("content") or "").strip()
1015-
if content:
1016-
history_lines.append(f"{role}: {content[:300]}")
1040+
history_lines = _format_chat_history_lines(history)
10171041

10181042
summary = {
10191043
"subject_count": len(groups),
@@ -1071,11 +1095,7 @@ def _build_chat_context_for_request(query: str, user_message: str, history: list
10711095
def _build_chat_prompt(query: str, user_message: str, context_payload: dict, history: list[dict] | None = None) -> str:
10721096
history_text = (context_payload.get("history_text") or "").strip()
10731097
if history and not history_text:
1074-
history_text = "\n".join(
1075-
f"{'用户' if msg.get('role') == 'user' else '助手'}: {(msg.get('content') or '').strip()[:300]}"
1076-
for msg in history[-6:]
1077-
if (msg.get("content") or "").strip()
1078-
)
1098+
history_text = "\n".join(_format_chat_history_lines(history))
10791099
if not history_text:
10801100
history_text = "(无)"
10811101

@@ -1805,17 +1825,12 @@ def related(
18051825

18061826

18071827
@app.post("/api/chat/context")
1808-
def chat_context(payload: dict = Body(...)):
1828+
async def chat_context(payload: dict = Body(...)):
18091829
"""Build grounded context for AI chat before calling an external model service."""
18101830
query = str(payload.get("query", "")).strip()
18111831
user_message = str(payload.get("user_message", "")).strip()
18121832
history = payload.get("history") or []
1813-
1814-
con = get_db()
1815-
try:
1816-
return _build_chat_context_payload(con, query, user_message, history=history)
1817-
finally:
1818-
con.close()
1833+
return await run_in_threadpool(_build_chat_context_for_request, query, user_message, history)
18191834

18201835

18211836
@app.post("/api/chat/log")
@@ -2101,11 +2116,12 @@ def _expand_cross_subject(concepts: list[dict], con) -> list[str]:
21012116

21022117

21032118
def _score_result(result_text: str, query_terms: list[str],
2104-
matched_concepts: list[str], is_same_subject: bool) -> int:
2119+
matched_concepts: list[str], is_same_subject: bool,
2120+
*, normalized_text: str | None = None, phrase_set: set[str] | None = None) -> int:
21052121
"""Compute relevance score 0-100 with IDF-weighted term importance."""
21062122
score = 0
2107-
normalized_text = _normalize_match_text(result_text)
2108-
phrase_set = _build_token_phrases(_segment_text_tokens(normalized_text))
2123+
if normalized_text is None or phrase_set is None:
2124+
normalized_text, phrase_set = _build_text_match_context(result_text)
21092125

21102126
# Term overlap — weight longer/rarer terms higher (max 35 points)
21112127
term_hits = 0
@@ -2298,10 +2314,22 @@ def gaokao_link(
22982314
scoring_concepts = list(dict.fromkeys(precomputed_terms[:6] + concept_names[:8]))
22992315
for r, link_type in all_results:
23002316
r_text = r["text"] or ""
2317+
normalized_text, phrase_set = _build_text_match_context(r_text)
23012318
# Find which concepts matched in this result
2302-
r_matched = _present_terms_in_text(scoring_concepts, r_text)
2303-
score = _score_result(r_text, scoring_terms, scoring_concepts,
2304-
r["subject"] == q_subject)
2319+
r_matched = _present_terms_in_text(
2320+
scoring_concepts,
2321+
r_text,
2322+
normalized_text=normalized_text,
2323+
phrase_set=phrase_set,
2324+
)
2325+
score = _score_result(
2326+
r_text,
2327+
scoring_terms,
2328+
scoring_concepts,
2329+
r["subject"] == q_subject,
2330+
normalized_text=normalized_text,
2331+
phrase_set=phrase_set,
2332+
)
23052333
# Skip results below minimum quality threshold
23062334
if score < 15:
23072335
continue
@@ -2434,8 +2462,21 @@ def textbook_links(
24342462
results = []
24352463
for r in rows:
24362464
r_text = r["text"] or ""
2437-
r_matched = _present_terms_in_text(concept_names, r_text)
2438-
score = _score_result(r_text, top_terms, concept_names, False)
2465+
normalized_text, phrase_set = _build_text_match_context(r_text)
2466+
r_matched = _present_terms_in_text(
2467+
concept_names,
2468+
r_text,
2469+
normalized_text=normalized_text,
2470+
phrase_set=phrase_set,
2471+
)
2472+
score = _score_result(
2473+
r_text,
2474+
top_terms,
2475+
concept_names,
2476+
False,
2477+
normalized_text=normalized_text,
2478+
phrase_set=phrase_set,
2479+
)
24392480
results.append({
24402481
"id": r["id"],
24412482
"subject": r["subject"],

0 commit comments

Comments
 (0)