Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ DEEPSEEK_BASE_URL=https://api.deepseek.com
DEEPSEEK_MODEL=deepseek-chat
MYSQL_IMAGE=mysql:8.4

INTERVIEW_MAX_FOLLOW_UP_ROUNDS=3
JWT_SECRET=replace-with-at-least-32-random-characters
RESUME_TOKEN_SECRET=replace-with-a-different-32-character-secret

MYSQL_DSN=mysql+asyncmy://interview:interview@localhost:3307/interview_agent?charset=utf8mb4
SNOWFLAKE_NODE_ID=0
REDIS_URL=redis://localhost:6381/0
MILVUS_URI=http://localhost:19531
MILVUS_COLLECTION=interview_questions_v2
Expand Down Expand Up @@ -36,3 +38,30 @@ CORS_ORIGINS=["http://localhost:5173"]
SESSION_RESUME_TTL_SECONDS=86400
EVENT_RETENTION_SECONDS=604800
AUTO_RECOVER_SESSIONS=true
DYNAMIC_PAGE_FETCH_ENABLED=true
DYNAMIC_PAGE_TIMEOUT_SECONDS=20
DYNAMIC_PAGE_MAX_TRANSFER_BYTES=26214400
DYNAMIC_PAGE_MAX_CONCURRENT=2
DYNAMIC_PAGE_QUEUE_TIMEOUT_SECONDS=5
GITHUB_RECOMMENDATIONS_ENABLED=true
# Optional. Without a token GitHub's public API has a lower rate limit.
GITHUB_TOKEN=
GITHUB_REPOSITORIES_PER_TOPIC=2
GITHUB_CANDIDATE_LIMIT=15
GITHUB_ACTIVE_DAYS=365
GITHUB_FALLBACK_ACTIVE_DAYS=548
GITHUB_MINIMUM_RECOMMENDATION_SCORE=60

# Optional personal-memory semantic index. MySQL remains authoritative when disabled/unavailable.
MEM0_ENABLED=false
MEM0_MODE=cloud
MEM0_API_KEY=
MEM0_HOST=https://api.mem0.ai
MEM0_AGENT_ID=interview-coach
MEM0_TIMEOUT_SECONDS=10
MEM0_OSS_CONFIG_PATH=data/mem0-oss-local.json
MEM0_OSS_OLLAMA_BASE_URL=
MEM0_OSS_QDRANT_PATH=data/mem0_qdrant
MEM0_OSS_COLLECTION_NAME=interview_personal_memory
MEM0_OSS_HISTORY_DB_PATH=data/mem0_history.db
PERSONAL_MEMORY_TOP_K=8
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,7 @@ output/
dist/
volumes/
data/uploads/
data/mem0_qdrant/
data/mem0_history.db
data/eval/reports/*.json
data/eval/reports/*.md
8 changes: 8 additions & 0 deletions CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,11 @@ _Avoid_: 文档、向量集合
**候选人画像**:
根据历次面试表现形成的能力与薄弱点记录,用于后续面试的个性化选题。
_Avoid_: 简历、用户资料

**业务时间**:
面试记录、检查点和候选人画像中面向业务展示与持久化的北京时间(Asia/Shanghai,UTC+8)。
_Avoid_: 服务器本地时间、UTC 存储时间

**业务标识**:
新建业务记录使用的 64 位 Snowflake 十进制 ID;历史 UUID 和自增 ID 仍是有效业务标识。
_Avoid_: 请求 ID、连接 ID、JWT jti
1 change: 1 addition & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ RUN apt-get update \

COPY pyproject.toml uv.lock ./
RUN uv sync --frozen --no-dev --no-install-project
RUN playwright install --with-deps chromium

COPY README.md ./
COPY src ./src
Expand Down
28 changes: 26 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ flowchart LR

## 关键能力

- JD 分析、简历匹配、混合 RAG 出题、自适应难度、追问、评分、评估报告和复习计划。
- JD 分析、简历匹配、混合 RAG 出题、自适应难度、受限多轮动态追问、评分、评估报告和复习计划;单题追问上限由 `INTERVIEW_MAX_FOLLOW_UP_ROUNDS` 配置
- MySQL 权威检查点 + outbox、Redis Streams 事件补发、Milvus 用户隔离题库。
- `session_id + resume_token + last_event_id` 续面,`client_message_id` 写入幂等。
- 后建立连接接管同一面试会话;未完成会话默认保留 24 小时。
Expand Down Expand Up @@ -63,13 +63,36 @@ cp .env.example .env
# 编辑 .env,填写 DEEPSEEK_API_KEY,并分别生成 JWT_SECRET、RESUME_TOKEN_SECRET

uv sync --frozen
uv run playwright install chromium
docker compose up -d mysql redis etcd minio milvus embeddings reranker speech speech-models
uv run alembic upgrade head
uv run interview-agent serve --reload
```

这个开发模式适合调试 Python;完整语音链路建议使用上面的 `--profile app` 全容器启动,因为中文 VITS 模型卷会直接挂载到应用容器。

### 数据库时间与 ID

- MySQL 业务时间列使用 `DATETIME(6)` 保存北京时间(`Asia/Shanghai`),API 返回的业务时间携带 `+08:00` 偏移。
- 新建用户、面试会话、题目、候选人记忆、outbox 事件和内部记录统一使用 64 位 Snowflake ID;对外 ID 保持十进制字符串,避免浏览器 JavaScript 丢失整数精度。
- `SNOWFLAKE_NODE_ID` 取值为 `0-1023`,默认 `0`。单实例本地开发可保留默认值;多副本部署必须为每个同时运行的实例分配不同节点号,否则可能产生重复 ID。
- 既有 UUID 和自增 ID 不会重写,新旧 ID 可以同时查询和关联。

从 `0001` 或更早的现有数据库升级前先备份 MySQL,然后执行:

```bash
uv run alembic upgrade head
```

`0002` 会把现有 UTC 时间列一次性增加 8 小时,并将原自增内部主键扩展为 `BIGINT`;`0003` 会把检查点和候选人画像 JSON 内的 UTC 时间同步转换为 `+08:00`。已执行过 `0002` 的环境继续运行 `upgrade head` 即可补迁,且不会重复转换。迁移完成后可用下面的查询确认北京时间:

```sql
SELECT id, status, created_at, updated_at
FROM interview_sessions
ORDER BY created_at DESC
LIMIT 10;
```

服务地址:

- 浏览器前端:http://localhost:9092/
Expand All @@ -88,11 +111,12 @@ uv run interview-agent serve --reload
对应的可编辑 Figma 面试台位于 [Interview Room — Apple Developer UI](https://www.figma.com/design/QUCP3r8UZnwNU0YwP32wVa?node-id=3-2)。

1. 创建账号,在“面试台”为 JD 和简历粘贴文字、填写 URL,或分别上传 PDF。上传后页面原样预览 PDF,提取文字只交给后端面试流程,不显示在编辑器中。
JD URL 会先进行轻量正文提取;遇到 JavaScript 动态页面或站点阻止普通请求时,会自动降级为 Chromium 渲染。复习计划会按结构化薄弱主题查询 GitHub,过滤资源清单和长期未维护仓库,并在项目卡片展示推荐理由与最近推送日期;可选配置 `GITHUB_TOKEN` 提高 API 限额。
桌面端向右拖动 PDF 面板旁的分隔条即可放大;方向键可微调,`Shift + 方向键` 可快速调节,双击恢复默认宽度。“放大”按钮会打开全屏原文件预览。
2. 收到题目后数字人自动用普通话播报;点击“语音回答”录音,结束后可编辑 Whisper 转写,再提交并观察评分、追问、评估报告与复习计划。
3. 面试途中点击“模拟断线”,客户端会自动用 `session_id + resume_token + last_event_id` 续面。
4. 在等待回答时直接刷新浏览器,事件时间线与当前作答状态应恢复。
5. 使用“我的题库”上传 Markdown、TXT、PDF 或 DOCX,再开始面试验证个人 RAG完成后可在“面试档案”查看历史报告。
5. 使用“我的题库”上传 Markdown、TXT、PDF 或 DOCX;上传后可按来源、难度、题型或关键词浏览题目与完整参考答案,再开始面试验证个人 RAG完成后可在“面试档案”查看历史报告。

为避免覆盖机器上常见的本地服务,Compose 默认把 MySQL、Redis、Milvus 分别映射到 `3307`、`6381`、`19531`,应用映射到 `9092`;都可通过同名 `*_PORT` 环境变量覆盖。MinIO 只供 Milvus 内部使用,不暴露宿主机端口。

Expand Down
26 changes: 26 additions & 0 deletions data/mem0-oss-local.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"mode": "oss",
"oss": {
"llm": {
"provider": "ollama",
"config": {
"model": "qwen2.5-coder:14b",
"ollama_base_url": "http://127.0.0.1:11434"
}
},
"embedder": {
"provider": "ollama",
"config": {
"model": "nomic-embed-text",
"ollama_base_url": "http://127.0.0.1:11434",
"embedding_dims": 768
}
},
"vector_store": {
"provider": "qdrant",
"config": {
"path": "data/mem0_qdrant"
}
}
}
}
7 changes: 7 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,11 @@ services:
RERANK_BASE_URL: http://reranker:80
SPEECH_BASE_URL: http://speech:8000
TTS_MODEL_DIRECTORY: /models/sherpa-onnx-vits-zh-ll
MEM0_OSS_OLLAMA_BASE_URL: http://host.docker.internal:11434
MEM0_OSS_QDRANT_PATH: /mem0/qdrant
MEM0_OSS_HISTORY_DB_PATH: /mem0/history.db
extra_hosts:
- "host.docker.internal:host-gateway"
ports:
- "${APP_PORT:-9092}:9090"
depends_on:
Expand All @@ -252,6 +257,7 @@ services:
condition: service_completed_successfully
volumes:
- tts_models:/models:ro
- mem0_data:/mem0
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:9090/health/ready"]
interval: 10s
Expand All @@ -269,3 +275,4 @@ volumes:
speech_hf_cache:
tts_models:
reranker_models:
mem0_data:
20 changes: 20 additions & 0 deletions docs/adr/0006-use-isolated-qdrant-for-mem0-oss.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# ADR 0006:mem0 OSS 使用独立 Qdrant 派生索引

## 状态

已接受。

## 背景

InterviewAgent 已使用 MySQL 保存权威业务状态,使用 Milvus 保存可重建题库向量。现有 Hermes 配置提供了 mem0 OSS 所需的 Ollama LLM、Ollama Embedder 与本地 Qdrant 参数,但其 Qdrant 目录属于 Hermes,嵌入式 Qdrant 也不适合被两个进程并发打开。

## 决策

- 允许读取 Hermes 兼容的 mem0 JSON 配置,只复用 provider、model、嵌入维度和 Ollama 地址。
- InterviewAgent 覆盖 Qdrant 路径、集合名和 mem0 history 数据库,使用自己的持久化目录。
- MySQL 继续是个人画像的唯一权威存储;mem0/Qdrant 仅保存可删除、可重建的压缩学习信号。
- mem0 仍为显式 opt-in,初始化或运行失败时回退 MySQL 确定性召回。

## 结果

该方案可以直接利用本机已有 Ollama 模型,又不会让 Hermes 与 InterviewAgent 共享用户命名空间、文件锁或向量生命周期。代价是本地额外保存一份小型派生索引;如果以后改用 mem0 的 Milvus provider,需要单独验证版本、过滤和集合隔离后再替换。
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# ADR 0007:使用北京时间与 Snowflake 业务标识

## 状态

已接受。

## 背景

面试记录此前以 UTC 写入 MySQL,用户直接查询表时会看到比北京时间少 8 小时的值;新业务实体同时混用 UUID 与数据库自增 ID,不利于跨服务生成和按时间粗略排序。历史数据已被外键、检查点 JSON 和候选人画像 JSON 引用,不能整体重写标识。

## 决策

- 领域层持久化时间使用带 `+08:00` 的 `Asia/Shanghai` 时间;MySQL `DATETIME(6)` 保存去除时区后的北京时间墙上时间,读取时恢复 `+08:00`。
- `0002` 升级把既有关系型时间列整体加 8 小时;`0003` 把会话检查点与候选人画像 JSON 中已有的 UTC 时间转换为等价的 `+08:00` 时间。拆分版本确保已执行 `0002` 的环境仍会得到 JSON 补迁。
- 新建用户、会话、问题、记忆、事件及内部记录使用 64 位 Snowflake ID。历史 UUID 和自增 ID 原样保留,因此所有已有外键和恢复凭证继续有效。
- 每个并行写入实例必须配置唯一的 `SNOWFLAKE_NODE_ID`(0–1023)。同一部署内复用节点号属于配置错误;数据库唯一约束是最终防线,碰撞时写事务失败,不静默改号或覆盖数据。
- 连接 ID、客户端消息 ID 和 JWT `jti` 不是持久化业务主键,继续使用 UUID。

## 迁移与失败语义

升级前必须备份 MySQL。列类型变更由 MySQL DDL 执行;关系型时间更新在显式事务中完成,JSON 时间转换可重复执行且只转换带时区值。升级中断后先检查 `alembic_version` 和备份,再重跑升级;若数据库状态无法确认,从升级前备份恢复。

Snowflake 序列在同一毫秒耗尽时等待下一毫秒。短暂时钟回拨使用进程内最后时间戳继续递增,避免本实例重复;跨实例唯一性由不同节点号保证。产生新 Snowflake 值后不支持直接降级回 32 位自增列,回退应恢复升级前备份,而不是执行破坏性 ID 转换。

## 取舍

该方案让数据库中的时间符合当前主要用户习惯,并允许应用层生成趋势递增的业务 ID;代价是数据库值不再是 UTC,跨时区分析必须显式转换,并且部署需要管理节点号。保留历史标识避免高风险全量主外键重写,但同一列会长期兼容 UUID 与十进制 Snowflake 字符串。
Loading
Loading