loop-agent-cli 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- loop_agent_cli-0.1.0/MANIFEST.in +13 -0
- loop_agent_cli-0.1.0/PKG-INFO +202 -0
- loop_agent_cli-0.1.0/README.md +167 -0
- loop_agent_cli-0.1.0/app/__init__.py +0 -0
- loop_agent_cli-0.1.0/app/agents/__init__.py +0 -0
- loop_agent_cli-0.1.0/app/agents/graph.py +40 -0
- loop_agent_cli-0.1.0/app/agents/nodes.py +245 -0
- loop_agent_cli-0.1.0/app/agents/state.py +19 -0
- loop_agent_cli-0.1.0/app/api/__init__.py +0 -0
- loop_agent_cli-0.1.0/app/api/candidates.py +49 -0
- loop_agent_cli-0.1.0/app/api/dashboard.py +7 -0
- loop_agent_cli-0.1.0/app/api/outreach.py +7 -0
- loop_agent_cli-0.1.0/app/api/pipelines.py +58 -0
- loop_agent_cli-0.1.0/app/api/positions.py +76 -0
- loop_agent_cli-0.1.0/app/api/router.py +14 -0
- loop_agent_cli-0.1.0/app/api/scheduler.py +7 -0
- loop_agent_cli-0.1.0/app/api/skills.py +7 -0
- loop_agent_cli-0.1.0/app/api/system.py +7 -0
- loop_agent_cli-0.1.0/app/core/__init__.py +0 -0
- loop_agent_cli-0.1.0/app/core/config.py +18 -0
- loop_agent_cli-0.1.0/app/core/exception_handler.py +91 -0
- loop_agent_cli-0.1.0/app/core/exceptions.py +33 -0
- loop_agent_cli-0.1.0/app/core/logging.py +19 -0
- loop_agent_cli-0.1.0/app/database/__init__.py +0 -0
- loop_agent_cli-0.1.0/app/database/base.py +4 -0
- loop_agent_cli-0.1.0/app/database/session.py +20 -0
- loop_agent_cli-0.1.0/app/main.py +72 -0
- loop_agent_cli-0.1.0/app/models/__init__.py +0 -0
- loop_agent_cli-0.1.0/app/models/agent_run.py +18 -0
- loop_agent_cli-0.1.0/app/models/candidate.py +28 -0
- loop_agent_cli-0.1.0/app/models/node_log.py +18 -0
- loop_agent_cli-0.1.0/app/models/outreach_log.py +16 -0
- loop_agent_cli-0.1.0/app/models/pipeline.py +21 -0
- loop_agent_cli-0.1.0/app/models/position.py +22 -0
- loop_agent_cli-0.1.0/app/models/scheduler_job.py +16 -0
- loop_agent_cli-0.1.0/app/models/skill.py +13 -0
- loop_agent_cli-0.1.0/app/models/system_config.py +12 -0
- loop_agent_cli-0.1.0/app/repositories/__init__.py +0 -0
- loop_agent_cli-0.1.0/app/repositories/agent_run.py +74 -0
- loop_agent_cli-0.1.0/app/repositories/candidate.py +84 -0
- loop_agent_cli-0.1.0/app/repositories/node_log.py +57 -0
- loop_agent_cli-0.1.0/app/repositories/outreach_log.py +60 -0
- loop_agent_cli-0.1.0/app/repositories/pipeline.py +80 -0
- loop_agent_cli-0.1.0/app/repositories/position.py +67 -0
- loop_agent_cli-0.1.0/app/repositories/scheduler_job.py +74 -0
- loop_agent_cli-0.1.0/app/schemas/__init__.py +0 -0
- loop_agent_cli-0.1.0/app/schemas/agent_run.py +32 -0
- loop_agent_cli-0.1.0/app/schemas/candidate.py +58 -0
- loop_agent_cli-0.1.0/app/schemas/node_log.py +31 -0
- loop_agent_cli-0.1.0/app/schemas/outreach_log.py +28 -0
- loop_agent_cli-0.1.0/app/schemas/pipeline.py +34 -0
- loop_agent_cli-0.1.0/app/schemas/position.py +49 -0
- loop_agent_cli-0.1.0/app/schemas/scheduler_job.py +29 -0
- loop_agent_cli-0.1.0/app/services/__init__.py +0 -0
- loop_agent_cli-0.1.0/app/services/candidate.py +58 -0
- loop_agent_cli-0.1.0/app/services/dashboard.py +230 -0
- loop_agent_cli-0.1.0/app/services/email.py +116 -0
- loop_agent_cli-0.1.0/app/services/health.py +105 -0
- loop_agent_cli-0.1.0/app/services/pipeline.py +75 -0
- loop_agent_cli-0.1.0/app/services/position.py +36 -0
- loop_agent_cli-0.1.0/app/services/runner.py +292 -0
- loop_agent_cli-0.1.0/app/services/scheduler.py +174 -0
- loop_agent_cli-0.1.0/app/services/score.py +155 -0
- loop_agent_cli-0.1.0/app/services/search.py +92 -0
- loop_agent_cli-0.1.0/app/skills/base.py +30 -0
- loop_agent_cli-0.1.0/app/skills/github.py +106 -0
- loop_agent_cli-0.1.0/app/skills/registry.py +51 -0
- loop_agent_cli-0.1.0/app/tests/__init__.py +3 -0
- loop_agent_cli-0.1.0/app/tests/conftest.py +96 -0
- loop_agent_cli-0.1.0/app/tests/generate_report.py +144 -0
- loop_agent_cli-0.1.0/app/tests/test_candidates.py +158 -0
- loop_agent_cli-0.1.0/app/tests/test_dashboard.py +27 -0
- loop_agent_cli-0.1.0/app/tests/test_outreach.py +15 -0
- loop_agent_cli-0.1.0/app/tests/test_pipelines.py +249 -0
- loop_agent_cli-0.1.0/app/tests/test_positions.py +183 -0
- loop_agent_cli-0.1.0/app/tests/test_scheduler.py +15 -0
- loop_agent_cli-0.1.0/app/tests/test_skills.py +15 -0
- loop_agent_cli-0.1.0/app/tests/test_system.py +35 -0
- loop_agent_cli-0.1.0/app/utils/__init__.py +0 -0
- loop_agent_cli-0.1.0/loop_agent_cli/__init__.py +5 -0
- loop_agent_cli-0.1.0/loop_agent_cli/cli.py +728 -0
- loop_agent_cli-0.1.0/loop_agent_cli/container.py +191 -0
- loop_agent_cli-0.1.0/loop_agent_cli.egg-info/PKG-INFO +202 -0
- loop_agent_cli-0.1.0/loop_agent_cli.egg-info/SOURCES.txt +88 -0
- loop_agent_cli-0.1.0/loop_agent_cli.egg-info/dependency_links.txt +1 -0
- loop_agent_cli-0.1.0/loop_agent_cli.egg-info/entry_points.txt +2 -0
- loop_agent_cli-0.1.0/loop_agent_cli.egg-info/requires.txt +12 -0
- loop_agent_cli-0.1.0/loop_agent_cli.egg-info/top_level.txt +2 -0
- loop_agent_cli-0.1.0/pyproject.toml +57 -0
- loop_agent_cli-0.1.0/setup.cfg +4 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
include LICENSE
|
|
2
|
+
include README.md
|
|
3
|
+
include pyproject.toml
|
|
4
|
+
|
|
5
|
+
recursive-include loop_agent_cli *.py
|
|
6
|
+
recursive-include app *.py
|
|
7
|
+
|
|
8
|
+
global-exclude __pycache__
|
|
9
|
+
global-exclude *.py[cod]
|
|
10
|
+
global-exclude *.so
|
|
11
|
+
global-exclude .pytest_cache/*
|
|
12
|
+
global-exclude venv/*
|
|
13
|
+
global-exclude frontend/*
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: loop-agent-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Typer + Rich CLI for recruit-loop-agent — run recruiting loops from the terminal
|
|
5
|
+
Author-email: Chandler Song <275737875@qq.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/Chandler-Song/recruit-loop-agent
|
|
8
|
+
Project-URL: Repository, https://github.com/Chandler-Song/recruit-loop-agent
|
|
9
|
+
Project-URL: Issues, https://github.com/Chandler-Song/recruit-loop-agent/issues
|
|
10
|
+
Keywords: recruiting,agent,cli,typer,langgraph
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
21
|
+
Requires-Python: >=3.9
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
Requires-Dist: typer>=0.9.0
|
|
24
|
+
Requires-Dist: rich>=13.0.0
|
|
25
|
+
Requires-Dist: fastapi>=0.104.0
|
|
26
|
+
Requires-Dist: sqlalchemy[asyncio]>=2.0.23
|
|
27
|
+
Requires-Dist: aiosqlite>=0.19.0
|
|
28
|
+
Requires-Dist: pydantic>=2.5.0
|
|
29
|
+
Requires-Dist: pydantic-settings>=2.1.0
|
|
30
|
+
Requires-Dist: httpx>=0.25.0
|
|
31
|
+
Requires-Dist: langgraph>=0.0.40
|
|
32
|
+
Requires-Dist: apscheduler>=3.10.0
|
|
33
|
+
Requires-Dist: python-dotenv>=1.0.0
|
|
34
|
+
Requires-Dist: cryptography>=41.0.0
|
|
35
|
+
|
|
36
|
+
# loop-agent-cli
|
|
37
|
+
|
|
38
|
+
> Typer + Rich CLI for [recruit-loop-agent](https://github.com/Chandler-Song/recruit-loop-agent) — 在终端中完成招聘循环的全部操作,无需启动 FastAPI Web 服务。
|
|
39
|
+
|
|
40
|
+
## 功能特性
|
|
41
|
+
|
|
42
|
+
- **零服务依赖**:不启动 uvicorn,直接在进程内调用核心引擎
|
|
43
|
+
- **完整功能覆盖**:职位管理、候选人查看、管道操作、循环执行、调度控制、仪表盘
|
|
44
|
+
- **终端友好**:Rich 彩色表格/面板,结构化输出
|
|
45
|
+
|
|
46
|
+
## 安装
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
# 从 PyPI 安装(正式发布后)
|
|
50
|
+
pip install loop-agent-cli
|
|
51
|
+
|
|
52
|
+
# 开发模式安装(从项目根目录)
|
|
53
|
+
cd recruit-loop-agent
|
|
54
|
+
pip install -e .
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## 前置条件
|
|
58
|
+
|
|
59
|
+
- Python >= 3.11
|
|
60
|
+
- `.env` 文件需存在于项目根目录(配置 `GITHUB_TOKEN`、`SMTP_*` 等)
|
|
61
|
+
- 项目根目录需包含 `app/` 核心引擎包
|
|
62
|
+
|
|
63
|
+
## 使用示例
|
|
64
|
+
|
|
65
|
+
### 查看帮助
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
loop-agent --help
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### 职位管理
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
# 列出所有职位
|
|
75
|
+
loop-agent position list
|
|
76
|
+
|
|
77
|
+
# 创建新职位
|
|
78
|
+
loop-agent position create -t "Senior Backend Engineer" -c "Tech Corp" \
|
|
79
|
+
-s "Python,FastAPI,PostgreSQL" -k "backend,python,remote"
|
|
80
|
+
|
|
81
|
+
# 查看职位详情
|
|
82
|
+
loop-agent position show <position_id>
|
|
83
|
+
|
|
84
|
+
# 关闭职位
|
|
85
|
+
loop-agent position close <position_id>
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### 执行招聘循环
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
# 对已有职位执行一次循环
|
|
92
|
+
loop-agent run position <position_id>
|
|
93
|
+
|
|
94
|
+
# 创建职位并立即执行循环
|
|
95
|
+
loop-agent run create-and-run -t "Python Developer" -c "Startup Inc" \
|
|
96
|
+
-s "Python,Django" -i 30
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### 候选人管理
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
# 列出候选人(默认 20 条)
|
|
103
|
+
loop-agent candidate list -n 10
|
|
104
|
+
|
|
105
|
+
# 查看候选人详情
|
|
106
|
+
loop-agent candidate show <candidate_id>
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### 管道管理
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
# 列出所有管道
|
|
113
|
+
loop-agent pipeline list
|
|
114
|
+
|
|
115
|
+
# 按职位过滤
|
|
116
|
+
loop-agent pipeline list -p <position_id>
|
|
117
|
+
|
|
118
|
+
# 更新管道状态
|
|
119
|
+
loop-agent pipeline update-status <pipeline_id> contacted
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### 调度管理
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
# 启动后台调度器(Ctrl+C 退出)
|
|
126
|
+
loop-agent schedule start
|
|
127
|
+
|
|
128
|
+
# 列出调度任务
|
|
129
|
+
loop-agent schedule list
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### 仪表盘
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
# 查看仪表盘摘要
|
|
136
|
+
loop-agent dashboard
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### 其他
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
# 显示 LangGraph 图结构
|
|
143
|
+
loop-agent graph
|
|
144
|
+
|
|
145
|
+
# 显示版本信息
|
|
146
|
+
loop-agent version
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### 全局选项
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
# 覆盖数据库路径
|
|
153
|
+
loop-agent --db sqlite+aiosqlite:///./custom.db position list
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## 命令树
|
|
157
|
+
|
|
158
|
+
```
|
|
159
|
+
loop-agent
|
|
160
|
+
├── run 执行招聘循环
|
|
161
|
+
│ ├── position <position_id> 对已有职位执行一次循环
|
|
162
|
+
│ └── create-and-run 创建职位并立即执行循环
|
|
163
|
+
├── position 职位管理
|
|
164
|
+
│ ├── list 列出所有职位
|
|
165
|
+
│ ├── create 创建新职位
|
|
166
|
+
│ ├── show <position_id> 查看职位详情
|
|
167
|
+
│ └── close <position_id> 关闭职位
|
|
168
|
+
├── candidate 候选人管理
|
|
169
|
+
│ ├── list 列出候选人
|
|
170
|
+
│ └── show <candidate_id> 查看候选人详情
|
|
171
|
+
├── pipeline 招聘管道管理
|
|
172
|
+
│ ├── list 列出管道
|
|
173
|
+
│ └── update-status 更新管道状态
|
|
174
|
+
├── schedule 调度管理
|
|
175
|
+
│ ├── start 启动后台调度器
|
|
176
|
+
│ └── list 列出调度任务
|
|
177
|
+
├── dashboard 查看仪表盘摘要
|
|
178
|
+
├── graph 显示 LangGraph 图结构
|
|
179
|
+
└── version 显示版本信息
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## 开发
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
# 克隆项目
|
|
186
|
+
git clone git@github.com:Chandler-Song/recruit-loop-agent.git
|
|
187
|
+
cd recruit-loop-agent
|
|
188
|
+
|
|
189
|
+
# 创建虚拟环境
|
|
190
|
+
python -m venv venv
|
|
191
|
+
source venv/bin/activate
|
|
192
|
+
|
|
193
|
+
# 安装依赖
|
|
194
|
+
pip install -e .
|
|
195
|
+
|
|
196
|
+
# 运行测试
|
|
197
|
+
pytest app/tests/ -v
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
## 许可证
|
|
201
|
+
|
|
202
|
+
MIT License
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# loop-agent-cli
|
|
2
|
+
|
|
3
|
+
> Typer + Rich CLI for [recruit-loop-agent](https://github.com/Chandler-Song/recruit-loop-agent) — 在终端中完成招聘循环的全部操作,无需启动 FastAPI Web 服务。
|
|
4
|
+
|
|
5
|
+
## 功能特性
|
|
6
|
+
|
|
7
|
+
- **零服务依赖**:不启动 uvicorn,直接在进程内调用核心引擎
|
|
8
|
+
- **完整功能覆盖**:职位管理、候选人查看、管道操作、循环执行、调度控制、仪表盘
|
|
9
|
+
- **终端友好**:Rich 彩色表格/面板,结构化输出
|
|
10
|
+
|
|
11
|
+
## 安装
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
# 从 PyPI 安装(正式发布后)
|
|
15
|
+
pip install loop-agent-cli
|
|
16
|
+
|
|
17
|
+
# 开发模式安装(从项目根目录)
|
|
18
|
+
cd recruit-loop-agent
|
|
19
|
+
pip install -e .
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## 前置条件
|
|
23
|
+
|
|
24
|
+
- Python >= 3.11
|
|
25
|
+
- `.env` 文件需存在于项目根目录(配置 `GITHUB_TOKEN`、`SMTP_*` 等)
|
|
26
|
+
- 项目根目录需包含 `app/` 核心引擎包
|
|
27
|
+
|
|
28
|
+
## 使用示例
|
|
29
|
+
|
|
30
|
+
### 查看帮助
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
loop-agent --help
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### 职位管理
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
# 列出所有职位
|
|
40
|
+
loop-agent position list
|
|
41
|
+
|
|
42
|
+
# 创建新职位
|
|
43
|
+
loop-agent position create -t "Senior Backend Engineer" -c "Tech Corp" \
|
|
44
|
+
-s "Python,FastAPI,PostgreSQL" -k "backend,python,remote"
|
|
45
|
+
|
|
46
|
+
# 查看职位详情
|
|
47
|
+
loop-agent position show <position_id>
|
|
48
|
+
|
|
49
|
+
# 关闭职位
|
|
50
|
+
loop-agent position close <position_id>
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### 执行招聘循环
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
# 对已有职位执行一次循环
|
|
57
|
+
loop-agent run position <position_id>
|
|
58
|
+
|
|
59
|
+
# 创建职位并立即执行循环
|
|
60
|
+
loop-agent run create-and-run -t "Python Developer" -c "Startup Inc" \
|
|
61
|
+
-s "Python,Django" -i 30
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### 候选人管理
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
# 列出候选人(默认 20 条)
|
|
68
|
+
loop-agent candidate list -n 10
|
|
69
|
+
|
|
70
|
+
# 查看候选人详情
|
|
71
|
+
loop-agent candidate show <candidate_id>
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### 管道管理
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
# 列出所有管道
|
|
78
|
+
loop-agent pipeline list
|
|
79
|
+
|
|
80
|
+
# 按职位过滤
|
|
81
|
+
loop-agent pipeline list -p <position_id>
|
|
82
|
+
|
|
83
|
+
# 更新管道状态
|
|
84
|
+
loop-agent pipeline update-status <pipeline_id> contacted
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### 调度管理
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
# 启动后台调度器(Ctrl+C 退出)
|
|
91
|
+
loop-agent schedule start
|
|
92
|
+
|
|
93
|
+
# 列出调度任务
|
|
94
|
+
loop-agent schedule list
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### 仪表盘
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
# 查看仪表盘摘要
|
|
101
|
+
loop-agent dashboard
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### 其他
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
# 显示 LangGraph 图结构
|
|
108
|
+
loop-agent graph
|
|
109
|
+
|
|
110
|
+
# 显示版本信息
|
|
111
|
+
loop-agent version
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### 全局选项
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
# 覆盖数据库路径
|
|
118
|
+
loop-agent --db sqlite+aiosqlite:///./custom.db position list
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## 命令树
|
|
122
|
+
|
|
123
|
+
```
|
|
124
|
+
loop-agent
|
|
125
|
+
├── run 执行招聘循环
|
|
126
|
+
│ ├── position <position_id> 对已有职位执行一次循环
|
|
127
|
+
│ └── create-and-run 创建职位并立即执行循环
|
|
128
|
+
├── position 职位管理
|
|
129
|
+
│ ├── list 列出所有职位
|
|
130
|
+
│ ├── create 创建新职位
|
|
131
|
+
│ ├── show <position_id> 查看职位详情
|
|
132
|
+
│ └── close <position_id> 关闭职位
|
|
133
|
+
├── candidate 候选人管理
|
|
134
|
+
│ ├── list 列出候选人
|
|
135
|
+
│ └── show <candidate_id> 查看候选人详情
|
|
136
|
+
├── pipeline 招聘管道管理
|
|
137
|
+
│ ├── list 列出管道
|
|
138
|
+
│ └── update-status 更新管道状态
|
|
139
|
+
├── schedule 调度管理
|
|
140
|
+
│ ├── start 启动后台调度器
|
|
141
|
+
│ └── list 列出调度任务
|
|
142
|
+
├── dashboard 查看仪表盘摘要
|
|
143
|
+
├── graph 显示 LangGraph 图结构
|
|
144
|
+
└── version 显示版本信息
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## 开发
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
# 克隆项目
|
|
151
|
+
git clone git@github.com:Chandler-Song/recruit-loop-agent.git
|
|
152
|
+
cd recruit-loop-agent
|
|
153
|
+
|
|
154
|
+
# 创建虚拟环境
|
|
155
|
+
python -m venv venv
|
|
156
|
+
source venv/bin/activate
|
|
157
|
+
|
|
158
|
+
# 安装依赖
|
|
159
|
+
pip install -e .
|
|
160
|
+
|
|
161
|
+
# 运行测试
|
|
162
|
+
pytest app/tests/ -v
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## 许可证
|
|
166
|
+
|
|
167
|
+
MIT License
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from langgraph.graph import StateGraph
|
|
2
|
+
from app.agents.state import RecruitingState
|
|
3
|
+
from app.agents.nodes import search_node, dedup_node, score_node, pipeline_node, outreach_node, evaluate_node
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def create_recruiting_graph():
|
|
7
|
+
"""
|
|
8
|
+
Create the recruiting loop agent graph
|
|
9
|
+
"""
|
|
10
|
+
workflow = StateGraph(RecruitingState)
|
|
11
|
+
|
|
12
|
+
# Add nodes to the graph
|
|
13
|
+
workflow.add_node("search", search_node)
|
|
14
|
+
workflow.add_node("dedup", dedup_node)
|
|
15
|
+
workflow.add_node("score", score_node)
|
|
16
|
+
workflow.add_node("pipeline", pipeline_node)
|
|
17
|
+
workflow.add_node("outreach", outreach_node)
|
|
18
|
+
workflow.add_node("evaluate", evaluate_node)
|
|
19
|
+
|
|
20
|
+
# Define the flow
|
|
21
|
+
workflow.set_entry_point("search")
|
|
22
|
+
workflow.add_edge("search", "dedup")
|
|
23
|
+
workflow.add_edge("dedup", "score")
|
|
24
|
+
workflow.add_edge("score", "pipeline")
|
|
25
|
+
workflow.add_edge("pipeline", "outreach")
|
|
26
|
+
workflow.add_edge("outreach", "evaluate")
|
|
27
|
+
|
|
28
|
+
# The evaluate node decides whether to finish or loop back
|
|
29
|
+
# For now, we'll have it finish, but in a real implementation
|
|
30
|
+
# it might loop back to search based on conditions
|
|
31
|
+
workflow.add_conditional_edges(
|
|
32
|
+
"evaluate",
|
|
33
|
+
lambda x: "continue" if x.get("continue_loop", False) else "finish",
|
|
34
|
+
{
|
|
35
|
+
"continue": "search", # Would loop back to search in a real implementation
|
|
36
|
+
"finish": "__end__"
|
|
37
|
+
}
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
return workflow.compile()
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
from typing import Dict, Any, List
|
|
2
|
+
from app.agents.state import RecruitingState
|
|
3
|
+
from app.services.search import SearchService
|
|
4
|
+
from app.services.score import ScoreService
|
|
5
|
+
from app.services.pipeline import PipelineService
|
|
6
|
+
from app.services.email import EmailService
|
|
7
|
+
from app.repositories.candidate import CandidateRepository
|
|
8
|
+
from app.repositories.position import PositionRepository
|
|
9
|
+
from app.repositories.pipeline import PipelineRepository
|
|
10
|
+
from app.repositories.outreach_log import OutreachLogRepository
|
|
11
|
+
from app.repositories.agent_run import AgentRunRepository
|
|
12
|
+
from app.repositories.node_log import NodeLogRepository
|
|
13
|
+
from app.models.candidate import Candidate
|
|
14
|
+
from app.models.position import Position
|
|
15
|
+
from app.models.pipeline import Pipeline
|
|
16
|
+
import asyncio
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
async def search_node(state: RecruitingState) -> Dict[str, Any]:
|
|
20
|
+
"""
|
|
21
|
+
Search node: Perform candidate search based on position requirements
|
|
22
|
+
"""
|
|
23
|
+
try:
|
|
24
|
+
# This would integrate with the SearchService
|
|
25
|
+
position = state["position"]
|
|
26
|
+
|
|
27
|
+
# Generate search keywords based on position requirements
|
|
28
|
+
keywords = [position.title] + (position.required_skills or []) + (position.search_keywords or [])
|
|
29
|
+
|
|
30
|
+
# Simulate search (in real implementation, this would call SearchService)
|
|
31
|
+
# For now, return empty results as the actual search happens in RunnerService
|
|
32
|
+
search_results = {
|
|
33
|
+
"keywords": keywords,
|
|
34
|
+
"candidates": state.get("candidates", []),
|
|
35
|
+
"found_count": len(state.get("candidates", []))
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
# Update metrics
|
|
39
|
+
metrics = state.get("metrics", {})
|
|
40
|
+
metrics["search_count"] = metrics.get("search_count", 0) + 1
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
**state,
|
|
44
|
+
"keywords": keywords,
|
|
45
|
+
"candidates": state.get("candidates", []),
|
|
46
|
+
"metrics": metrics
|
|
47
|
+
}
|
|
48
|
+
except Exception as e:
|
|
49
|
+
errors = state.get("errors", [])
|
|
50
|
+
errors.append(f"Search node error: {str(e)}")
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
**state,
|
|
54
|
+
"errors": errors
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
async def dedup_node(state: RecruitingState) -> Dict[str, Any]:
|
|
59
|
+
"""
|
|
60
|
+
Deduplication node: Remove duplicate candidates
|
|
61
|
+
"""
|
|
62
|
+
try:
|
|
63
|
+
candidates = state.get("candidates", [])
|
|
64
|
+
|
|
65
|
+
# In a real implementation, this would use CandidateService's deduplication methods
|
|
66
|
+
# For now, simulate deduplication by keeping unique candidates based on source_id
|
|
67
|
+
seen_ids = set()
|
|
68
|
+
unique_candidates = []
|
|
69
|
+
|
|
70
|
+
for candidate in candidates:
|
|
71
|
+
# Extract source and source_id from candidate data
|
|
72
|
+
source_id = candidate.get("source_id") or (candidate.get("id") if hasattr(candidate, "id") else None)
|
|
73
|
+
if source_id not in seen_ids:
|
|
74
|
+
seen_ids.add(source_id)
|
|
75
|
+
unique_candidates.append(candidate)
|
|
76
|
+
|
|
77
|
+
dedup_result = {
|
|
78
|
+
"original_count": len(candidates),
|
|
79
|
+
"unique_count": len(unique_candidates),
|
|
80
|
+
"duplicates_removed": len(candidates) - len(unique_candidates)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
# Update metrics
|
|
84
|
+
metrics = state.get("metrics", {})
|
|
85
|
+
metrics["candidates_deduped"] = len(unique_candidates)
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
**state,
|
|
89
|
+
"dedup_result": [unique_candidates],
|
|
90
|
+
"candidates": unique_candidates,
|
|
91
|
+
"metrics": metrics
|
|
92
|
+
}
|
|
93
|
+
except Exception as e:
|
|
94
|
+
errors = state.get("errors", [])
|
|
95
|
+
errors.append(f"Dedup node error: {str(e)}")
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
**state,
|
|
99
|
+
"errors": errors
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
async def score_node(state: RecruitingState) -> Dict[str, Any]:
|
|
104
|
+
"""
|
|
105
|
+
Scoring node: Score candidates based on position requirements
|
|
106
|
+
"""
|
|
107
|
+
try:
|
|
108
|
+
candidates = state.get("candidates", [])
|
|
109
|
+
position = state["position"]
|
|
110
|
+
|
|
111
|
+
# In a real implementation, this would use ScoreService
|
|
112
|
+
# For now, simulate scoring
|
|
113
|
+
scored_candidates = []
|
|
114
|
+
for candidate in candidates:
|
|
115
|
+
# Just add a dummy score for now - in real implementation, use ScoreService
|
|
116
|
+
candidate_with_score = {**candidate, "score": 75.0} # Default score
|
|
117
|
+
scored_candidates.append(candidate_with_score)
|
|
118
|
+
|
|
119
|
+
# Update metrics
|
|
120
|
+
metrics = state.get("metrics", {})
|
|
121
|
+
metrics["candidates_scored"] = len(scored_candidates)
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
**state,
|
|
125
|
+
"candidates": scored_candidates,
|
|
126
|
+
"metrics": metrics
|
|
127
|
+
}
|
|
128
|
+
except Exception as e:
|
|
129
|
+
errors = state.get("errors", [])
|
|
130
|
+
errors.append(f"Score node error: {str(e)}")
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
**state,
|
|
134
|
+
"errors": errors
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
async def pipeline_node(state: RecruitingState) -> Dict[str, Any]:
|
|
139
|
+
"""
|
|
140
|
+
Pipeline node: Update pipeline with scored candidates
|
|
141
|
+
"""
|
|
142
|
+
try:
|
|
143
|
+
candidates = state.get("candidates", [])
|
|
144
|
+
position = state["position"]
|
|
145
|
+
|
|
146
|
+
# In a real implementation, this would use PipelineService
|
|
147
|
+
# For now, simulate pipeline updates
|
|
148
|
+
pipeline_updates = []
|
|
149
|
+
for candidate in candidates:
|
|
150
|
+
# Create a simulated pipeline update
|
|
151
|
+
pipeline_update = {
|
|
152
|
+
"candidate_id": candidate.get("id", "unknown"),
|
|
153
|
+
"position_id": position.id,
|
|
154
|
+
"status": "discovered",
|
|
155
|
+
"score": candidate.get("score", 0)
|
|
156
|
+
}
|
|
157
|
+
pipeline_updates.append(pipeline_update)
|
|
158
|
+
|
|
159
|
+
# Update metrics
|
|
160
|
+
metrics = state.get("metrics", {})
|
|
161
|
+
metrics["pipeline_updates"] = len(pipeline_updates)
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
**state,
|
|
165
|
+
"pipeline_updates": pipeline_updates,
|
|
166
|
+
"metrics": metrics
|
|
167
|
+
}
|
|
168
|
+
except Exception as e:
|
|
169
|
+
errors = state.get("errors", [])
|
|
170
|
+
errors.append(f"Pipeline node error: {str(e)}")
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
**state,
|
|
174
|
+
"errors": errors
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
async def outreach_node(state: RecruitingState) -> Dict[str, Any]:
|
|
179
|
+
"""
|
|
180
|
+
Outreach node: Send communications to candidates
|
|
181
|
+
"""
|
|
182
|
+
try:
|
|
183
|
+
candidates = state.get("candidates", [])
|
|
184
|
+
position = state["position"]
|
|
185
|
+
|
|
186
|
+
# In a real implementation, this would use EmailService
|
|
187
|
+
# For now, simulate outreach
|
|
188
|
+
outreach_results = []
|
|
189
|
+
for candidate in candidates[:5]: # Only outreach to first 5 candidates
|
|
190
|
+
# Simulate email sending
|
|
191
|
+
outreach_result = {
|
|
192
|
+
"candidate_id": candidate.get("id", "unknown"),
|
|
193
|
+
"status": "sent", # or "failed"
|
|
194
|
+
"type": "email"
|
|
195
|
+
}
|
|
196
|
+
outreach_results.append(outreach_result)
|
|
197
|
+
|
|
198
|
+
# Update metrics
|
|
199
|
+
metrics = state.get("metrics", {})
|
|
200
|
+
metrics["outreach_attempts"] = len(outreach_results)
|
|
201
|
+
metrics["emails_sent"] = len(outreach_results) # Assuming all attempts were successful
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
**state,
|
|
205
|
+
"metrics": metrics
|
|
206
|
+
}
|
|
207
|
+
except Exception as e:
|
|
208
|
+
errors = state.get("errors", [])
|
|
209
|
+
errors.append(f"Outreach node error: {str(e)}")
|
|
210
|
+
|
|
211
|
+
return {
|
|
212
|
+
**state,
|
|
213
|
+
"errors": errors
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
async def evaluate_node(state: RecruitingState) -> Dict[str, Any]:
|
|
218
|
+
"""
|
|
219
|
+
Evaluation node: Decide whether to continue the loop
|
|
220
|
+
"""
|
|
221
|
+
try:
|
|
222
|
+
position = state["position"]
|
|
223
|
+
|
|
224
|
+
# Determine if the position is still open
|
|
225
|
+
# In a real implementation, this would check the position status
|
|
226
|
+
continue_loop = position.status == "active"
|
|
227
|
+
|
|
228
|
+
# Update metrics
|
|
229
|
+
metrics = state.get("metrics", {})
|
|
230
|
+
metrics["evaluation_completed"] = True
|
|
231
|
+
|
|
232
|
+
return {
|
|
233
|
+
**state,
|
|
234
|
+
"continue_loop": continue_loop,
|
|
235
|
+
"metrics": metrics
|
|
236
|
+
}
|
|
237
|
+
except Exception as e:
|
|
238
|
+
errors = state.get("errors", [])
|
|
239
|
+
errors.append(f"Evaluate node error: {str(e)}")
|
|
240
|
+
|
|
241
|
+
return {
|
|
242
|
+
**state,
|
|
243
|
+
"errors": errors,
|
|
244
|
+
"continue_loop": False # On error, stop the loop
|
|
245
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from typing import TypedDict, List, Dict, Any, Optional
|
|
2
|
+
from app.models.candidate import Candidate
|
|
3
|
+
from app.models.position import Position
|
|
4
|
+
import uuid
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class RecruitingState(TypedDict):
|
|
8
|
+
"""
|
|
9
|
+
State for the recruiting loop agent
|
|
10
|
+
"""
|
|
11
|
+
position: Position
|
|
12
|
+
keywords: List[str]
|
|
13
|
+
candidates: List[Dict[str, Any]]
|
|
14
|
+
dedup_result: List[Dict[str, Any]]
|
|
15
|
+
pipeline_updates: List[Dict[str, Any]]
|
|
16
|
+
metrics: Dict[str, int]
|
|
17
|
+
errors: List[str]
|
|
18
|
+
continue_loop: bool
|
|
19
|
+
run_id: Optional[uuid.UUID]
|
|
File without changes
|