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.
Files changed (84) hide show
  1. app/__init__.py +0 -0
  2. app/agents/__init__.py +0 -0
  3. app/agents/graph.py +40 -0
  4. app/agents/nodes.py +245 -0
  5. app/agents/state.py +19 -0
  6. app/api/__init__.py +0 -0
  7. app/api/candidates.py +49 -0
  8. app/api/dashboard.py +7 -0
  9. app/api/outreach.py +7 -0
  10. app/api/pipelines.py +58 -0
  11. app/api/positions.py +76 -0
  12. app/api/router.py +14 -0
  13. app/api/scheduler.py +7 -0
  14. app/api/skills.py +7 -0
  15. app/api/system.py +7 -0
  16. app/core/__init__.py +0 -0
  17. app/core/config.py +18 -0
  18. app/core/exception_handler.py +91 -0
  19. app/core/exceptions.py +33 -0
  20. app/core/logging.py +19 -0
  21. app/database/__init__.py +0 -0
  22. app/database/base.py +4 -0
  23. app/database/session.py +20 -0
  24. app/main.py +72 -0
  25. app/models/__init__.py +0 -0
  26. app/models/agent_run.py +18 -0
  27. app/models/candidate.py +28 -0
  28. app/models/node_log.py +18 -0
  29. app/models/outreach_log.py +16 -0
  30. app/models/pipeline.py +21 -0
  31. app/models/position.py +22 -0
  32. app/models/scheduler_job.py +16 -0
  33. app/models/skill.py +13 -0
  34. app/models/system_config.py +12 -0
  35. app/repositories/__init__.py +0 -0
  36. app/repositories/agent_run.py +74 -0
  37. app/repositories/candidate.py +84 -0
  38. app/repositories/node_log.py +57 -0
  39. app/repositories/outreach_log.py +60 -0
  40. app/repositories/pipeline.py +80 -0
  41. app/repositories/position.py +67 -0
  42. app/repositories/scheduler_job.py +74 -0
  43. app/schemas/__init__.py +0 -0
  44. app/schemas/agent_run.py +32 -0
  45. app/schemas/candidate.py +58 -0
  46. app/schemas/node_log.py +31 -0
  47. app/schemas/outreach_log.py +28 -0
  48. app/schemas/pipeline.py +34 -0
  49. app/schemas/position.py +49 -0
  50. app/schemas/scheduler_job.py +29 -0
  51. app/services/__init__.py +0 -0
  52. app/services/candidate.py +58 -0
  53. app/services/dashboard.py +230 -0
  54. app/services/email.py +116 -0
  55. app/services/health.py +105 -0
  56. app/services/pipeline.py +75 -0
  57. app/services/position.py +36 -0
  58. app/services/runner.py +292 -0
  59. app/services/scheduler.py +174 -0
  60. app/services/score.py +155 -0
  61. app/services/search.py +92 -0
  62. app/skills/base.py +30 -0
  63. app/skills/github.py +106 -0
  64. app/skills/registry.py +51 -0
  65. app/tests/__init__.py +3 -0
  66. app/tests/conftest.py +96 -0
  67. app/tests/generate_report.py +144 -0
  68. app/tests/test_candidates.py +158 -0
  69. app/tests/test_dashboard.py +27 -0
  70. app/tests/test_outreach.py +15 -0
  71. app/tests/test_pipelines.py +249 -0
  72. app/tests/test_positions.py +183 -0
  73. app/tests/test_scheduler.py +15 -0
  74. app/tests/test_skills.py +15 -0
  75. app/tests/test_system.py +35 -0
  76. app/utils/__init__.py +0 -0
  77. loop_agent_cli/__init__.py +5 -0
  78. loop_agent_cli/cli.py +728 -0
  79. loop_agent_cli/container.py +191 -0
  80. loop_agent_cli-0.1.0.dist-info/METADATA +202 -0
  81. loop_agent_cli-0.1.0.dist-info/RECORD +84 -0
  82. loop_agent_cli-0.1.0.dist-info/WHEEL +5 -0
  83. loop_agent_cli-0.1.0.dist-info/entry_points.txt +2 -0
  84. 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
@@ -0,0 +1,7 @@
1
+ from fastapi import APIRouter
2
+
3
+ router = APIRouter()
4
+
5
+ @router.get("/summary")
6
+ async def get_dashboard_summary():
7
+ return {"running_positions": 0, "today_loops": 0, "today_candidates": 0, "today_emails": 0, "today_replies": 0, "today_errors": 0}
app/api/outreach.py ADDED
@@ -0,0 +1,7 @@
1
+ from fastapi import APIRouter
2
+
3
+ router = APIRouter()
4
+
5
+ @router.get("/logs")
6
+ async def get_outreach_logs():
7
+ return []
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
@@ -0,0 +1,7 @@
1
+ from fastapi import APIRouter
2
+
3
+ router = APIRouter()
4
+
5
+ @router.get("/jobs")
6
+ async def get_scheduler_jobs():
7
+ return []
app/api/skills.py ADDED
@@ -0,0 +1,7 @@
1
+ from fastapi import APIRouter
2
+
3
+ router = APIRouter()
4
+
5
+ @router.get("")
6
+ async def get_skills():
7
+ return []
app/api/system.py ADDED
@@ -0,0 +1,7 @@
1
+ from fastapi import APIRouter
2
+
3
+ router = APIRouter()
4
+
5
+ @router.get("/config")
6
+ async def get_system_config():
7
+ return {}
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
+ )