[AC-API-UPDATE] feat(api): 更新 API 端点和实体模型
- 更新 dialogue API 支持新的对话功能 - 更新 share_page API 优化分享页面 - 更新 main.py 注册新的路由模块 - 更新 entities 模型添加新字段
This commit is contained in:
parent
9196247578
commit
1490235b8f
|
|
@ -580,6 +580,35 @@ async def respond_dialogue(
|
||||||
f"guardrail={guardrail_result.triggered}, kb_hit={final_trace.kb_hit}"
|
f"guardrail={guardrail_result.triggered}, kb_hit={final_trace.kb_hit}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if dialogue_request.user_id:
|
||||||
|
try:
|
||||||
|
from app.services.mid.memory_adapter import MemoryAdapter
|
||||||
|
from app.services.mid.memory_summary_generator import MemorySummaryGenerator
|
||||||
|
|
||||||
|
memory_adapter = MemoryAdapter(session=session)
|
||||||
|
summary_generator = MemorySummaryGenerator()
|
||||||
|
|
||||||
|
history_messages = [
|
||||||
|
{"role": h.role, "content": h.content}
|
||||||
|
for h in (dialogue_request.history or [])
|
||||||
|
]
|
||||||
|
assistant_reply = "\n".join(s.text for s in final_segments)
|
||||||
|
update_messages = history_messages + [
|
||||||
|
{"role": "user", "content": dialogue_request.user_message},
|
||||||
|
{"role": "assistant", "content": assistant_reply},
|
||||||
|
]
|
||||||
|
|
||||||
|
await memory_adapter.update_with_summary_generation(
|
||||||
|
user_id=dialogue_request.user_id,
|
||||||
|
session_id=dialogue_request.session_id,
|
||||||
|
messages=update_messages,
|
||||||
|
tenant_id=tenant_id,
|
||||||
|
summary_generator=summary_generator,
|
||||||
|
recent_turns=8,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"[AC-IDMP-14] Memory update trigger failed: {e}")
|
||||||
|
|
||||||
return DialogueResponse(
|
return DialogueResponse(
|
||||||
segments=final_segments,
|
segments=final_segments,
|
||||||
trace=final_trace,
|
trace=final_trace,
|
||||||
|
|
@ -1429,10 +1458,38 @@ async def _execute_agent_mode(
|
||||||
|
|
||||||
runtime_observer.record_react(request_id, react_ctx.iteration, react_ctx.tool_calls)
|
runtime_observer.record_react(request_id, react_ctx.iteration, react_ctx.tool_calls)
|
||||||
|
|
||||||
|
# 合并 tool_calls:优先使用 KB 工具内部的 trace(包含注入后的参数)
|
||||||
|
final_tool_calls = list(react_ctx.tool_calls) if react_ctx.tool_calls else []
|
||||||
|
logger.info(
|
||||||
|
f"[TRACE-MERGE] Before merge: final_tool_calls count={len(final_tool_calls)}, "
|
||||||
|
f"kb_dynamic_result exists={kb_dynamic_result is not None}, "
|
||||||
|
f"kb_dynamic_result.tool_trace exists={kb_dynamic_result.tool_trace if kb_dynamic_result else None}"
|
||||||
|
)
|
||||||
|
if kb_dynamic_result and kb_dynamic_result.tool_trace:
|
||||||
|
kb_trace = kb_dynamic_result.tool_trace
|
||||||
|
logger.info(
|
||||||
|
f"[TRACE-MERGE] KB trace arguments: {kb_trace.arguments}"
|
||||||
|
)
|
||||||
|
for i, tc in enumerate(final_tool_calls):
|
||||||
|
logger.info(
|
||||||
|
f"[TRACE-MERGE] Checking tool_call[{i}]: tool_name={tc.tool_name}"
|
||||||
|
)
|
||||||
|
if tc.tool_name == "kb_search_dynamic":
|
||||||
|
logger.info(
|
||||||
|
f"[TRACE-MERGE] Replacing trace at index {i}: old_args={tc.arguments}, new_args={kb_trace.arguments}"
|
||||||
|
)
|
||||||
|
final_tool_calls[i] = kb_trace
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
logger.info(
|
||||||
|
f"[TRACE-MERGE] Skipped merge: kb_dynamic_result={kb_dynamic_result is not None}, "
|
||||||
|
f"tool_trace={kb_dynamic_result.tool_trace if kb_dynamic_result else 'N/A'}"
|
||||||
|
)
|
||||||
|
|
||||||
trace_logger.update_trace(
|
trace_logger.update_trace(
|
||||||
request_id=request_id,
|
request_id=request_id,
|
||||||
react_iterations=react_ctx.iteration,
|
react_iterations=react_ctx.iteration,
|
||||||
tool_calls=react_ctx.tool_calls,
|
tool_calls=final_tool_calls,
|
||||||
)
|
)
|
||||||
|
|
||||||
segments = _text_to_segments(final_answer)
|
segments = _text_to_segments(final_answer)
|
||||||
|
|
@ -1444,8 +1501,8 @@ async def _execute_agent_mode(
|
||||||
request_id=trace.request_id,
|
request_id=trace.request_id,
|
||||||
generation_id=trace.generation_id,
|
generation_id=trace.generation_id,
|
||||||
react_iterations=react_ctx.iteration,
|
react_iterations=react_ctx.iteration,
|
||||||
tools_used=[tc.tool_name for tc in react_ctx.tool_calls] if react_ctx.tool_calls else None,
|
tools_used=[tc.tool_name for tc in final_tool_calls] if final_tool_calls else None,
|
||||||
tool_calls=react_ctx.tool_calls,
|
tool_calls=final_tool_calls,
|
||||||
timeout_profile=timeout_governor.profile,
|
timeout_profile=timeout_governor.profile,
|
||||||
kb_tool_called=True,
|
kb_tool_called=True,
|
||||||
kb_hit=kb_success and len(kb_hits) > 0,
|
kb_hit=kb_success and len(kb_hits) > 0,
|
||||||
|
|
|
||||||
|
|
@ -176,14 +176,38 @@ async def share_chat_page(
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>对话分享</title>
|
<title>对话分享</title>
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600&display=swap" rel="stylesheet">
|
||||||
<style>
|
<style>
|
||||||
|
:root {{
|
||||||
|
--bg-primary: #f8fafc;
|
||||||
|
--bg-secondary: #ffffff;
|
||||||
|
--bg-tertiary: #f1f5f9;
|
||||||
|
--text-primary: #0f172a;
|
||||||
|
--text-secondary: #475569;
|
||||||
|
--text-muted: #94a3b8;
|
||||||
|
--accent: #6366f1;
|
||||||
|
--accent-hover: #4f46e5;
|
||||||
|
--accent-light: #eef2ff;
|
||||||
|
--border: #e2e8f0;
|
||||||
|
--shadow-sm: 0 1px 2px rgba(0,0,0,0.04);
|
||||||
|
--shadow-md: 0 4px 12px rgba(0,0,0,0.06);
|
||||||
|
--shadow-lg: 0 8px 32px rgba(0,0,0,0.08);
|
||||||
|
--radius-sm: 8px;
|
||||||
|
--radius-md: 12px;
|
||||||
|
--radius-lg: 16px;
|
||||||
|
--radius-xl: 24px;
|
||||||
|
}}
|
||||||
* {{ box-sizing: border-box; margin: 0; padding: 0; }}
|
* {{ box-sizing: border-box; margin: 0; padding: 0; }}
|
||||||
body {{
|
body {{
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'PingFang SC', 'Microsoft YaHei', sans-serif;
|
font-family: 'Plus Jakarta Sans', -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Microsoft YaHei', sans-serif;
|
||||||
background: #f8f9fa;
|
background: var(--bg-primary);
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
color: var(--text-primary);
|
||||||
|
line-height: 1.6;
|
||||||
}}
|
}}
|
||||||
.welcome-screen {{
|
.welcome-screen {{
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|
@ -191,105 +215,221 @@ async def share_chat_page(
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 40px 20px;
|
padding: 48px 24px;
|
||||||
|
background: linear-gradient(180deg, var(--bg-secondary) 0%, var(--bg-primary) 100%);
|
||||||
}}
|
}}
|
||||||
.welcome-screen.hidden {{ display: none; }}
|
.welcome-screen.hidden {{ display: none; }}
|
||||||
|
.welcome-title {{
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
margin-bottom: 32px;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}}
|
||||||
.welcome-input-wrapper {{
|
.welcome-input-wrapper {{
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 680px;
|
max-width: 600px;
|
||||||
background: white;
|
background: var(--bg-secondary);
|
||||||
border-radius: 16px;
|
border-radius: var(--radius-xl);
|
||||||
padding: 16px 20px;
|
padding: 8px;
|
||||||
box-shadow: 0 2px 12px rgba(0,0,0,0.08);
|
box-shadow: var(--shadow-lg);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: 8px;
|
||||||
|
transition: box-shadow 0.2s ease, border-color 0.2s ease;
|
||||||
|
}}
|
||||||
|
.welcome-input-wrapper:focus-within {{
|
||||||
|
box-shadow: var(--shadow-lg), 0 0 0 3px var(--accent-light);
|
||||||
|
border-color: var(--accent);
|
||||||
}}
|
}}
|
||||||
.welcome-textarea, .input-textarea {{
|
.welcome-textarea, .input-textarea {{
|
||||||
width: 100%;
|
flex: 1;
|
||||||
min-height: 56px;
|
min-height: 48px;
|
||||||
border: 1px solid #e5e5e5;
|
max-height: 200px;
|
||||||
border-radius: 12px;
|
border: none;
|
||||||
padding: 12px 16px;
|
border-radius: var(--radius-lg);
|
||||||
|
padding: 14px 16px;
|
||||||
resize: none;
|
resize: none;
|
||||||
outline: none;
|
outline: none;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
line-height: 1.6;
|
line-height: 1.5;
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
background: #fafafa;
|
background: transparent;
|
||||||
transition: all 0.2s;
|
color: var(--text-primary);
|
||||||
}}
|
}}
|
||||||
.welcome-textarea:focus, .input-textarea:focus {{
|
.welcome-textarea::placeholder, .input-textarea::placeholder {{
|
||||||
border-color: #1677ff;
|
color: var(--text-muted);
|
||||||
background: white;
|
|
||||||
}}
|
}}
|
||||||
.chat-screen {{ flex: 1; display: none; flex-direction: column; }}
|
.chat-screen {{ flex: 1; display: none; flex-direction: column; background: var(--bg-primary); }}
|
||||||
.chat-screen.active {{ display: flex; }}
|
.chat-screen.active {{ display: flex; }}
|
||||||
.chat-list {{
|
.chat-list {{
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding: 20px;
|
padding: 24px;
|
||||||
max-width: 800px;
|
max-width: 720px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 16px;
|
gap: 20px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}}
|
}}
|
||||||
.bubble {{ display: flex; gap: 10px; align-items: flex-start; }}
|
.bubble {{ display: flex; gap: 12px; align-items: flex-start; animation: fadeIn 0.3s ease; }}
|
||||||
|
@keyframes fadeIn {{
|
||||||
|
from {{ opacity: 0; transform: translateY(8px); }}
|
||||||
|
to {{ opacity: 1; transform: translateY(0); }}
|
||||||
|
}}
|
||||||
.bubble.user {{ flex-direction: row-reverse; }}
|
.bubble.user {{ flex-direction: row-reverse; }}
|
||||||
.avatar {{
|
.avatar {{
|
||||||
width: 32px; height: 32px; border-radius: 50%;
|
width: 36px; height: 36px; border-radius: 50%;
|
||||||
background: white; display: flex; align-items: center; justify-content: center;
|
background: var(--bg-tertiary);
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
overflow: hidden;
|
||||||
}}
|
}}
|
||||||
.bubble-content {{ max-width: 75%; }}
|
.avatar svg {{
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
}}
|
||||||
|
.bubble.user .avatar {{
|
||||||
|
background: var(--accent-light);
|
||||||
|
}}
|
||||||
|
.bubble.user .avatar svg path {{
|
||||||
|
fill: var(--accent);
|
||||||
|
}}
|
||||||
|
.bubble.bot .avatar svg path {{
|
||||||
|
fill: var(--accent);
|
||||||
|
}}
|
||||||
|
.bubble-content {{ max-width: 80%; min-width: 0; }}
|
||||||
.bubble-text {{
|
.bubble-text {{
|
||||||
padding: 12px 16px; border-radius: 16px; white-space: pre-wrap; word-break: break-word;
|
padding: 14px 18px;
|
||||||
font-size: 14px; line-height: 1.6;
|
border-radius: var(--radius-lg);
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.65;
|
||||||
|
}}
|
||||||
|
.bubble.user .bubble-text {{
|
||||||
|
background: var(--accent);
|
||||||
|
color: white;
|
||||||
|
border-bottom-right-radius: var(--radius-sm);
|
||||||
|
}}
|
||||||
|
.bubble.bot .bubble-text {{
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
border-bottom-left-radius: var(--radius-sm);
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
}}
|
||||||
|
.bubble.error .bubble-text {{
|
||||||
|
background: #fef2f2;
|
||||||
|
color: #dc2626;
|
||||||
|
border: 1px solid #fecaca;
|
||||||
}}
|
}}
|
||||||
.bubble.user .bubble-text {{ background: #1677ff; color: white; }}
|
|
||||||
.bubble.bot .bubble-text {{ background: white; color: #333; }}
|
|
||||||
.bubble.error .bubble-text {{ background: #fff2f0; color: #ff4d4f; border: 1px solid #ffccc7; }}
|
|
||||||
.thought-block {{
|
.thought-block {{
|
||||||
background: #f5f5f5;
|
background: var(--bg-tertiary);
|
||||||
color: #888;
|
color: var(--text-secondary);
|
||||||
padding: 12px 16px;
|
padding: 14px 18px;
|
||||||
border-radius: 12px;
|
border-radius: var(--radius-md);
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
line-height: 1.6;
|
line-height: 1.65;
|
||||||
border-left: 3px solid #ddd;
|
border-left: 3px solid var(--text-muted);
|
||||||
}}
|
}}
|
||||||
.thought-label {{
|
.thought-label {{
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #999;
|
color: var(--text-muted);
|
||||||
margin-bottom: 6px;
|
margin-bottom: 8px;
|
||||||
font-size: 12px;
|
font-size: 11px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
}}
|
}}
|
||||||
.final-answer-block {{
|
.final-answer-block {{
|
||||||
background: white;
|
background: var(--bg-secondary);
|
||||||
color: #333;
|
color: var(--text-primary);
|
||||||
padding: 12px 16px;
|
padding: 14px 18px;
|
||||||
border-radius: 12px;
|
border-radius: var(--radius-md);
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
line-height: 1.6;
|
line-height: 1.65;
|
||||||
}}
|
}}
|
||||||
.final-answer-label {{
|
.final-answer-label {{
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #1677ff;
|
color: var(--accent);
|
||||||
margin-bottom: 6px;
|
margin-bottom: 8px;
|
||||||
font-size: 12px;
|
font-size: 11px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
}}
|
||||||
|
.input-area {{
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
padding: 16px 24px 24px;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
}}
|
||||||
|
.input-wrapper {{
|
||||||
|
max-width: 720px;
|
||||||
|
margin: 0 auto;
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: flex-end;
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
padding: 6px 6px 6px 16px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
||||||
|
}}
|
||||||
|
.input-wrapper:focus-within {{
|
||||||
|
border-color: var(--accent);
|
||||||
|
box-shadow: 0 0 0 3px var(--accent-light);
|
||||||
|
}}
|
||||||
|
.input-textarea {{
|
||||||
|
background: transparent;
|
||||||
|
padding: 10px 0;
|
||||||
}}
|
}}
|
||||||
.input-area {{ background: white; padding: 16px 20px 20px; border-top: 1px solid #eee; }}
|
|
||||||
.input-wrapper {{ max-width: 800px; margin: 0 auto; display: flex; gap: 12px; align-items: flex-end; }}
|
|
||||||
.send-btn, .welcome-send {{
|
.send-btn, .welcome-send {{
|
||||||
width: 40px; height: 40px; border-radius: 50%; border: none; cursor: pointer;
|
width: 44px; height: 44px;
|
||||||
background: #1677ff; color: white;
|
border-radius: 50%;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
background: var(--accent);
|
||||||
|
color: white;
|
||||||
|
font-size: 18px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: background 0.2s ease, transform 0.15s ease;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}}
|
||||||
|
.send-btn:hover, .welcome-send:hover {{
|
||||||
|
background: var(--accent-hover);
|
||||||
|
transform: scale(1.05);
|
||||||
|
}}
|
||||||
|
.send-btn:active, .welcome-send:active {{
|
||||||
|
transform: scale(0.95);
|
||||||
|
}}
|
||||||
|
.send-btn:disabled, .welcome-send:disabled {{
|
||||||
|
background: var(--text-muted);
|
||||||
|
cursor: not-allowed;
|
||||||
|
transform: none;
|
||||||
|
}}
|
||||||
|
.status {{
|
||||||
|
text-align: center;
|
||||||
|
padding: 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-weight: 500;
|
||||||
|
}}
|
||||||
|
.status.error {{ color: #dc2626; }}
|
||||||
|
@media (max-width: 640px) {{
|
||||||
|
.welcome-screen {{ padding: 32px 16px; }}
|
||||||
|
.welcome-title {{ font-size: 22px; margin-bottom: 24px; }}
|
||||||
|
.chat-list {{ padding: 16px; gap: 16px; }}
|
||||||
|
.bubble-content {{ max-width: 85%; }}
|
||||||
|
.input-area {{ padding: 12px 16px 20px; }}
|
||||||
}}
|
}}
|
||||||
.status {{ text-align: center; padding: 8px; font-size: 12px; color: #999; }}
|
|
||||||
.status.error {{ color: #ff4d4f; }}
|
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="welcome-screen" id="welcomeScreen">
|
<div class="welcome-screen" id="welcomeScreen">
|
||||||
<h1>今天有什么可以帮到你?</h1>
|
<h1 class="welcome-title">今天有什么可以帮到你?</h1>
|
||||||
<div class="welcome-input-wrapper">
|
<div class="welcome-input-wrapper">
|
||||||
<textarea class="welcome-textarea" id="welcomeInput" placeholder="输入消息,按 Enter 发送" rows="1"></textarea>
|
<textarea class="welcome-textarea" id="welcomeInput" placeholder="输入消息,按 Enter 发送" rows="1"></textarea>
|
||||||
<button class="welcome-send" id="welcomeSendBtn">➤</button>
|
<button class="welcome-send" id="welcomeSendBtn">➤</button>
|
||||||
|
|
@ -367,7 +507,12 @@ function formatBotMessage(text) {{
|
||||||
function addMessage(role, text) {{
|
function addMessage(role, text) {{
|
||||||
const div = document.createElement('div');
|
const div = document.createElement('div');
|
||||||
div.className = 'bubble ' + role;
|
div.className = 'bubble ' + role;
|
||||||
const avatar = role === 'user' ? '👤' : (role === 'bot' ? '🤖' : '⚠️');
|
|
||||||
|
const userSvg = '<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M573.9 516.2L512 640l-61.9-123.8C232 546.4 64 733.6 64 960h896c0-226.4-168-413.6-386.1-443.8zM480 384h64c17.7 0 32.1 14.4 32.1 32.1 0 17.7-14.4 32.1-32.1 32.1h-64c-11.9 0-22.3-6.5-27.8-16.1H356c34.9 48.5 91.7 80 156 80 106 0 192-86 192-192s-86-192-192-192-192 86-192 192c0 28.5 6.2 55.6 17.4 80h114.8c5.5-9.6 15.9-16.1 27.8-16.1z"/><path d="M272 432.1h84c-4.2-5.9-8.1-12-11.7-18.4-2.3-4.1-4.4-8.3-6.4-12.5-0.2-0.4-0.4-0.7-0.5-1.1H288c-8.8 0-16-7.2-16-16v-48.4c0-64.1 25-124.3 70.3-169.6S447.9 95.8 512 95.8s124.3 25 169.7 70.3c38.3 38.3 62.1 87.2 68.5 140.2-8.4 4-14.2 12.5-14.2 22.4v78.6c0 13.7 11.1 24.8 24.8 24.8h14.6c13.7 0 24.8-11.1 24.8-24.8v-78.6c0-11.3-7.6-20.9-18-23.8-6.9-60.9-33.9-117.4-78-161.3C652.9 92.1 584.6 63.9 512 63.9s-140.9 28.3-192.3 79.6C268.3 194.8 240 263.1 240 335.7v64.4c0 17.7 14.3 32 32 32z"/></svg>';
|
||||||
|
const botSvg = '<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M894.1 355.6h-1.7C853 177.6 687.6 51.4 498.1 54.9S148.2 190.5 115.9 369.7c-35.2 5.6-61.1 36-61.1 71.7v143.4c0.9 40.4 34.3 72.5 74.7 71.7 21.7-0.3 42.2-10 56-26.7 33.6 84.5 99.9 152 183.8 187 1.1-2 2.3-3.9 3.7-5.7 0.9-1.5 2.4-2.6 4.1-3 1.3 0 2.5 0.5 3.6 1.2a318.46 318.46 0 0 1-105.3-187.1c-5.1-44.4 24.1-85.4 67.6-95.2 64.3-11.7 128.1-24.7 192.4-35.9 37.9-5.3 70.4-29.8 85.7-64.9 6.8-15.9 11-32.8 12.5-50 0.5-3.1 2.9-5.6 5.9-6.2 3.1-0.7 6.4 0.5 8.2 3l1.7-1.1c25.4 35.9 74.7 114.4 82.7 197.2 8.2 94.8 3.7 160-71.4 226.5-1.1 1.1-1.7 2.6-1.7 4.1 0.1 2 1.1 3.8 2.8 4.8h4.8l3.2-1.8c75.6-40.4 132.8-108.2 159.9-189.5 11.4 16.1 28.5 27.1 47.8 30.8C846 783.9 716.9 871.6 557.2 884.9c-12-28.6-42.5-44.8-72.9-38.6-33.6 5.4-56.6 37-51.2 70.6 4.4 27.6 26.8 48.8 54.5 51.6 30.6 4.6 60.3-13 70.8-42.2 184.9-14.5 333.2-120.8 364.2-286.9 27.8-10.8 46.3-37.4 46.6-67.2V428.7c-0.1-19.5-8.1-38.2-22.3-51.6-14.5-13.8-33.8-21.4-53.8-21.3l1-0.2zM825.9 397c-71.1-176.9-272.1-262.7-449-191.7-86.8 34.9-155.7 103.4-191 190-2.5-2.8-5.2-5.4-8-7.9 25.3-154.6 163.8-268.6 326.8-269.2s302.3 112.6 328.7 267c-2.9 3.8-5.4 7.7-7.5 11.8z"/></svg>';
|
||||||
|
const errorSvg = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>';
|
||||||
|
|
||||||
|
const avatar = role === 'user' ? userSvg : (role === 'bot' ? botSvg : errorSvg);
|
||||||
|
|
||||||
let contentHtml;
|
let contentHtml;
|
||||||
if (role === 'bot') {{
|
if (role === 'bot') {{
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,8 @@ Main FastAPI application for AI Service.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
|
from logging.handlers import RotatingFileHandler
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
|
|
||||||
from fastapi import FastAPI, Request, status
|
from fastapi import FastAPI, Request, status
|
||||||
|
|
@ -52,15 +54,51 @@ from app.core.qdrant_client import close_qdrant_client
|
||||||
|
|
||||||
settings = get_settings()
|
settings = get_settings()
|
||||||
|
|
||||||
logging.basicConfig(
|
|
||||||
level=getattr(logging, settings.log_level.upper()),
|
|
||||||
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
|
||||||
)
|
|
||||||
|
|
||||||
logging.getLogger("sqlalchemy.engine").setLevel(logging.WARNING)
|
def setup_logging():
|
||||||
logging.getLogger("sqlalchemy.pool").setLevel(logging.WARNING)
|
"""
|
||||||
logging.getLogger("sqlalchemy.dialects").setLevel(logging.WARNING)
|
配置滚动日志文件。
|
||||||
logging.getLogger("sqlalchemy.orm").setLevel(logging.WARNING)
|
- 日志文件存储在 logs/ 目录
|
||||||
|
- 单文件最大 2MB,超过则切分
|
||||||
|
- 保留最近 7 天的日志(约 70 个备份文件)
|
||||||
|
- 同时输出到控制台
|
||||||
|
"""
|
||||||
|
log_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), "logs")
|
||||||
|
os.makedirs(log_dir, exist_ok=True)
|
||||||
|
|
||||||
|
log_format = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
||||||
|
formatter = logging.Formatter(log_format)
|
||||||
|
|
||||||
|
root_logger = logging.getLogger()
|
||||||
|
root_logger.setLevel(getattr(logging, settings.log_level.upper()))
|
||||||
|
|
||||||
|
root_logger.handlers.clear()
|
||||||
|
|
||||||
|
console_handler = logging.StreamHandler()
|
||||||
|
console_handler.setLevel(getattr(logging, settings.log_level.upper()))
|
||||||
|
console_handler.setFormatter(formatter)
|
||||||
|
root_logger.addHandler(console_handler)
|
||||||
|
|
||||||
|
log_file = os.path.join(log_dir, "ai-service.log")
|
||||||
|
file_handler = RotatingFileHandler(
|
||||||
|
filename=log_file,
|
||||||
|
maxBytes=2 * 1024 * 1024,
|
||||||
|
backupCount=70,
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
file_handler.setLevel(getattr(logging, settings.log_level.upper()))
|
||||||
|
file_handler.setFormatter(formatter)
|
||||||
|
root_logger.addHandler(file_handler)
|
||||||
|
|
||||||
|
logging.getLogger("sqlalchemy.engine").setLevel(logging.WARNING)
|
||||||
|
logging.getLogger("sqlalchemy.pool").setLevel(logging.WARNING)
|
||||||
|
logging.getLogger("sqlalchemy.dialects").setLevel(logging.WARNING)
|
||||||
|
logging.getLogger("sqlalchemy.orm").setLevel(logging.WARNING)
|
||||||
|
|
||||||
|
return log_dir
|
||||||
|
|
||||||
|
|
||||||
|
setup_logging()
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -116,6 +116,44 @@ class ChatMessageCreate(SQLModel):
|
||||||
content: str
|
content: str
|
||||||
|
|
||||||
|
|
||||||
|
class UserMemory(SQLModel, table=True):
|
||||||
|
"""
|
||||||
|
[AC-IDMP-14] 用户级记忆存储(滚动摘要)
|
||||||
|
支持多租户隔离,存储最新 summary + facts/preferences/open_issues
|
||||||
|
"""
|
||||||
|
|
||||||
|
__tablename__ = "user_memories"
|
||||||
|
__table_args__ = (
|
||||||
|
Index("ix_user_memories_tenant_user", "tenant_id", "user_id"),
|
||||||
|
Index("ix_user_memories_tenant_user_updated", "tenant_id", "user_id", "updated_at"),
|
||||||
|
)
|
||||||
|
|
||||||
|
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
|
||||||
|
tenant_id: str = Field(..., description="Tenant ID for multi-tenant isolation", index=True)
|
||||||
|
user_id: str = Field(..., description="User ID for memory storage", index=True)
|
||||||
|
summary: str | None = Field(default=None, description="Rolling summary for user")
|
||||||
|
facts: list[str] | None = Field(
|
||||||
|
default=None,
|
||||||
|
sa_column=Column("facts", JSON, nullable=True),
|
||||||
|
description="Extracted stable facts list",
|
||||||
|
)
|
||||||
|
preferences: dict[str, Any] | None = Field(
|
||||||
|
default=None,
|
||||||
|
sa_column=Column("preferences", JSON, nullable=True),
|
||||||
|
description="User preferences as structured JSON",
|
||||||
|
)
|
||||||
|
open_issues: list[str] | None = Field(
|
||||||
|
default=None,
|
||||||
|
sa_column=Column("open_issues", JSON, nullable=True),
|
||||||
|
description="Open issues list",
|
||||||
|
)
|
||||||
|
summary_version: int = Field(default=1, description="Summary version / update round")
|
||||||
|
last_turn_id: str | None = Field(default=None, description="Last turn identifier (optional)")
|
||||||
|
expires_at: datetime | None = Field(default=None, description="Expiration time (optional)")
|
||||||
|
created_at: datetime = Field(default_factory=datetime.utcnow, description="Creation time")
|
||||||
|
updated_at: datetime = Field(default_factory=datetime.utcnow, description="Last update time")
|
||||||
|
|
||||||
|
|
||||||
class SharedSession(SQLModel, table=True):
|
class SharedSession(SQLModel, table=True):
|
||||||
"""
|
"""
|
||||||
[AC-IDMP-SHARE] Shared session entity for dialogue sharing.
|
[AC-IDMP-SHARE] Shared session entity for dialogue sharing.
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue