[AC-API-UPDATE] feat(api): 更新 API 端点和实体模型

- 更新 dialogue API 支持新的对话功能
- 更新 share_page API 优化分享页面
- 更新 main.py 注册新的路由模块
- 更新 entities 模型添加新字段
This commit is contained in:
MerCry 2026-03-11 19:07:03 +08:00
parent 9196247578
commit 1490235b8f
4 changed files with 346 additions and 68 deletions

View File

@ -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,

View File

@ -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') {{

View File

@ -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__)

View File

@ -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.