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.
Files changed (90) hide show
  1. loop_agent_cli-0.1.0/MANIFEST.in +13 -0
  2. loop_agent_cli-0.1.0/PKG-INFO +202 -0
  3. loop_agent_cli-0.1.0/README.md +167 -0
  4. loop_agent_cli-0.1.0/app/__init__.py +0 -0
  5. loop_agent_cli-0.1.0/app/agents/__init__.py +0 -0
  6. loop_agent_cli-0.1.0/app/agents/graph.py +40 -0
  7. loop_agent_cli-0.1.0/app/agents/nodes.py +245 -0
  8. loop_agent_cli-0.1.0/app/agents/state.py +19 -0
  9. loop_agent_cli-0.1.0/app/api/__init__.py +0 -0
  10. loop_agent_cli-0.1.0/app/api/candidates.py +49 -0
  11. loop_agent_cli-0.1.0/app/api/dashboard.py +7 -0
  12. loop_agent_cli-0.1.0/app/api/outreach.py +7 -0
  13. loop_agent_cli-0.1.0/app/api/pipelines.py +58 -0
  14. loop_agent_cli-0.1.0/app/api/positions.py +76 -0
  15. loop_agent_cli-0.1.0/app/api/router.py +14 -0
  16. loop_agent_cli-0.1.0/app/api/scheduler.py +7 -0
  17. loop_agent_cli-0.1.0/app/api/skills.py +7 -0
  18. loop_agent_cli-0.1.0/app/api/system.py +7 -0
  19. loop_agent_cli-0.1.0/app/core/__init__.py +0 -0
  20. loop_agent_cli-0.1.0/app/core/config.py +18 -0
  21. loop_agent_cli-0.1.0/app/core/exception_handler.py +91 -0
  22. loop_agent_cli-0.1.0/app/core/exceptions.py +33 -0
  23. loop_agent_cli-0.1.0/app/core/logging.py +19 -0
  24. loop_agent_cli-0.1.0/app/database/__init__.py +0 -0
  25. loop_agent_cli-0.1.0/app/database/base.py +4 -0
  26. loop_agent_cli-0.1.0/app/database/session.py +20 -0
  27. loop_agent_cli-0.1.0/app/main.py +72 -0
  28. loop_agent_cli-0.1.0/app/models/__init__.py +0 -0
  29. loop_agent_cli-0.1.0/app/models/agent_run.py +18 -0
  30. loop_agent_cli-0.1.0/app/models/candidate.py +28 -0
  31. loop_agent_cli-0.1.0/app/models/node_log.py +18 -0
  32. loop_agent_cli-0.1.0/app/models/outreach_log.py +16 -0
  33. loop_agent_cli-0.1.0/app/models/pipeline.py +21 -0
  34. loop_agent_cli-0.1.0/app/models/position.py +22 -0
  35. loop_agent_cli-0.1.0/app/models/scheduler_job.py +16 -0
  36. loop_agent_cli-0.1.0/app/models/skill.py +13 -0
  37. loop_agent_cli-0.1.0/app/models/system_config.py +12 -0
  38. loop_agent_cli-0.1.0/app/repositories/__init__.py +0 -0
  39. loop_agent_cli-0.1.0/app/repositories/agent_run.py +74 -0
  40. loop_agent_cli-0.1.0/app/repositories/candidate.py +84 -0
  41. loop_agent_cli-0.1.0/app/repositories/node_log.py +57 -0
  42. loop_agent_cli-0.1.0/app/repositories/outreach_log.py +60 -0
  43. loop_agent_cli-0.1.0/app/repositories/pipeline.py +80 -0
  44. loop_agent_cli-0.1.0/app/repositories/position.py +67 -0
  45. loop_agent_cli-0.1.0/app/repositories/scheduler_job.py +74 -0
  46. loop_agent_cli-0.1.0/app/schemas/__init__.py +0 -0
  47. loop_agent_cli-0.1.0/app/schemas/agent_run.py +32 -0
  48. loop_agent_cli-0.1.0/app/schemas/candidate.py +58 -0
  49. loop_agent_cli-0.1.0/app/schemas/node_log.py +31 -0
  50. loop_agent_cli-0.1.0/app/schemas/outreach_log.py +28 -0
  51. loop_agent_cli-0.1.0/app/schemas/pipeline.py +34 -0
  52. loop_agent_cli-0.1.0/app/schemas/position.py +49 -0
  53. loop_agent_cli-0.1.0/app/schemas/scheduler_job.py +29 -0
  54. loop_agent_cli-0.1.0/app/services/__init__.py +0 -0
  55. loop_agent_cli-0.1.0/app/services/candidate.py +58 -0
  56. loop_agent_cli-0.1.0/app/services/dashboard.py +230 -0
  57. loop_agent_cli-0.1.0/app/services/email.py +116 -0
  58. loop_agent_cli-0.1.0/app/services/health.py +105 -0
  59. loop_agent_cli-0.1.0/app/services/pipeline.py +75 -0
  60. loop_agent_cli-0.1.0/app/services/position.py +36 -0
  61. loop_agent_cli-0.1.0/app/services/runner.py +292 -0
  62. loop_agent_cli-0.1.0/app/services/scheduler.py +174 -0
  63. loop_agent_cli-0.1.0/app/services/score.py +155 -0
  64. loop_agent_cli-0.1.0/app/services/search.py +92 -0
  65. loop_agent_cli-0.1.0/app/skills/base.py +30 -0
  66. loop_agent_cli-0.1.0/app/skills/github.py +106 -0
  67. loop_agent_cli-0.1.0/app/skills/registry.py +51 -0
  68. loop_agent_cli-0.1.0/app/tests/__init__.py +3 -0
  69. loop_agent_cli-0.1.0/app/tests/conftest.py +96 -0
  70. loop_agent_cli-0.1.0/app/tests/generate_report.py +144 -0
  71. loop_agent_cli-0.1.0/app/tests/test_candidates.py +158 -0
  72. loop_agent_cli-0.1.0/app/tests/test_dashboard.py +27 -0
  73. loop_agent_cli-0.1.0/app/tests/test_outreach.py +15 -0
  74. loop_agent_cli-0.1.0/app/tests/test_pipelines.py +249 -0
  75. loop_agent_cli-0.1.0/app/tests/test_positions.py +183 -0
  76. loop_agent_cli-0.1.0/app/tests/test_scheduler.py +15 -0
  77. loop_agent_cli-0.1.0/app/tests/test_skills.py +15 -0
  78. loop_agent_cli-0.1.0/app/tests/test_system.py +35 -0
  79. loop_agent_cli-0.1.0/app/utils/__init__.py +0 -0
  80. loop_agent_cli-0.1.0/loop_agent_cli/__init__.py +5 -0
  81. loop_agent_cli-0.1.0/loop_agent_cli/cli.py +728 -0
  82. loop_agent_cli-0.1.0/loop_agent_cli/container.py +191 -0
  83. loop_agent_cli-0.1.0/loop_agent_cli.egg-info/PKG-INFO +202 -0
  84. loop_agent_cli-0.1.0/loop_agent_cli.egg-info/SOURCES.txt +88 -0
  85. loop_agent_cli-0.1.0/loop_agent_cli.egg-info/dependency_links.txt +1 -0
  86. loop_agent_cli-0.1.0/loop_agent_cli.egg-info/entry_points.txt +2 -0
  87. loop_agent_cli-0.1.0/loop_agent_cli.egg-info/requires.txt +12 -0
  88. loop_agent_cli-0.1.0/loop_agent_cli.egg-info/top_level.txt +2 -0
  89. loop_agent_cli-0.1.0/pyproject.toml +57 -0
  90. 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