diff --git a/eval/utils.py b/eval/utils.py index 38ed646..df49a0e 100644 --- a/eval/utils.py +++ b/eval/utils.py @@ -114,6 +114,41 @@ def gpt_summarize(dialogs, client): print("调用 GPT 生成主题摘要...") return gpt_generate_answer(prompt, messages, client) +def strip_code_fence(text): + """Return `text` without a surrounding Markdown code fence. + + Many current models wrap JSON in a ```json ... ``` fence by default, + even when asked for bare JSON. That is a correct answer, but + json.loads rejects it, so every caller below falls back and loses the + structure it asked for. Text that is not fenced is returned unchanged. + + >>> strip_code_fence('[{"theme": "trip"}]') + '[{"theme": "trip"}]' + >>> strip_code_fence('```json\\n[{"theme": "trip"}]\\n```') + '[{"theme": "trip"}]' + >>> strip_code_fence('```\\n{"a": 1}\\n```') + '{"a": 1}' + >>> strip_code_fence(' ```json\\n{"a": 1}\\n``` ') + '{"a": 1}' + >>> strip_code_fence('```json\\n{"a": 1}') + '{"a": 1}' + >>> strip_code_fence('') + '' + """ + if not text: + return text + stripped = text.strip() + if not stripped.startswith("```"): + return text + lines = stripped.splitlines() + # Drop the opening fence line, which carries the optional language tag. + lines = lines[1:] + # Drop the closing fence when the model sent one (it may be truncated). + if lines and lines[-1].strip().startswith("```"): + lines = lines[:-1] + return "\n".join(lines).strip() + + def gpt_generate_multi_summary(text, client): """ 调用 LLM 生成多子主题摘要,返回格式示例如下: @@ -137,7 +172,7 @@ def gpt_generate_multi_summary(text, client): response_text = gpt_generate_answer(prompt, messages, client) import json try: - summaries = json.loads(response_text) + summaries = json.loads(strip_code_fence(response_text)) except Exception: summaries = [] return {"input": text, "summaries": summaries} diff --git a/memoryos-chromadb/utils.py b/memoryos-chromadb/utils.py index 015bec5..8dc5a15 100644 --- a/memoryos-chromadb/utils.py +++ b/memoryos-chromadb/utils.py @@ -173,6 +173,41 @@ def gpt_summarize_dialogs(dialogs, client: OpenAIClient, model="gpt-4o-mini"): print("Calling LLM to generate topic summary...") return client.chat_completion(model=model, messages=messages) +def strip_code_fence(text): + """Return `text` without a surrounding Markdown code fence. + + Many current models wrap JSON in a ```json ... ``` fence by default, + even when asked for bare JSON. That is a correct answer, but + json.loads rejects it, so every caller below falls back and loses the + structure it asked for. Text that is not fenced is returned unchanged. + + >>> strip_code_fence('[{"theme": "trip"}]') + '[{"theme": "trip"}]' + >>> strip_code_fence('```json\\n[{"theme": "trip"}]\\n```') + '[{"theme": "trip"}]' + >>> strip_code_fence('```\\n{"a": 1}\\n```') + '{"a": 1}' + >>> strip_code_fence(' ```json\\n{"a": 1}\\n``` ') + '{"a": 1}' + >>> strip_code_fence('```json\\n{"a": 1}') + '{"a": 1}' + >>> strip_code_fence('') + '' + """ + if not text: + return text + stripped = text.strip() + if not stripped.startswith("```"): + return text + lines = stripped.splitlines() + # Drop the opening fence line, which carries the optional language tag. + lines = lines[1:] + # Drop the closing fence when the model sent one (it may be truncated). + if lines and lines[-1].strip().startswith("```"): + lines = lines[:-1] + return "\n".join(lines).strip() + + def gpt_generate_multi_summary(text, client: OpenAIClient, model="gpt-4o-mini"): messages = [ {"role": "system", "content": prompts.MULTI_SUMMARY_SYSTEM_PROMPT}, @@ -181,7 +216,7 @@ def gpt_generate_multi_summary(text, client: OpenAIClient, model="gpt-4o-mini"): print("Calling LLM to generate multi-topic summary...") response_text = client.chat_completion(model=model, messages=messages) try: - summaries = json.loads(response_text) + summaries = json.loads(strip_code_fence(response_text)) except json.JSONDecodeError: print(f"Warning: Could not parse multi-summary JSON: {response_text}") summaries = [] @@ -224,7 +259,7 @@ def gpt_user_profile_analysis(conversation_str: str, client: OpenAIClient, model print("Calling LLM for user profile analysis and update...") result_text = client.chat_completion(model=model, messages=messages) try: - return json.loads(result_text) + return json.loads(strip_code_fence(result_text)) except json.JSONDecodeError: print(f"Warning: User profile analysis did not return valid JSON. Content: {result_text}") return {"raw_text_profile": result_text} diff --git a/memoryos-mcp/memoryos/utils.py b/memoryos-mcp/memoryos/utils.py index 6983a44..c95afcc 100644 --- a/memoryos-mcp/memoryos/utils.py +++ b/memoryos-mcp/memoryos/utils.py @@ -248,6 +248,41 @@ def gpt_summarize_dialogs(dialogs, client: OpenAIClient, model="gpt-4o-mini"): print("Calling LLM to generate topic summary...") return client.chat_completion(model=model, messages=messages) +def strip_code_fence(text): + """Return `text` without a surrounding Markdown code fence. + + Many current models wrap JSON in a ```json ... ``` fence by default, + even when asked for bare JSON. That is a correct answer, but + json.loads rejects it, so every caller below falls back and loses the + structure it asked for. Text that is not fenced is returned unchanged. + + >>> strip_code_fence('[{"theme": "trip"}]') + '[{"theme": "trip"}]' + >>> strip_code_fence('```json\\n[{"theme": "trip"}]\\n```') + '[{"theme": "trip"}]' + >>> strip_code_fence('```\\n{"a": 1}\\n```') + '{"a": 1}' + >>> strip_code_fence(' ```json\\n{"a": 1}\\n``` ') + '{"a": 1}' + >>> strip_code_fence('```json\\n{"a": 1}') + '{"a": 1}' + >>> strip_code_fence('') + '' + """ + if not text: + return text + stripped = text.strip() + if not stripped.startswith("```"): + return text + lines = stripped.splitlines() + # Drop the opening fence line, which carries the optional language tag. + lines = lines[1:] + # Drop the closing fence when the model sent one (it may be truncated). + if lines and lines[-1].strip().startswith("```"): + lines = lines[:-1] + return "\n".join(lines).strip() + + def gpt_generate_multi_summary(text, client: OpenAIClient, model="gpt-4o-mini"): messages = [ {"role": "system", "content": prompts.MULTI_SUMMARY_SYSTEM_PROMPT}, @@ -256,7 +291,7 @@ def gpt_generate_multi_summary(text, client: OpenAIClient, model="gpt-4o-mini"): print("Calling LLM to generate multi-topic summary...") response_text = client.chat_completion(model=model, messages=messages) try: - summaries = json.loads(response_text) + summaries = json.loads(strip_code_fence(response_text)) except json.JSONDecodeError: print(f"Warning: Could not parse multi-summary JSON: {response_text}") summaries = [] # Return empty list or a default structure diff --git a/memoryos-playground/utils.py b/memoryos-playground/utils.py index 40d95ff..427049b 100644 --- a/memoryos-playground/utils.py +++ b/memoryos-playground/utils.py @@ -248,6 +248,41 @@ def gpt_summarize_dialogs(dialogs, client: OpenAIClient, model="gpt-4o-mini"): print("Calling LLM to generate topic summary...") return client.chat_completion(model=model, messages=messages) +def strip_code_fence(text): + """Return `text` without a surrounding Markdown code fence. + + Many current models wrap JSON in a ```json ... ``` fence by default, + even when asked for bare JSON. That is a correct answer, but + json.loads rejects it, so every caller below falls back and loses the + structure it asked for. Text that is not fenced is returned unchanged. + + >>> strip_code_fence('[{"theme": "trip"}]') + '[{"theme": "trip"}]' + >>> strip_code_fence('```json\\n[{"theme": "trip"}]\\n```') + '[{"theme": "trip"}]' + >>> strip_code_fence('```\\n{"a": 1}\\n```') + '{"a": 1}' + >>> strip_code_fence(' ```json\\n{"a": 1}\\n``` ') + '{"a": 1}' + >>> strip_code_fence('```json\\n{"a": 1}') + '{"a": 1}' + >>> strip_code_fence('') + '' + """ + if not text: + return text + stripped = text.strip() + if not stripped.startswith("```"): + return text + lines = stripped.splitlines() + # Drop the opening fence line, which carries the optional language tag. + lines = lines[1:] + # Drop the closing fence when the model sent one (it may be truncated). + if lines and lines[-1].strip().startswith("```"): + lines = lines[:-1] + return "\n".join(lines).strip() + + def gpt_generate_multi_summary(text, client: OpenAIClient, model="gpt-4o-mini"): messages = [ {"role": "system", "content": prompts.MULTI_SUMMARY_SYSTEM_PROMPT}, @@ -256,7 +291,7 @@ def gpt_generate_multi_summary(text, client: OpenAIClient, model="gpt-4o-mini"): print("Calling LLM to generate multi-topic summary...") response_text = client.chat_completion(model=model, messages=messages) try: - summaries = json.loads(response_text) + summaries = json.loads(strip_code_fence(response_text)) except json.JSONDecodeError: print(f"Warning: Could not parse multi-summary JSON: {response_text}") summaries = [] # Return empty list or a default structure diff --git a/memoryos-pypi/utils.py b/memoryos-pypi/utils.py index 40d95ff..427049b 100644 --- a/memoryos-pypi/utils.py +++ b/memoryos-pypi/utils.py @@ -248,6 +248,41 @@ def gpt_summarize_dialogs(dialogs, client: OpenAIClient, model="gpt-4o-mini"): print("Calling LLM to generate topic summary...") return client.chat_completion(model=model, messages=messages) +def strip_code_fence(text): + """Return `text` without a surrounding Markdown code fence. + + Many current models wrap JSON in a ```json ... ``` fence by default, + even when asked for bare JSON. That is a correct answer, but + json.loads rejects it, so every caller below falls back and loses the + structure it asked for. Text that is not fenced is returned unchanged. + + >>> strip_code_fence('[{"theme": "trip"}]') + '[{"theme": "trip"}]' + >>> strip_code_fence('```json\\n[{"theme": "trip"}]\\n```') + '[{"theme": "trip"}]' + >>> strip_code_fence('```\\n{"a": 1}\\n```') + '{"a": 1}' + >>> strip_code_fence(' ```json\\n{"a": 1}\\n``` ') + '{"a": 1}' + >>> strip_code_fence('```json\\n{"a": 1}') + '{"a": 1}' + >>> strip_code_fence('') + '' + """ + if not text: + return text + stripped = text.strip() + if not stripped.startswith("```"): + return text + lines = stripped.splitlines() + # Drop the opening fence line, which carries the optional language tag. + lines = lines[1:] + # Drop the closing fence when the model sent one (it may be truncated). + if lines and lines[-1].strip().startswith("```"): + lines = lines[:-1] + return "\n".join(lines).strip() + + def gpt_generate_multi_summary(text, client: OpenAIClient, model="gpt-4o-mini"): messages = [ {"role": "system", "content": prompts.MULTI_SUMMARY_SYSTEM_PROMPT}, @@ -256,7 +291,7 @@ def gpt_generate_multi_summary(text, client: OpenAIClient, model="gpt-4o-mini"): print("Calling LLM to generate multi-topic summary...") response_text = client.chat_completion(model=model, messages=messages) try: - summaries = json.loads(response_text) + summaries = json.loads(strip_code_fence(response_text)) except json.JSONDecodeError: print(f"Warning: Could not parse multi-summary JSON: {response_text}") summaries = [] # Return empty list or a default structure