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
@@ -0,0 +1,27 @@
1
+ """
2
+ Dashboard API tests
3
+ """
4
+ import pytest
5
+
6
+
7
+ @pytest.mark.asyncio
8
+ async def test_get_dashboard_summary(client):
9
+ """Test getting dashboard summary"""
10
+ response = await client.get("/api/v1/dashboard/summary")
11
+
12
+ assert response.status_code == 200
13
+ data = response.json()
14
+
15
+ assert "running_positions" in data
16
+ assert "today_loops" in data
17
+ assert "today_candidates" in data
18
+ assert "today_emails" in data
19
+ assert "today_replies" in data
20
+ assert "today_errors" in data
21
+
22
+ assert isinstance(data["running_positions"], int)
23
+ assert isinstance(data["today_loops"], int)
24
+ assert isinstance(data["today_candidates"], int)
25
+ assert isinstance(data["today_emails"], int)
26
+ assert isinstance(data["today_replies"], int)
27
+ assert isinstance(data["today_errors"], int)
@@ -0,0 +1,15 @@
1
+ """
2
+ Outreach API tests
3
+ """
4
+ import pytest
5
+
6
+
7
+ @pytest.mark.asyncio
8
+ async def test_get_outreach_logs(client):
9
+ """Test getting outreach logs"""
10
+ response = await client.get("/api/v1/outreach/logs")
11
+
12
+ assert response.status_code == 200
13
+ data = response.json()
14
+
15
+ assert isinstance(data, list)
@@ -0,0 +1,249 @@
1
+ """
2
+ Pipelines API tests
3
+ """
4
+ import pytest
5
+ import uuid
6
+
7
+
8
+ @pytest.mark.asyncio
9
+ async def test_create_pipeline(client, sample_position_data, sample_candidate_data):
10
+ """Test creating a new pipeline"""
11
+ # Create position and candidate first
12
+ pos_response = await client.post("/api/v1/positions", json=sample_position_data)
13
+ position_id = pos_response.json()["id"]
14
+
15
+ cand_response = await client.post("/api/v1/candidates", json=sample_candidate_data)
16
+ candidate_id = cand_response.json()["id"]
17
+
18
+ pipeline_data = {
19
+ "position_id": position_id,
20
+ "candidate_id": candidate_id,
21
+ "status": "discovered",
22
+ "score": 85.5,
23
+ "contact_count": 0,
24
+ "candidate_interest": "high",
25
+ "notes": "Strong candidate"
26
+ }
27
+
28
+ response = await client.post("/api/v1/pipelines", json=pipeline_data)
29
+
30
+ assert response.status_code == 200
31
+ data = response.json()
32
+
33
+ assert data["position_id"] == position_id
34
+ assert data["candidate_id"] == candidate_id
35
+ assert data["status"] == "discovered"
36
+ assert data["score"] == 85.5
37
+ assert "id" in data
38
+ assert "created_at" in data
39
+
40
+
41
+ @pytest.mark.asyncio
42
+ async def test_create_duplicate_pipeline(client, sample_position_data, sample_candidate_data):
43
+ """Test creating a duplicate pipeline returns existing one"""
44
+ pos_response = await client.post("/api/v1/positions", json=sample_position_data)
45
+ position_id = pos_response.json()["id"]
46
+
47
+ cand_response = await client.post("/api/v1/candidates", json=sample_candidate_data)
48
+ candidate_id = cand_response.json()["id"]
49
+
50
+ pipeline_data = {
51
+ "position_id": position_id,
52
+ "candidate_id": candidate_id,
53
+ "status": "discovered"
54
+ }
55
+
56
+ # Create first time
57
+ response1 = await client.post("/api/v1/pipelines", json=pipeline_data)
58
+ assert response1.status_code == 200
59
+ pipeline_id_1 = response1.json()["id"]
60
+
61
+ # Create duplicate
62
+ response2 = await client.post("/api/v1/pipelines", json=pipeline_data)
63
+ assert response2.status_code == 200
64
+ pipeline_id_2 = response2.json()["id"]
65
+
66
+ # Should return the same pipeline
67
+ assert pipeline_id_1 == pipeline_id_2
68
+
69
+
70
+ @pytest.mark.asyncio
71
+ async def test_get_pipelines(client, sample_position_data, sample_candidate_data):
72
+ """Test getting all pipelines"""
73
+ pos_response = await client.post("/api/v1/positions", json=sample_position_data)
74
+ position_id = pos_response.json()["id"]
75
+
76
+ cand_response = await client.post("/api/v1/candidates", json=sample_candidate_data)
77
+ candidate_id = cand_response.json()["id"]
78
+
79
+ pipeline_data = {
80
+ "position_id": position_id,
81
+ "candidate_id": candidate_id
82
+ }
83
+ await client.post("/api/v1/pipelines", json=pipeline_data)
84
+
85
+ response = await client.get("/api/v1/pipelines")
86
+
87
+ assert response.status_code == 200
88
+ data = response.json()
89
+
90
+ assert isinstance(data, list)
91
+ assert len(data) >= 1
92
+
93
+
94
+ @pytest.mark.asyncio
95
+ async def test_get_pipelines_with_pagination(client, sample_position_data, sample_candidate_data):
96
+ """Test getting pipelines with pagination"""
97
+ pos_response = await client.post("/api/v1/positions", json=sample_position_data)
98
+ position_id = pos_response.json()["id"]
99
+
100
+ # Create multiple candidates and pipelines
101
+ for i in range(3):
102
+ cand_data = sample_candidate_data.copy()
103
+ cand_data["source_id"] = str(20000 + i)
104
+ cand_data["github_login"] = f"pipeuser{i}"
105
+ cand_response = await client.post("/api/v1/candidates", json=cand_data)
106
+ candidate_id = cand_response.json()["id"]
107
+
108
+ pipeline_data = {
109
+ "position_id": position_id,
110
+ "candidate_id": candidate_id
111
+ }
112
+ await client.post("/api/v1/pipelines", json=pipeline_data)
113
+
114
+ response = await client.get("/api/v1/pipelines?skip=0&limit=2")
115
+
116
+ assert response.status_code == 200
117
+ data = response.json()
118
+ assert len(data) == 2
119
+
120
+
121
+ @pytest.mark.asyncio
122
+ async def test_get_pipeline_by_id(client, sample_position_data, sample_candidate_data):
123
+ """Test getting a specific pipeline by ID"""
124
+ pos_response = await client.post("/api/v1/positions", json=sample_position_data)
125
+ position_id = pos_response.json()["id"]
126
+
127
+ cand_response = await client.post("/api/v1/candidates", json=sample_candidate_data)
128
+ candidate_id = cand_response.json()["id"]
129
+
130
+ pipeline_data = {
131
+ "position_id": position_id,
132
+ "candidate_id": candidate_id
133
+ }
134
+
135
+ create_response = await client.post("/api/v1/pipelines", json=pipeline_data)
136
+ pipeline_id = create_response.json()["id"]
137
+
138
+ response = await client.get(f"/api/v1/pipelines/{pipeline_id}")
139
+
140
+ assert response.status_code == 200
141
+ data = response.json()
142
+ assert data["id"] == pipeline_id
143
+
144
+
145
+ @pytest.mark.asyncio
146
+ async def test_get_pipeline_not_found(client):
147
+ """Test getting a non-existent pipeline"""
148
+ fake_id = str(uuid.uuid4())
149
+ response = await client.get(f"/api/v1/pipelines/{fake_id}")
150
+
151
+ assert response.status_code == 404
152
+
153
+
154
+ @pytest.mark.asyncio
155
+ async def test_update_pipeline(client, sample_position_data, sample_candidate_data):
156
+ """Test updating a pipeline"""
157
+ pos_response = await client.post("/api/v1/positions", json=sample_position_data)
158
+ position_id = pos_response.json()["id"]
159
+
160
+ cand_response = await client.post("/api/v1/candidates", json=sample_candidate_data)
161
+ candidate_id = cand_response.json()["id"]
162
+
163
+ pipeline_data = {
164
+ "position_id": position_id,
165
+ "candidate_id": candidate_id
166
+ }
167
+
168
+ create_response = await client.post("/api/v1/pipelines", json=pipeline_data)
169
+ pipeline_id = create_response.json()["id"]
170
+
171
+ update_data = {"status": "contacted", "score": 90.0, "notes": "Updated notes"}
172
+ response = await client.put(f"/api/v1/pipelines/{pipeline_id}", json=update_data)
173
+
174
+ assert response.status_code == 200
175
+ data = response.json()
176
+ assert data["status"] == "contacted"
177
+ assert data["score"] == 90.0
178
+ assert data["notes"] == "Updated notes"
179
+
180
+
181
+ @pytest.mark.asyncio
182
+ async def test_update_pipeline_not_found(client):
183
+ """Test updating a non-existent pipeline"""
184
+ fake_id = str(uuid.uuid4())
185
+ update_data = {"status": "contacted"}
186
+ response = await client.put(f"/api/v1/pipelines/{fake_id}", json=update_data)
187
+
188
+ assert response.status_code == 404
189
+
190
+
191
+ @pytest.mark.asyncio
192
+ async def test_update_pipeline_status(client, sample_position_data, sample_candidate_data):
193
+ """Test updating pipeline status"""
194
+ pos_response = await client.post("/api/v1/positions", json=sample_position_data)
195
+ position_id = pos_response.json()["id"]
196
+
197
+ cand_response = await client.post("/api/v1/candidates", json=sample_candidate_data)
198
+ candidate_id = cand_response.json()["id"]
199
+
200
+ pipeline_data = {
201
+ "position_id": position_id,
202
+ "candidate_id": candidate_id
203
+ }
204
+
205
+ create_response = await client.post("/api/v1/pipelines", json=pipeline_data)
206
+ pipeline_id = create_response.json()["id"]
207
+
208
+ response = await client.put(f"/api/v1/pipelines/{pipeline_id}/status?status=interview")
209
+
210
+ assert response.status_code == 200
211
+ data = response.json()
212
+ assert data["status"] == "interview"
213
+
214
+
215
+ @pytest.mark.asyncio
216
+ async def test_delete_pipeline(client, sample_position_data, sample_candidate_data):
217
+ """Test deleting a pipeline"""
218
+ pos_response = await client.post("/api/v1/positions", json=sample_position_data)
219
+ position_id = pos_response.json()["id"]
220
+
221
+ cand_response = await client.post("/api/v1/candidates", json=sample_candidate_data)
222
+ candidate_id = cand_response.json()["id"]
223
+
224
+ pipeline_data = {
225
+ "position_id": position_id,
226
+ "candidate_id": candidate_id
227
+ }
228
+
229
+ create_response = await client.post("/api/v1/pipelines", json=pipeline_data)
230
+ pipeline_id = create_response.json()["id"]
231
+
232
+ response = await client.delete(f"/api/v1/pipelines/{pipeline_id}")
233
+
234
+ assert response.status_code == 200
235
+ data = response.json()
236
+ assert data["message"] == "Pipeline deleted successfully"
237
+
238
+ # Verify it's deleted
239
+ get_response = await client.get(f"/api/v1/pipelines/{pipeline_id}")
240
+ assert get_response.status_code == 404
241
+
242
+
243
+ @pytest.mark.asyncio
244
+ async def test_delete_pipeline_not_found(client):
245
+ """Test deleting a non-existent pipeline"""
246
+ fake_id = str(uuid.uuid4())
247
+ response = await client.delete(f"/api/v1/pipelines/{fake_id}")
248
+
249
+ assert response.status_code == 404
@@ -0,0 +1,183 @@
1
+ """
2
+ Positions API tests
3
+ """
4
+ import pytest
5
+ import uuid
6
+
7
+
8
+ @pytest.mark.asyncio
9
+ async def test_create_position(client, sample_position_data):
10
+ """Test creating a new position"""
11
+ response = await client.post("/api/v1/positions", json=sample_position_data)
12
+
13
+ assert response.status_code == 200
14
+ data = response.json()
15
+
16
+ assert data["title"] == sample_position_data["title"]
17
+ assert data["company"] == sample_position_data["company"]
18
+ assert data["status"] == "active"
19
+ assert "id" in data
20
+ assert "created_at" in data
21
+ assert "updated_at" in data
22
+
23
+
24
+ @pytest.mark.asyncio
25
+ async def test_get_positions(client, sample_position_data):
26
+ """Test getting all positions"""
27
+ # Create a position first
28
+ await client.post("/api/v1/positions", json=sample_position_data)
29
+
30
+ response = await client.get("/api/v1/positions")
31
+
32
+ assert response.status_code == 200
33
+ data = response.json()
34
+
35
+ assert isinstance(data, list)
36
+ assert len(data) >= 1
37
+
38
+
39
+ @pytest.mark.asyncio
40
+ async def test_get_positions_with_pagination(client, sample_position_data):
41
+ """Test getting positions with pagination"""
42
+ # Create multiple positions
43
+ for i in range(3):
44
+ data = sample_position_data.copy()
45
+ data["title"] = f"Position {i}"
46
+ await client.post("/api/v1/positions", json=data)
47
+
48
+ response = await client.get("/api/v1/positions?skip=0&limit=2")
49
+
50
+ assert response.status_code == 200
51
+ data = response.json()
52
+ assert len(data) == 2
53
+
54
+
55
+ @pytest.mark.asyncio
56
+ async def test_get_positions_with_status_filter(client, sample_position_data):
57
+ """Test getting positions with status filter"""
58
+ await client.post("/api/v1/positions", json=sample_position_data)
59
+
60
+ response = await client.get("/api/v1/positions?status=active")
61
+
62
+ assert response.status_code == 200
63
+ data = response.json()
64
+ assert all(pos["status"] == "active" for pos in data)
65
+
66
+
67
+ @pytest.mark.asyncio
68
+ async def test_get_position_by_id(client, sample_position_data):
69
+ """Test getting a specific position by ID"""
70
+ create_response = await client.post("/api/v1/positions", json=sample_position_data)
71
+ position_id = create_response.json()["id"]
72
+
73
+ response = await client.get(f"/api/v1/positions/{position_id}")
74
+
75
+ assert response.status_code == 200
76
+ data = response.json()
77
+ assert data["id"] == position_id
78
+ assert data["title"] == sample_position_data["title"]
79
+
80
+
81
+ @pytest.mark.asyncio
82
+ async def test_get_position_not_found(client):
83
+ """Test getting a non-existent position"""
84
+ fake_id = str(uuid.uuid4())
85
+ response = await client.get(f"/api/v1/positions/{fake_id}")
86
+
87
+ assert response.status_code == 404
88
+
89
+
90
+ @pytest.mark.asyncio
91
+ async def test_update_position(client, sample_position_data):
92
+ """Test updating a position"""
93
+ create_response = await client.post("/api/v1/positions", json=sample_position_data)
94
+ position_id = create_response.json()["id"]
95
+
96
+ update_data = {"title": "Updated Title", "company": "Updated Company"}
97
+ response = await client.put(f"/api/v1/positions/{position_id}", json=update_data)
98
+
99
+ assert response.status_code == 200
100
+ data = response.json()
101
+ assert data["title"] == "Updated Title"
102
+ assert data["company"] == "Updated Company"
103
+
104
+
105
+ @pytest.mark.asyncio
106
+ async def test_update_position_not_found(client):
107
+ """Test updating a non-existent position"""
108
+ fake_id = str(uuid.uuid4())
109
+ update_data = {"title": "Updated Title"}
110
+ response = await client.put(f"/api/v1/positions/{fake_id}", json=update_data)
111
+
112
+ assert response.status_code == 404
113
+
114
+
115
+ @pytest.mark.asyncio
116
+ async def test_delete_position(client, sample_position_data):
117
+ """Test deleting a position"""
118
+ create_response = await client.post("/api/v1/positions", json=sample_position_data)
119
+ position_id = create_response.json()["id"]
120
+
121
+ response = await client.delete(f"/api/v1/positions/{position_id}")
122
+
123
+ assert response.status_code == 200
124
+ data = response.json()
125
+ assert data["message"] == "Position deleted successfully"
126
+
127
+ # Verify it's deleted
128
+ get_response = await client.get(f"/api/v1/positions/{position_id}")
129
+ assert get_response.status_code == 404
130
+
131
+
132
+ @pytest.mark.asyncio
133
+ async def test_delete_position_not_found(client):
134
+ """Test deleting a non-existent position"""
135
+ fake_id = str(uuid.uuid4())
136
+ response = await client.delete(f"/api/v1/positions/{fake_id}")
137
+
138
+ assert response.status_code == 404
139
+
140
+
141
+ @pytest.mark.asyncio
142
+ async def test_pause_position(client, sample_position_data):
143
+ """Test pausing a position"""
144
+ create_response = await client.post("/api/v1/positions", json=sample_position_data)
145
+ position_id = create_response.json()["id"]
146
+
147
+ response = await client.post(f"/api/v1/positions/{position_id}/pause")
148
+
149
+ assert response.status_code == 200
150
+ data = response.json()
151
+ assert data["status"] == "paused"
152
+ assert data["loop_enabled"] == False
153
+
154
+
155
+ @pytest.mark.asyncio
156
+ async def test_resume_position(client, sample_position_data):
157
+ """Test resuming a paused position"""
158
+ create_response = await client.post("/api/v1/positions", json=sample_position_data)
159
+ position_id = create_response.json()["id"]
160
+
161
+ # Pause first
162
+ await client.post(f"/api/v1/positions/{position_id}/pause")
163
+
164
+ # Then resume
165
+ response = await client.post(f"/api/v1/positions/{position_id}/resume")
166
+
167
+ assert response.status_code == 200
168
+ data = response.json()
169
+ assert data["status"] == "active"
170
+ assert data["loop_enabled"] == True
171
+
172
+
173
+ @pytest.mark.asyncio
174
+ async def test_close_position(client, sample_position_data):
175
+ """Test closing a position"""
176
+ create_response = await client.post("/api/v1/positions", json=sample_position_data)
177
+ position_id = create_response.json()["id"]
178
+
179
+ response = await client.post(f"/api/v1/positions/{position_id}/close")
180
+
181
+ assert response.status_code == 200
182
+ data = response.json()
183
+ assert data["status"] == "closed"
@@ -0,0 +1,15 @@
1
+ """
2
+ Scheduler API tests
3
+ """
4
+ import pytest
5
+
6
+
7
+ @pytest.mark.asyncio
8
+ async def test_get_scheduler_jobs(client):
9
+ """Test getting scheduler jobs"""
10
+ response = await client.get("/api/v1/scheduler/jobs")
11
+
12
+ assert response.status_code == 200
13
+ data = response.json()
14
+
15
+ assert isinstance(data, list)
@@ -0,0 +1,15 @@
1
+ """
2
+ Skills API tests
3
+ """
4
+ import pytest
5
+
6
+
7
+ @pytest.mark.asyncio
8
+ async def test_get_skills(client):
9
+ """Test getting skills"""
10
+ response = await client.get("/api/v1/skills")
11
+
12
+ assert response.status_code == 200
13
+ data = response.json()
14
+
15
+ assert isinstance(data, list)
@@ -0,0 +1,35 @@
1
+ """
2
+ System API tests
3
+ """
4
+ import pytest
5
+
6
+
7
+ @pytest.mark.asyncio
8
+ async def test_get_system_config(client):
9
+ """Test getting system configuration"""
10
+ response = await client.get("/api/v1/system/config")
11
+
12
+ assert response.status_code == 200
13
+ data = response.json()
14
+
15
+ assert isinstance(data, dict)
16
+
17
+
18
+ @pytest.mark.asyncio
19
+ async def test_root_endpoint(client):
20
+ """Test root endpoint"""
21
+ response = await client.get("/")
22
+
23
+ assert response.status_code == 200
24
+ data = response.json()
25
+ assert data["message"] == "Recruiting Loop Agent is running!"
26
+
27
+
28
+ @pytest.mark.asyncio
29
+ async def test_health_check(client):
30
+ """Test health check endpoint"""
31
+ response = await client.get("/health")
32
+
33
+ assert response.status_code == 200
34
+ data = response.json()
35
+ assert data["status"] == "ok"
app/utils/__init__.py ADDED
File without changes
@@ -0,0 +1,5 @@
1
+ """
2
+ loop-agent-cli — Typer + Rich CLI for recruit-loop-agent.
3
+ """
4
+
5
+ __version__ = "0.1.0"