loop-agent-cli 0.1.0__py3-none-any.whl
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.
- app/__init__.py +0 -0
- app/agents/__init__.py +0 -0
- app/agents/graph.py +40 -0
- app/agents/nodes.py +245 -0
- app/agents/state.py +19 -0
- app/api/__init__.py +0 -0
- app/api/candidates.py +49 -0
- app/api/dashboard.py +7 -0
- app/api/outreach.py +7 -0
- app/api/pipelines.py +58 -0
- app/api/positions.py +76 -0
- app/api/router.py +14 -0
- app/api/scheduler.py +7 -0
- app/api/skills.py +7 -0
- app/api/system.py +7 -0
- app/core/__init__.py +0 -0
- app/core/config.py +18 -0
- app/core/exception_handler.py +91 -0
- app/core/exceptions.py +33 -0
- app/core/logging.py +19 -0
- app/database/__init__.py +0 -0
- app/database/base.py +4 -0
- app/database/session.py +20 -0
- app/main.py +72 -0
- app/models/__init__.py +0 -0
- app/models/agent_run.py +18 -0
- app/models/candidate.py +28 -0
- app/models/node_log.py +18 -0
- app/models/outreach_log.py +16 -0
- app/models/pipeline.py +21 -0
- app/models/position.py +22 -0
- app/models/scheduler_job.py +16 -0
- app/models/skill.py +13 -0
- app/models/system_config.py +12 -0
- app/repositories/__init__.py +0 -0
- app/repositories/agent_run.py +74 -0
- app/repositories/candidate.py +84 -0
- app/repositories/node_log.py +57 -0
- app/repositories/outreach_log.py +60 -0
- app/repositories/pipeline.py +80 -0
- app/repositories/position.py +67 -0
- app/repositories/scheduler_job.py +74 -0
- app/schemas/__init__.py +0 -0
- app/schemas/agent_run.py +32 -0
- app/schemas/candidate.py +58 -0
- app/schemas/node_log.py +31 -0
- app/schemas/outreach_log.py +28 -0
- app/schemas/pipeline.py +34 -0
- app/schemas/position.py +49 -0
- app/schemas/scheduler_job.py +29 -0
- app/services/__init__.py +0 -0
- app/services/candidate.py +58 -0
- app/services/dashboard.py +230 -0
- app/services/email.py +116 -0
- app/services/health.py +105 -0
- app/services/pipeline.py +75 -0
- app/services/position.py +36 -0
- app/services/runner.py +292 -0
- app/services/scheduler.py +174 -0
- app/services/score.py +155 -0
- app/services/search.py +92 -0
- app/skills/base.py +30 -0
- app/skills/github.py +106 -0
- app/skills/registry.py +51 -0
- app/tests/__init__.py +3 -0
- app/tests/conftest.py +96 -0
- app/tests/generate_report.py +144 -0
- app/tests/test_candidates.py +158 -0
- app/tests/test_dashboard.py +27 -0
- app/tests/test_outreach.py +15 -0
- app/tests/test_pipelines.py +249 -0
- app/tests/test_positions.py +183 -0
- app/tests/test_scheduler.py +15 -0
- app/tests/test_skills.py +15 -0
- app/tests/test_system.py +35 -0
- app/utils/__init__.py +0 -0
- loop_agent_cli/__init__.py +5 -0
- loop_agent_cli/cli.py +728 -0
- loop_agent_cli/container.py +191 -0
- loop_agent_cli-0.1.0.dist-info/METADATA +202 -0
- loop_agent_cli-0.1.0.dist-info/RECORD +84 -0
- loop_agent_cli-0.1.0.dist-info/WHEEL +5 -0
- loop_agent_cli-0.1.0.dist-info/entry_points.txt +2 -0
- loop_agent_cli-0.1.0.dist-info/top_level.txt +2 -0
app/__init__.py
ADDED
|
File without changes
|
app/agents/__init__.py
ADDED
|
File without changes
|
app/agents/graph.py
ADDED
|
@@ -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()
|
app/agents/nodes.py
ADDED
|
@@ -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
|
+
}
|
app/agents/state.py
ADDED
|
@@ -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]
|
app/api/__init__.py
ADDED
|
File without changes
|
app/api/candidates.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
from fastapi import APIRouter, Depends, HTTPException
|
|
2
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
3
|
+
from app.database.session import get_db
|
|
4
|
+
from app.services.candidate import CandidateService
|
|
5
|
+
from app.repositories.candidate import CandidateRepository
|
|
6
|
+
from app.schemas.candidate import CandidateCreate, CandidateUpdate, Candidate
|
|
7
|
+
from typing import List
|
|
8
|
+
import uuid
|
|
9
|
+
|
|
10
|
+
router = APIRouter()
|
|
11
|
+
|
|
12
|
+
@router.post("", response_model=Candidate)
|
|
13
|
+
async def create_candidate(candidate: CandidateCreate, db: AsyncSession = Depends(get_db)):
|
|
14
|
+
repo = CandidateRepository(db)
|
|
15
|
+
service = CandidateService(repo)
|
|
16
|
+
return await service.create_candidate(candidate)
|
|
17
|
+
|
|
18
|
+
@router.get("", response_model=List[Candidate])
|
|
19
|
+
async def get_candidates(skip: int = 0, limit: int = 100, keyword: str = None, db: AsyncSession = Depends(get_db)):
|
|
20
|
+
repo = CandidateRepository(db)
|
|
21
|
+
service = CandidateService(repo)
|
|
22
|
+
return await service.get_all_candidates(skip=skip, limit=limit, keyword=keyword)
|
|
23
|
+
|
|
24
|
+
@router.get("/{candidate_id}", response_model=Candidate)
|
|
25
|
+
async def get_candidate(candidate_id: uuid.UUID, db: AsyncSession = Depends(get_db)):
|
|
26
|
+
repo = CandidateRepository(db)
|
|
27
|
+
service = CandidateService(repo)
|
|
28
|
+
try:
|
|
29
|
+
return await service.get_candidate_by_id(candidate_id)
|
|
30
|
+
except Exception as e:
|
|
31
|
+
raise HTTPException(status_code=404, detail=str(e))
|
|
32
|
+
|
|
33
|
+
@router.put("/{candidate_id}", response_model=Candidate)
|
|
34
|
+
async def update_candidate(candidate_id: uuid.UUID, candidate_update: CandidateUpdate, db: AsyncSession = Depends(get_db)):
|
|
35
|
+
repo = CandidateRepository(db)
|
|
36
|
+
service = CandidateService(repo)
|
|
37
|
+
updated_candidate = await service.update_candidate(candidate_id, candidate_update)
|
|
38
|
+
if not updated_candidate:
|
|
39
|
+
raise HTTPException(status_code=404, detail="Candidate not found")
|
|
40
|
+
return updated_candidate
|
|
41
|
+
|
|
42
|
+
@router.delete("/{candidate_id}")
|
|
43
|
+
async def delete_candidate(candidate_id: uuid.UUID, db: AsyncSession = Depends(get_db)):
|
|
44
|
+
repo = CandidateRepository(db)
|
|
45
|
+
service = CandidateService(repo)
|
|
46
|
+
success = await service.delete_candidate(candidate_id)
|
|
47
|
+
if not success:
|
|
48
|
+
raise HTTPException(status_code=404, detail="Candidate not found")
|
|
49
|
+
return {"message": "Candidate deleted successfully"}
|
app/api/dashboard.py
ADDED
app/api/outreach.py
ADDED
app/api/pipelines.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
from fastapi import APIRouter, Depends, HTTPException
|
|
2
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
3
|
+
from app.database.session import get_db
|
|
4
|
+
from app.services.pipeline import PipelineService
|
|
5
|
+
from app.repositories.pipeline import PipelineRepository
|
|
6
|
+
from app.schemas.pipeline import PipelineCreate, PipelineUpdate, Pipeline
|
|
7
|
+
from typing import List
|
|
8
|
+
import uuid
|
|
9
|
+
|
|
10
|
+
router = APIRouter()
|
|
11
|
+
|
|
12
|
+
@router.post("", response_model=Pipeline)
|
|
13
|
+
async def create_pipeline(pipeline: PipelineCreate, db: AsyncSession = Depends(get_db)):
|
|
14
|
+
repo = PipelineRepository(db)
|
|
15
|
+
service = PipelineService(repo)
|
|
16
|
+
return await service.create_pipeline(pipeline)
|
|
17
|
+
|
|
18
|
+
@router.get("", response_model=List[Pipeline])
|
|
19
|
+
async def get_pipelines(skip: int = 0, limit: int = 100, db: AsyncSession = Depends(get_db)):
|
|
20
|
+
repo = PipelineRepository(db)
|
|
21
|
+
service = PipelineService(repo)
|
|
22
|
+
return await service.get_all_pipelines(skip=skip, limit=limit)
|
|
23
|
+
|
|
24
|
+
@router.get("/{pipeline_id}", response_model=Pipeline)
|
|
25
|
+
async def get_pipeline_by_id(pipeline_id: uuid.UUID, db: AsyncSession = Depends(get_db)):
|
|
26
|
+
repo = PipelineRepository(db)
|
|
27
|
+
service = PipelineService(repo)
|
|
28
|
+
try:
|
|
29
|
+
return await service.get_pipeline_by_id(pipeline_id)
|
|
30
|
+
except Exception as e:
|
|
31
|
+
raise HTTPException(status_code=404, detail=str(e))
|
|
32
|
+
|
|
33
|
+
@router.put("/{pipeline_id}", response_model=Pipeline)
|
|
34
|
+
async def update_pipeline(pipeline_id: uuid.UUID, pipeline_update: PipelineUpdate, db: AsyncSession = Depends(get_db)):
|
|
35
|
+
repo = PipelineRepository(db)
|
|
36
|
+
service = PipelineService(repo)
|
|
37
|
+
updated_pipeline = await service.update_pipeline(pipeline_id, pipeline_update)
|
|
38
|
+
if not updated_pipeline:
|
|
39
|
+
raise HTTPException(status_code=404, detail="Pipeline not found")
|
|
40
|
+
return updated_pipeline
|
|
41
|
+
|
|
42
|
+
@router.delete("/{pipeline_id}")
|
|
43
|
+
async def delete_pipeline(pipeline_id: uuid.UUID, db: AsyncSession = Depends(get_db)):
|
|
44
|
+
repo = PipelineRepository(db)
|
|
45
|
+
service = PipelineService(repo)
|
|
46
|
+
success = await service.delete_pipeline(pipeline_id)
|
|
47
|
+
if not success:
|
|
48
|
+
raise HTTPException(status_code=404, detail="Pipeline not found")
|
|
49
|
+
return {"message": "Pipeline deleted successfully"}
|
|
50
|
+
|
|
51
|
+
@router.put("/{pipeline_id}/status", response_model=Pipeline)
|
|
52
|
+
async def update_pipeline_status(pipeline_id: uuid.UUID, status: str, db: AsyncSession = Depends(get_db)):
|
|
53
|
+
repo = PipelineRepository(db)
|
|
54
|
+
service = PipelineService(repo)
|
|
55
|
+
pipeline = await service.update_pipeline_status(pipeline_id, status)
|
|
56
|
+
if not pipeline:
|
|
57
|
+
raise HTTPException(status_code=404, detail="Pipeline not found")
|
|
58
|
+
return pipeline
|
app/api/positions.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
from fastapi import APIRouter, Depends, HTTPException
|
|
2
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
3
|
+
from app.database.session import get_db
|
|
4
|
+
from app.services.position import PositionService
|
|
5
|
+
from app.repositories.position import PositionRepository
|
|
6
|
+
from app.schemas.position import PositionCreate, PositionUpdate, Position
|
|
7
|
+
from typing import List
|
|
8
|
+
import uuid
|
|
9
|
+
|
|
10
|
+
router = APIRouter()
|
|
11
|
+
|
|
12
|
+
@router.post("", response_model=Position)
|
|
13
|
+
async def create_position(position: PositionCreate, db: AsyncSession = Depends(get_db)):
|
|
14
|
+
repo = PositionRepository(db)
|
|
15
|
+
service = PositionService(repo)
|
|
16
|
+
return await service.create_position(position)
|
|
17
|
+
|
|
18
|
+
@router.get("", response_model=List[Position])
|
|
19
|
+
async def get_positions(skip: int = 0, limit: int = 100, status: str = None, db: AsyncSession = Depends(get_db)):
|
|
20
|
+
repo = PositionRepository(db)
|
|
21
|
+
service = PositionService(repo)
|
|
22
|
+
return await service.get_all_positions(skip=skip, limit=limit, status=status)
|
|
23
|
+
|
|
24
|
+
@router.get("/{position_id}", response_model=Position)
|
|
25
|
+
async def get_position(position_id: uuid.UUID, db: AsyncSession = Depends(get_db)):
|
|
26
|
+
repo = PositionRepository(db)
|
|
27
|
+
service = PositionService(repo)
|
|
28
|
+
try:
|
|
29
|
+
return await service.get_position_by_id(position_id)
|
|
30
|
+
except Exception as e:
|
|
31
|
+
raise HTTPException(status_code=404, detail=str(e))
|
|
32
|
+
|
|
33
|
+
@router.put("/{position_id}", response_model=Position)
|
|
34
|
+
async def update_position(position_id: uuid.UUID, position_update: PositionUpdate, db: AsyncSession = Depends(get_db)):
|
|
35
|
+
repo = PositionRepository(db)
|
|
36
|
+
service = PositionService(repo)
|
|
37
|
+
updated_position = await service.update_position(position_id, position_update)
|
|
38
|
+
if not updated_position:
|
|
39
|
+
raise HTTPException(status_code=404, detail="Position not found")
|
|
40
|
+
return updated_position
|
|
41
|
+
|
|
42
|
+
@router.delete("/{position_id}")
|
|
43
|
+
async def delete_position(position_id: uuid.UUID, db: AsyncSession = Depends(get_db)):
|
|
44
|
+
repo = PositionRepository(db)
|
|
45
|
+
service = PositionService(repo)
|
|
46
|
+
success = await service.delete_position(position_id)
|
|
47
|
+
if not success:
|
|
48
|
+
raise HTTPException(status_code=404, detail="Position not found")
|
|
49
|
+
return {"message": "Position deleted successfully"}
|
|
50
|
+
|
|
51
|
+
@router.post("/{position_id}/pause", response_model=Position)
|
|
52
|
+
async def pause_position(position_id: uuid.UUID, db: AsyncSession = Depends(get_db)):
|
|
53
|
+
repo = PositionRepository(db)
|
|
54
|
+
service = PositionService(repo)
|
|
55
|
+
position = await service.pause_position(position_id)
|
|
56
|
+
if not position:
|
|
57
|
+
raise HTTPException(status_code=404, detail="Position not found")
|
|
58
|
+
return position
|
|
59
|
+
|
|
60
|
+
@router.post("/{position_id}/resume", response_model=Position)
|
|
61
|
+
async def resume_position(position_id: uuid.UUID, db: AsyncSession = Depends(get_db)):
|
|
62
|
+
repo = PositionRepository(db)
|
|
63
|
+
service = PositionService(repo)
|
|
64
|
+
position = await service.resume_position(position_id)
|
|
65
|
+
if not position:
|
|
66
|
+
raise HTTPException(status_code=404, detail="Position not found")
|
|
67
|
+
return position
|
|
68
|
+
|
|
69
|
+
@router.post("/{position_id}/close", response_model=Position)
|
|
70
|
+
async def close_position(position_id: uuid.UUID, db: AsyncSession = Depends(get_db)):
|
|
71
|
+
repo = PositionRepository(db)
|
|
72
|
+
service = PositionService(repo)
|
|
73
|
+
position = await service.close_position(position_id)
|
|
74
|
+
if not position:
|
|
75
|
+
raise HTTPException(status_code=404, detail="Position not found")
|
|
76
|
+
return position
|
app/api/router.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from fastapi import APIRouter
|
|
2
|
+
from app.api import dashboard, positions, candidates, pipelines, outreach, scheduler, skills, system
|
|
3
|
+
|
|
4
|
+
router = APIRouter()
|
|
5
|
+
|
|
6
|
+
# Include all API routers
|
|
7
|
+
router.include_router(dashboard.router, prefix="/dashboard", tags=["dashboard"])
|
|
8
|
+
router.include_router(positions.router, prefix="/positions", tags=["positions"])
|
|
9
|
+
router.include_router(candidates.router, prefix="/candidates", tags=["candidates"])
|
|
10
|
+
router.include_router(pipelines.router, prefix="/pipelines", tags=["pipelines"])
|
|
11
|
+
router.include_router(outreach.router, prefix="/outreach", tags=["outreach"])
|
|
12
|
+
router.include_router(scheduler.router, prefix="/scheduler", tags=["scheduler"])
|
|
13
|
+
router.include_router(skills.router, prefix="/skills", tags=["skills"])
|
|
14
|
+
router.include_router(system.router, prefix="/system", tags=["system"])
|
app/api/scheduler.py
ADDED
app/api/skills.py
ADDED
app/api/system.py
ADDED
app/core/__init__.py
ADDED
|
File without changes
|
app/core/config.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from pydantic_settings import BaseSettings
|
|
2
|
+
from typing import Optional
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
class Settings(BaseSettings):
|
|
6
|
+
database_url: str = "sqlite+aiosqlite:///./recruiting_agent.db"
|
|
7
|
+
debug: bool = False
|
|
8
|
+
github_token: Optional[str] = None
|
|
9
|
+
smtp_host: Optional[str] = None
|
|
10
|
+
smtp_user: Optional[str] = None
|
|
11
|
+
smtp_password: Optional[str] = None
|
|
12
|
+
smtp_port: int = 587
|
|
13
|
+
email_from: Optional[str] = None
|
|
14
|
+
|
|
15
|
+
class Config:
|
|
16
|
+
env_file = ".env"
|
|
17
|
+
|
|
18
|
+
settings = Settings()
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
from fastapi import Request, HTTPException
|
|
2
|
+
from fastapi.responses import JSONResponse
|
|
3
|
+
from app.core.exceptions import (
|
|
4
|
+
RecruitingAgentException,
|
|
5
|
+
PositionNotFoundException,
|
|
6
|
+
CandidateNotFoundException,
|
|
7
|
+
PipelineNotFoundException,
|
|
8
|
+
GitHubAPIException,
|
|
9
|
+
SMTPException,
|
|
10
|
+
DatabaseException
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
async def recruiting_agent_exception_handler(request: Request, exc: RecruitingAgentException):
|
|
14
|
+
return JSONResponse(
|
|
15
|
+
status_code=500,
|
|
16
|
+
content={
|
|
17
|
+
"success": False,
|
|
18
|
+
"message": str(exc),
|
|
19
|
+
"error_code": "INTERNAL_ERROR"
|
|
20
|
+
}
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
async def position_not_found_exception_handler(request: Request, exc: PositionNotFoundException):
|
|
24
|
+
return JSONResponse(
|
|
25
|
+
status_code=404,
|
|
26
|
+
content={
|
|
27
|
+
"success": False,
|
|
28
|
+
"message": "Position not found",
|
|
29
|
+
"error_code": "POSITION_NOT_FOUND"
|
|
30
|
+
}
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
async def candidate_not_found_exception_handler(request: Request, exc: CandidateNotFoundException):
|
|
34
|
+
return JSONResponse(
|
|
35
|
+
status_code=404,
|
|
36
|
+
content={
|
|
37
|
+
"success": False,
|
|
38
|
+
"message": "Candidate not found",
|
|
39
|
+
"error_code": "CANDIDATE_NOT_FOUND"
|
|
40
|
+
}
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
async def pipeline_not_found_exception_handler(request: Request, exc: PipelineNotFoundException):
|
|
44
|
+
return JSONResponse(
|
|
45
|
+
status_code=404,
|
|
46
|
+
content={
|
|
47
|
+
"success": False,
|
|
48
|
+
"message": "Pipeline not found",
|
|
49
|
+
"error_code": "PIPELINE_NOT_FOUND"
|
|
50
|
+
}
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
async def github_api_exception_handler(request: Request, exc: GitHubAPIException):
|
|
54
|
+
return JSONResponse(
|
|
55
|
+
status_code=500,
|
|
56
|
+
content={
|
|
57
|
+
"success": False,
|
|
58
|
+
"message": f"GitHub API Error: {str(exc)}",
|
|
59
|
+
"error_code": "GITHUB_API_ERROR"
|
|
60
|
+
}
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
async def smtp_exception_handler(request: Request, exc: SMTPException):
|
|
64
|
+
return JSONResponse(
|
|
65
|
+
status_code=500,
|
|
66
|
+
content={
|
|
67
|
+
"success": False,
|
|
68
|
+
"message": f"SMTP Error: {str(exc)}",
|
|
69
|
+
"error_code": "SMTP_ERROR"
|
|
70
|
+
}
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
async def database_exception_handler(request: Request, exc: DatabaseException):
|
|
74
|
+
return JSONResponse(
|
|
75
|
+
status_code=500,
|
|
76
|
+
content={
|
|
77
|
+
"success": False,
|
|
78
|
+
"message": f"Database Error: {str(exc)}",
|
|
79
|
+
"error_code": "DATABASE_ERROR"
|
|
80
|
+
}
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
async def http_exception_handler(request: Request, exc: HTTPException):
|
|
84
|
+
return JSONResponse(
|
|
85
|
+
status_code=exc.status_code,
|
|
86
|
+
content={
|
|
87
|
+
"success": False,
|
|
88
|
+
"message": exc.detail,
|
|
89
|
+
"error_code": "HTTP_ERROR"
|
|
90
|
+
}
|
|
91
|
+
)
|