Skip to content

Commit 00da74d

Browse files
committed
chore(profile-helper): squash merge optimize profile helper
1 parent 3908624 commit 00da74d

37 files changed

Lines changed: 5785 additions & 4070 deletions

File tree

.github/workflows/tests-ci.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ jobs:
2222
- uses: actions/checkout@v4
2323
with:
2424
fetch-depth: 0
25+
submodules: recursive
2526

2627
- name: Diff and set outputs
2728
id: diff
@@ -63,6 +64,8 @@ jobs:
6364
steps:
6465
- name: Checkout
6566
uses: actions/checkout@v4
67+
with:
68+
submodules: recursive
6669

6770
- name: Setup uv
6871
uses: astral-sh/setup-uv@v5
@@ -91,6 +94,8 @@ jobs:
9194
steps:
9295
- name: Checkout
9396
uses: actions/checkout@v4
97+
with:
98+
submodules: recursive
9499

95100
- name: Setup uv
96101
uses: astral-sh/setup-uv@v5
@@ -132,6 +137,8 @@ jobs:
132137
- name: Checkout
133138
if: steps.check_key.outputs.has_key == 'true'
134139
uses: actions/checkout@v4
140+
with:
141+
submodules: recursive
135142

136143
- name: Setup uv
137144
if: steps.check_key.outputs.has_key == 'true'

README.en.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,7 @@ Or use `docker compose up --build`.
239239
- **Moderator Modes**: `GET /moderator-modes`, `GET /moderator-modes/assignable/categories`, `GET /moderator-modes/assignable` (supports `category`, `q`, `fields`, `limit`, `offset`), `GET /moderator-modes/assignable/{id}/content`, `GET/PUT /topics/{topic_id}/moderator-mode`, `POST .../moderator-mode/generate`, `POST .../moderator-mode/share`
240240
- **Experts**: `GET /experts` (supports `fields=minimal` for list without skill_content), `GET /experts/{name}/content`, `GET/PUT /experts/{name}`, `POST /experts/import-profile` (import forum profile as expert)
241241
- **Libs**: `POST /libs/invalidate-cache` — clear libs meta cache immediately (hot-reload)
242-
- **Profile Helper**: `GET /profile-helper/session`, `POST /profile-helper/chat` (SSE), `GET /profile-helper/profile/{session_id}`, `GET /profile-helper/download/{session_id}`, `GET /profile-helper/download/{session_id}/forum`, `POST /profile-helper/session/reset/{session_id}`
242+
- **Profile Helper**: `GET /profile-helper/session`, `POST /profile-helper/chat` (SSE), `POST /profile-helper/chat/blocks` (Block SSE), `GET /profile-helper/chat-history/{session_id}`, `GET /profile-helper/profile/{session_id}`, `GET /profile-helper/profile/{session_id}/structured`, `GET /profile-helper/profile/{session_id}/scientists/famous|field`, `GET /profile-helper/download/{session_id}`, `GET /profile-helper/download/{session_id}/forum`, `POST /profile-helper/scales/submit`, `POST /profile-helper/publish-to-library`, `POST /profile-helper/session/reset/{session_id}`
243243

244244
> Profile Helper auth modes: `AUTH_MODE=none|jwt|proxy` (default `none`). Account sync after publish is optional via `ACCOUNT_SYNC_ENABLED` and does not block the main publish flow on failure.
245245
>

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,7 @@ docker run --rm -p 8000:8000 --env-file .env \
241241
- **Moderator Modes**`GET /moderator-modes``GET /moderator-modes/assignable`(支持 `category``q``fields``limit``offset`),`GET /moderator-modes/assignable/{id}/content``GET/PUT /topics/{topic_id}/moderator-mode``POST .../moderator-mode/generate`
242242
- **Experts**`GET /experts`(支持 `fields=minimal`,列表不加载 skill_content),`GET /experts/{name}/content``GET/PUT /experts/{name}``POST /experts/import-profile`(论坛画像导入为专家)
243243
- **Libs**`POST /libs/invalidate-cache` 立即清空库 meta 缓存(热更新)
244-
- **Profile Helper**`GET /profile-helper/session``POST /profile-helper/chat`(SSE),`GET /profile-helper/profile/{session_id}``GET /profile-helper/download/{session_id}``POST /profile-helper/session/reset/{session_id}`
244+
- **Profile Helper**`GET /profile-helper/session``POST /profile-helper/chat`(SSE),`POST /profile-helper/chat/blocks`(Block SSE),`GET /profile-helper/chat-history/{session_id}``GET /profile-helper/profile/{session_id}``GET /profile-helper/profile/{session_id}/structured``GET /profile-helper/profile/{session_id}/scientists/famous|field``GET /profile-helper/download/{session_id}``GET /profile-helper/download/{session_id}/forum``POST /profile-helper/scales/submit``POST /profile-helper/publish-to-library``POST /profile-helper/session/reset/{session_id}`
245245

246246
> Profile Helper 认证模式:`AUTH_MODE=none|jwt|proxy`(默认 `none`)。发布后的账号库同步由 `ACCOUNT_SYNC_ENABLED` 控制,失败不会阻断主发布流程。
247247
>

app/api/profile_helper.py

Lines changed: 105 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from app.api.auth_bridge import get_current_auth_context
1111
from app.integrations.account_sync import sync_twin_record
1212
from app.services.profile_helper import agent as profile_agent
13+
from app.services.profile_helper import block_agent as profile_block_agent
1314
from app.services.profile_helper import sessions as profile_sessions
1415

1516
router = APIRouter()
@@ -37,12 +38,18 @@ class PublishRequest(BaseModel):
3738
display_name: str | None = None
3839

3940

40-
def _get_uid(auth_ctx: dict) -> str:
41+
def _get_uid(auth_ctx: dict) -> str | None:
4142
auth_context = auth_ctx.get("auth_context")
4243
if auth_context is not None:
43-
return str(auth_context.subject)
44+
if getattr(auth_context, "is_anonymous", False):
45+
return None
46+
subject = str(auth_context.subject).strip()
47+
return subject or None
4448
user = auth_ctx.get("user") or {}
45-
return str(user.get("id", "anonymous"))
49+
uid = str(user.get("id", "")).strip()
50+
if not uid or uid.lower() == "anonymous":
51+
return None
52+
return uid
4653

4754

4855
def _normalize_session_id(session_id: str | None) -> str | None:
@@ -54,15 +61,17 @@ def _normalize_session_id(session_id: str | None) -> str | None:
5461
return sid
5562

5663

57-
def _get_session_for_user(session_id: str, uid: str) -> dict:
64+
def _get_session_for_user(session_id: str, uid: str | None) -> dict:
5865
session = profile_sessions.get(session_id)
5966
if not session:
60-
raise HTTPException(status_code=404, detail="会话不存在或已过期")
67+
# 会话不在内存中(常见于 uvicorn --reload 后热重载导致内存清空)
68+
# 尝试用 get_or_create 从磁盘恢复画像文件
69+
_, session = profile_sessions.get_or_create(session_id, user_id=uid)
6170

6271
existing_uid = session.get("user_id")
6372
if existing_uid and str(existing_uid) != str(uid):
6473
raise HTTPException(status_code=404, detail="会话不存在或已过期")
65-
if not existing_uid:
74+
if not existing_uid and uid:
6675
session["user_id"] = uid
6776
return session
6877

@@ -209,6 +218,46 @@ def generate():
209218
)
210219

211220

221+
@router.post("/chat/blocks", response_class=StreamingResponse)
222+
async def chat_blocks_stream(
223+
req: ChatRequest,
224+
auth_ctx: dict = Depends(get_current_auth_context),
225+
):
226+
"""Block 协议 SSE:每个 Block 作为一条 SSE 事件发送。"""
227+
uid = _get_uid(auth_ctx)
228+
normalized_session_id = _normalize_session_id(req.session_id)
229+
session_id, session = profile_sessions.get_or_create(
230+
normalized_session_id,
231+
user_id=uid,
232+
)
233+
if not req.message.strip():
234+
raise HTTPException(status_code=400, detail="消息不能为空")
235+
236+
def generate():
237+
try:
238+
blocks = profile_block_agent.run_block_agent(
239+
req.message, session, model=req.model
240+
)
241+
# 每轮对话结束后立即将消息历史持久化到磁盘
242+
# 确保 session 过期后仍可从磁盘恢复 AI 上下文
243+
profile_sessions.save_messages(session)
244+
for block in blocks:
245+
yield f"data: {json.dumps(block, ensure_ascii=False)}\n\n"
246+
except Exception as e:
247+
yield f"data: {json.dumps({'type': 'text', 'content': f'服务器错误: {e}'}, ensure_ascii=False)}\n\n"
248+
yield "data: [DONE]\n\n"
249+
250+
return StreamingResponse(
251+
generate(),
252+
media_type="text/event-stream",
253+
headers={
254+
"Cache-Control": "no-cache",
255+
"Connection": "keep-alive",
256+
"X-Session-Id": session_id,
257+
},
258+
)
259+
260+
212261
@router.get("/profile/{session_id}")
213262
async def get_profile(
214263
session_id: str,
@@ -223,42 +272,74 @@ async def get_profile(
223272
}
224273

225274

226-
@router.get("/profile/{session_id}/structured")
227-
async def get_structured_profile(
275+
@router.get("/chat-history/{session_id}")
276+
async def get_chat_history(
228277
session_id: str,
229278
auth_ctx: dict = Depends(get_current_auth_context),
230279
):
280+
"""返回 session 的用户可见对话历史(过滤 tool 角色,保留 user/assistant 可读消息)。"""
231281
uid = _get_uid(auth_ctx)
232-
session = _get_session_for_user(session_id, uid)
233-
from app.services.profile_helper.profile_parser import parse_profile
282+
normalized = _normalize_session_id(session_id)
283+
session = _get_session_for_user(normalized, uid)
234284

235-
return parse_profile(session["profile"])
285+
# 优先从内存 session 读取;内存中没有则从磁盘恢复
286+
raw_messages = session.get("messages") or profile_sessions.load_messages(normalized, uid)
287+
288+
# 只保留用户可读的消息(user 角色 + 有文字内容的 assistant 角色)
289+
visible: list[dict] = []
290+
for m in raw_messages:
291+
role = m.get("role")
292+
if role == "user":
293+
content = m.get("content", "")
294+
if content:
295+
visible.append({"role": "user", "content": content})
296+
elif role == "assistant":
297+
content = m.get("content", "")
298+
# 跳过纯工具调用中转消息(content 为空或仅是系统桥接短文本)
299+
if content and len(content) > 5 and not content.startswith("("):
300+
visible.append({"role": "assistant", "content": content})
301+
302+
return {"messages": visible, "count": len(visible)}
236303

237304

238305
@router.get("/profile/{session_id}/scientists/famous")
239-
async def get_famous_scientist_matches(
306+
async def get_famous_scientists(
240307
session_id: str,
241308
auth_ctx: dict = Depends(get_current_auth_context),
242309
):
243-
"""Top famous-scientist matches + scatter plot data (placeholder until matcher is wired)."""
310+
"""知名科学家相似度匹配(纯计算,瞬间返回)。"""
244311
uid = _get_uid(auth_ctx)
245-
_ = _get_session_for_user(session_id, uid)
246-
return {
247-
"top3": [],
248-
"scatter_data": [],
249-
"user_point": {"csi": 0.5, "rai": 0.5},
250-
}
312+
session = _get_session_for_user(session_id, uid)
313+
from app.services.profile_helper.profile_parser import parse_profile
314+
from app.services.profile_helper.scientist_match import match_famous_scientists
315+
parsed = parse_profile(session["profile"])
316+
return match_famous_scientists(parsed)
251317

252318

253319
@router.get("/profile/{session_id}/scientists/field")
254320
async def get_field_scientist_recommendations(
255321
session_id: str,
256322
auth_ctx: dict = Depends(get_current_auth_context),
257323
):
258-
"""Same-field scholar recommendations (placeholder until recommender is wired)."""
324+
"""领域相关科学家推荐(LLM 调用,可能较慢)。"""
259325
uid = _get_uid(auth_ctx)
260-
_ = _get_session_for_user(session_id, uid)
261-
return {"recommendations": []}
326+
session = _get_session_for_user(session_id, uid)
327+
from app.services.profile_helper.profile_parser import parse_profile
328+
from app.services.profile_helper.scientist_match import recommend_field_scientists
329+
parsed = parse_profile(session["profile"])
330+
return {"recommendations": recommend_field_scientists(parsed)}
331+
332+
333+
@router.get("/profile/{session_id}/structured")
334+
async def get_structured_profile(
335+
session_id: str,
336+
auth_ctx: dict = Depends(get_current_auth_context),
337+
):
338+
uid = _get_uid(auth_ctx)
339+
session = _get_session_for_user(session_id, uid)
340+
from app.services.profile_helper.profile_parser import parse_profile
341+
342+
return parse_profile(session["profile"])
262343

263344

264345
@router.get("/download/{session_id}")
@@ -335,6 +416,8 @@ async def publish_to_library(
335416

336417
token = auth_ctx.get("token")
337418
uid = _get_uid(auth_ctx)
419+
if uid is None:
420+
raise HTTPException(status_code=401, detail="请先登录后再发布数字分身")
338421
session = _get_session_for_user(req.session_id, uid)
339422
full_profile = session.get("profile", "")
340423
forum_profile = session.get("forum_profile", "")
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
"""Profile helper module: research profile collection assistant."""
1+
"""Profile helper module: research profile collection assistant."""

0 commit comments

Comments
 (0)