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/core/exceptions.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
class RecruitingAgentException(Exception):
|
|
2
|
+
"""Base exception for Recruiting Loop Agent"""
|
|
3
|
+
pass
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class PositionNotFoundException(RecruitingAgentException):
|
|
7
|
+
"""Raised when a position is not found"""
|
|
8
|
+
pass
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class CandidateNotFoundException(RecruitingAgentException):
|
|
12
|
+
"""Raised when a candidate is not found"""
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class PipelineNotFoundException(RecruitingAgentException):
|
|
17
|
+
"""Raised when a pipeline is not found"""
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class GitHubAPIException(RecruitingAgentException):
|
|
22
|
+
"""Raised when GitHub API returns an error"""
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class SMTPException(RecruitingAgentException):
|
|
27
|
+
"""Raised when SMTP operations fail"""
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class DatabaseException(RecruitingAgentException):
|
|
32
|
+
"""Raised when database operations fail"""
|
|
33
|
+
pass
|
app/core/logging.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import sys
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
# Create logs directory if it doesn't exist
|
|
6
|
+
log_dir = Path("logs")
|
|
7
|
+
log_dir.mkdir(exist_ok=True)
|
|
8
|
+
|
|
9
|
+
# Configure logger
|
|
10
|
+
logging.basicConfig(
|
|
11
|
+
level=logging.INFO,
|
|
12
|
+
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
|
13
|
+
handlers=[
|
|
14
|
+
logging.FileHandler(log_dir / "app.log"),
|
|
15
|
+
logging.StreamHandler(sys.stdout)
|
|
16
|
+
]
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger("recruiting_agent")
|
app/database/__init__.py
ADDED
|
File without changes
|
app/database/base.py
ADDED
app/database/session.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
|
|
2
|
+
from app.core.config import settings
|
|
3
|
+
|
|
4
|
+
# Create async database engine
|
|
5
|
+
engine = create_async_engine(
|
|
6
|
+
settings.database_url,
|
|
7
|
+
echo=True, # Set to True for SQL query logging in debug mode
|
|
8
|
+
pool_pre_ping=True,
|
|
9
|
+
pool_recycle=300,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
# Create async session maker
|
|
13
|
+
from sqlalchemy.orm import sessionmaker
|
|
14
|
+
AsyncSessionLocal = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
async def get_db():
|
|
18
|
+
"""Dependency for getting database session"""
|
|
19
|
+
async with AsyncSessionLocal() as session:
|
|
20
|
+
yield session
|
app/main.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
from fastapi import FastAPI
|
|
2
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
3
|
+
from app.api.router import router
|
|
4
|
+
import uvicorn
|
|
5
|
+
from app.core.exception_handler import (
|
|
6
|
+
recruiting_agent_exception_handler,
|
|
7
|
+
position_not_found_exception_handler,
|
|
8
|
+
candidate_not_found_exception_handler,
|
|
9
|
+
pipeline_not_found_exception_handler,
|
|
10
|
+
github_api_exception_handler,
|
|
11
|
+
smtp_exception_handler,
|
|
12
|
+
database_exception_handler,
|
|
13
|
+
http_exception_handler
|
|
14
|
+
)
|
|
15
|
+
from app.core.exceptions import (
|
|
16
|
+
RecruitingAgentException,
|
|
17
|
+
PositionNotFoundException,
|
|
18
|
+
CandidateNotFoundException,
|
|
19
|
+
PipelineNotFoundException,
|
|
20
|
+
GitHubAPIException,
|
|
21
|
+
SMTPException,
|
|
22
|
+
DatabaseException
|
|
23
|
+
)
|
|
24
|
+
from fastapi import HTTPException
|
|
25
|
+
from app.database.base import Base
|
|
26
|
+
from app.database.session import engine
|
|
27
|
+
|
|
28
|
+
app = FastAPI(
|
|
29
|
+
title="Recruiting Loop Agent",
|
|
30
|
+
description="An autonomous recruiting agent that continuously searches for candidates",
|
|
31
|
+
version="3.0.0",
|
|
32
|
+
redirect_slashes=False
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
# CORS middleware - allow frontend requests
|
|
36
|
+
app.add_middleware(
|
|
37
|
+
CORSMiddleware,
|
|
38
|
+
allow_origins=["http://localhost:3000", "http://127.0.0.1:3000"],
|
|
39
|
+
allow_credentials=True,
|
|
40
|
+
allow_methods=["*"],
|
|
41
|
+
allow_headers=["*"],
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
# Register exception handlers
|
|
45
|
+
app.add_exception_handler(RecruitingAgentException, recruiting_agent_exception_handler)
|
|
46
|
+
app.add_exception_handler(PositionNotFoundException, position_not_found_exception_handler)
|
|
47
|
+
app.add_exception_handler(CandidateNotFoundException, candidate_not_found_exception_handler)
|
|
48
|
+
app.add_exception_handler(PipelineNotFoundException, pipeline_not_found_exception_handler)
|
|
49
|
+
app.add_exception_handler(GitHubAPIException, github_api_exception_handler)
|
|
50
|
+
app.add_exception_handler(SMTPException, smtp_exception_handler)
|
|
51
|
+
app.add_exception_handler(DatabaseException, database_exception_handler)
|
|
52
|
+
app.add_exception_handler(HTTPException, http_exception_handler)
|
|
53
|
+
|
|
54
|
+
# Include API routes
|
|
55
|
+
app.include_router(router, prefix="/api/v1")
|
|
56
|
+
|
|
57
|
+
@app.get("/")
|
|
58
|
+
async def root():
|
|
59
|
+
return {"message": "Recruiting Loop Agent is running!"}
|
|
60
|
+
|
|
61
|
+
@app.get("/health")
|
|
62
|
+
async def health_check():
|
|
63
|
+
return {"status": "ok"}
|
|
64
|
+
|
|
65
|
+
# Create database tables on startup
|
|
66
|
+
@app.on_event("startup")
|
|
67
|
+
async def startup():
|
|
68
|
+
async with engine.begin() as conn:
|
|
69
|
+
await conn.run_sync(Base.metadata.create_all)
|
|
70
|
+
|
|
71
|
+
if __name__ == "__main__":
|
|
72
|
+
uvicorn.run(app, host="0.0.0.0", port=8000)
|
app/models/__init__.py
ADDED
|
File without changes
|
app/models/agent_run.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from sqlalchemy import Column, String, Text, Integer, DateTime
|
|
2
|
+
from app.database.base import Base
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
import uuid
|
|
5
|
+
|
|
6
|
+
class AgentRun(Base):
|
|
7
|
+
__tablename__ = "agent_runs"
|
|
8
|
+
|
|
9
|
+
id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))
|
|
10
|
+
position_id = Column(String) # Foreign key to positions table
|
|
11
|
+
started_at = Column(DateTime, default=datetime.utcnow)
|
|
12
|
+
finished_at = Column(DateTime)
|
|
13
|
+
duration_ms = Column(Integer) # Duration in milliseconds
|
|
14
|
+
candidates_found = Column(Integer, default=0)
|
|
15
|
+
candidates_added = Column(Integer, default=0)
|
|
16
|
+
emails_sent = Column(Integer, default=0)
|
|
17
|
+
status = Column(String, default="running") # running, success, failed
|
|
18
|
+
error = Column(Text) # Error message if failed
|
app/models/candidate.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from sqlalchemy import Column, String, Text, Integer, Boolean, DateTime, Float
|
|
2
|
+
from app.database.base import Base
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
import uuid
|
|
5
|
+
|
|
6
|
+
class Candidate(Base):
|
|
7
|
+
__tablename__ = "candidates"
|
|
8
|
+
|
|
9
|
+
id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))
|
|
10
|
+
source = Column(String, nullable=False) # github, linkedin, etc
|
|
11
|
+
source_id = Column(String, nullable=False) # GitHub user ID
|
|
12
|
+
github_login = Column(String) # GitHub username
|
|
13
|
+
name = Column(String)
|
|
14
|
+
email = Column(String)
|
|
15
|
+
location = Column(String)
|
|
16
|
+
company = Column(String)
|
|
17
|
+
title = Column(String)
|
|
18
|
+
bio = Column(Text)
|
|
19
|
+
followers = Column(Integer, default=0)
|
|
20
|
+
public_repos = Column(Integer, default=0)
|
|
21
|
+
skills = Column(Text) # Stored as JSON string
|
|
22
|
+
profile_url = Column(String)
|
|
23
|
+
avatar_url = Column(String)
|
|
24
|
+
search_keywords = Column(Text) # Stored as JSON string
|
|
25
|
+
appearance_count = Column(Integer, default=1)
|
|
26
|
+
source_weight = Column(Float, default=1.0) # Weight based on how often they appear
|
|
27
|
+
created_at = Column(DateTime, default=datetime.utcnow)
|
|
28
|
+
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
app/models/node_log.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from sqlalchemy import Column, String, Text, Integer, DateTime
|
|
2
|
+
from app.database.base import Base
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
import uuid
|
|
5
|
+
|
|
6
|
+
class NodeLog(Base):
|
|
7
|
+
__tablename__ = "node_logs"
|
|
8
|
+
|
|
9
|
+
id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))
|
|
10
|
+
run_id = Column(String) # Foreign key to agent_runs table
|
|
11
|
+
node_name = Column(String) # search, dedup, score, outreach, evaluate
|
|
12
|
+
started_at = Column(DateTime, default=datetime.utcnow)
|
|
13
|
+
finished_at = Column(DateTime)
|
|
14
|
+
duration_ms = Column(Integer) # Duration in milliseconds
|
|
15
|
+
status = Column(String) # success, failed
|
|
16
|
+
input = Column(Text) # Input data as JSON string
|
|
17
|
+
output = Column(Text) # Output data as JSON string
|
|
18
|
+
error = Column(Text) # Error message if failed
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from sqlalchemy import Column, String, Text, Integer, DateTime
|
|
2
|
+
from app.database.base import Base
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
import uuid
|
|
5
|
+
|
|
6
|
+
class OutreachLog(Base):
|
|
7
|
+
__tablename__ = "outreach_logs"
|
|
8
|
+
|
|
9
|
+
id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))
|
|
10
|
+
pipeline_id = Column(String) # Foreign key to pipelines table
|
|
11
|
+
subject = Column(String)
|
|
12
|
+
body = Column(Text)
|
|
13
|
+
status = Column(String, default="pending") # pending, sent, failed
|
|
14
|
+
error = Column(Text) # Error message if failed
|
|
15
|
+
sent_at = Column(DateTime)
|
|
16
|
+
created_at = Column(DateTime, default=datetime.utcnow)
|
app/models/pipeline.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from sqlalchemy import Column, String, Text, Integer, DateTime, Float
|
|
2
|
+
from app.database.base import Base
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
import uuid
|
|
5
|
+
|
|
6
|
+
class Pipeline(Base):
|
|
7
|
+
__tablename__ = "pipelines"
|
|
8
|
+
|
|
9
|
+
id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))
|
|
10
|
+
position_id = Column(String) # Foreign key to positions table
|
|
11
|
+
candidate_id = Column(String) # Foreign key to candidates table
|
|
12
|
+
status = Column(String, default="discovered") # discovered, contacted, replied, interview, offer, rejected
|
|
13
|
+
score = Column(Float, default=0.0) # Calculated score
|
|
14
|
+
score_detail = Column(Text) # Detailed score breakdown as JSON string
|
|
15
|
+
contact_count = Column(Integer, default=0)
|
|
16
|
+
last_contacted_at = Column(DateTime)
|
|
17
|
+
next_followup_at = Column(DateTime)
|
|
18
|
+
candidate_interest = Column(String) # high, medium, low
|
|
19
|
+
notes = Column(Text)
|
|
20
|
+
created_at = Column(DateTime, default=datetime.utcnow)
|
|
21
|
+
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
app/models/position.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from sqlalchemy import Column, String, Text, Integer, Boolean, DateTime
|
|
2
|
+
from app.database.base import Base
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
import uuid
|
|
5
|
+
|
|
6
|
+
class Position(Base):
|
|
7
|
+
__tablename__ = "positions"
|
|
8
|
+
|
|
9
|
+
id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))
|
|
10
|
+
title = Column(String, nullable=False)
|
|
11
|
+
company = Column(String, nullable=False)
|
|
12
|
+
description = Column(Text)
|
|
13
|
+
location = Column(String)
|
|
14
|
+
required_skills = Column(Text) # Stored as JSON string
|
|
15
|
+
search_keywords = Column(Text) # Stored as JSON string
|
|
16
|
+
status = Column(String, default="active") # active, paused, closed
|
|
17
|
+
loop_interval = Column(Integer, default=60) # minutes
|
|
18
|
+
loop_enabled = Column(Boolean, default=True)
|
|
19
|
+
last_loop_at = Column(DateTime)
|
|
20
|
+
next_loop_at = Column(DateTime)
|
|
21
|
+
created_at = Column(DateTime, default=datetime.utcnow)
|
|
22
|
+
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from sqlalchemy import Column, String, Text, Integer, Boolean, DateTime
|
|
2
|
+
from app.database.base import Base
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
import uuid
|
|
5
|
+
|
|
6
|
+
class SchedulerJob(Base):
|
|
7
|
+
__tablename__ = "scheduler_jobs"
|
|
8
|
+
|
|
9
|
+
id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))
|
|
10
|
+
position_id = Column(String) # Foreign key to positions table
|
|
11
|
+
enabled = Column(Boolean, default=True)
|
|
12
|
+
interval_minutes = Column(Integer, default=60)
|
|
13
|
+
next_run = Column(DateTime)
|
|
14
|
+
last_run = Column(DateTime)
|
|
15
|
+
total_runs = Column(Integer, default=0)
|
|
16
|
+
status = Column(String, default="waiting") # waiting, running, paused
|
app/models/skill.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from sqlalchemy import Column, String, Text, Boolean, DateTime
|
|
2
|
+
from app.database.base import Base
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
import uuid
|
|
5
|
+
|
|
6
|
+
class Skill(Base):
|
|
7
|
+
__tablename__ = "skills"
|
|
8
|
+
|
|
9
|
+
id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))
|
|
10
|
+
name = Column(String, nullable=False) # github_search, linkedin_search, etc
|
|
11
|
+
description = Column(Text)
|
|
12
|
+
enabled = Column(Boolean, default=True)
|
|
13
|
+
created_at = Column(DateTime, default=datetime.utcnow)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from sqlalchemy import Column, String, Text, DateTime
|
|
2
|
+
from app.database.base import Base
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
import uuid
|
|
5
|
+
|
|
6
|
+
class SystemConfig(Base):
|
|
7
|
+
__tablename__ = "system_configs"
|
|
8
|
+
|
|
9
|
+
id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))
|
|
10
|
+
key = Column(String, nullable=False) # SMTP_HOST, GITHUB_TOKEN, etc
|
|
11
|
+
value = Column(Text) # Configuration value
|
|
12
|
+
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
|
File without changes
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
2
|
+
from sqlalchemy.future import select
|
|
3
|
+
from app.models.agent_run import AgentRun
|
|
4
|
+
from app.schemas.agent_run import AgentRunCreate, AgentRunUpdate
|
|
5
|
+
from typing import List, Optional
|
|
6
|
+
import uuid
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
|
|
9
|
+
class AgentRunRepository:
|
|
10
|
+
def __init__(self, db_session: AsyncSession):
|
|
11
|
+
self.db_session = db_session
|
|
12
|
+
|
|
13
|
+
async def create(self, agent_run_data: AgentRunCreate) -> AgentRun:
|
|
14
|
+
db_agent_run = AgentRun(**agent_run_data.model_dump())
|
|
15
|
+
self.db_session.add(db_agent_run)
|
|
16
|
+
await self.db_session.commit()
|
|
17
|
+
await self.db_session.refresh(db_agent_run)
|
|
18
|
+
return db_agent_run
|
|
19
|
+
|
|
20
|
+
async def get_by_id(self, agent_run_id: uuid.UUID) -> Optional[AgentRun]:
|
|
21
|
+
stmt = select(AgentRun).where(AgentRun.id == agent_run_id)
|
|
22
|
+
result = await self.db_session.execute(stmt)
|
|
23
|
+
return result.scalar_one_or_none()
|
|
24
|
+
|
|
25
|
+
async def get_by_position(self, position_id: uuid.UUID, skip: int = 0, limit: int = 100) -> List[AgentRun]:
|
|
26
|
+
stmt = select(AgentRun).where(AgentRun.position_id == position_id).offset(skip).limit(limit)
|
|
27
|
+
result = await self.db_session.execute(stmt)
|
|
28
|
+
return result.scalars().all()
|
|
29
|
+
|
|
30
|
+
async def get_all(self, skip: int = 0, limit: int = 100) -> List[AgentRun]:
|
|
31
|
+
stmt = select(AgentRun).offset(skip).limit(limit)
|
|
32
|
+
result = await self.db_session.execute(stmt)
|
|
33
|
+
return result.scalars().all()
|
|
34
|
+
|
|
35
|
+
async def update(self, agent_run_id: uuid.UUID, agent_run_data: AgentRunUpdate) -> Optional[AgentRun]:
|
|
36
|
+
db_agent_run = await self.get_by_id(agent_run_id)
|
|
37
|
+
if db_agent_run:
|
|
38
|
+
for field, value in agent_run_data.model_dump(exclude_unset=True).items():
|
|
39
|
+
setattr(db_agent_run, field, value)
|
|
40
|
+
await self.db_session.commit()
|
|
41
|
+
await self.db_session.refresh(db_agent_run)
|
|
42
|
+
return db_agent_run
|
|
43
|
+
|
|
44
|
+
async def update_completion(
|
|
45
|
+
self,
|
|
46
|
+
agent_run_id: uuid.UUID,
|
|
47
|
+
status: str,
|
|
48
|
+
duration_ms: int,
|
|
49
|
+
candidates_found: int,
|
|
50
|
+
candidates_added: int,
|
|
51
|
+
emails_sent: int,
|
|
52
|
+
error: str = None
|
|
53
|
+
) -> Optional[AgentRun]:
|
|
54
|
+
db_agent_run = await self.get_by_id(agent_run_id)
|
|
55
|
+
if db_agent_run:
|
|
56
|
+
db_agent_run.status = status
|
|
57
|
+
db_agent_run.duration_ms = duration_ms
|
|
58
|
+
db_agent_run.finished_at = datetime.utcnow()
|
|
59
|
+
db_agent_run.candidates_found = candidates_found
|
|
60
|
+
db_agent_run.candidates_added = candidates_added
|
|
61
|
+
db_agent_run.emails_sent = emails_sent
|
|
62
|
+
if error:
|
|
63
|
+
db_agent_run.error = error
|
|
64
|
+
await self.db_session.commit()
|
|
65
|
+
await self.db_session.refresh(db_agent_run)
|
|
66
|
+
return db_agent_run
|
|
67
|
+
|
|
68
|
+
async def delete(self, agent_run_id: uuid.UUID) -> bool:
|
|
69
|
+
db_agent_run = await self.get_by_id(agent_run_id)
|
|
70
|
+
if db_agent_run:
|
|
71
|
+
await self.db_session.delete(db_agent_run)
|
|
72
|
+
await self.db_session.commit()
|
|
73
|
+
return True
|
|
74
|
+
return False
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
2
|
+
from sqlalchemy.future import select
|
|
3
|
+
from app.models.candidate import Candidate
|
|
4
|
+
from app.schemas.candidate import CandidateCreate, CandidateUpdate
|
|
5
|
+
from typing import List, Optional
|
|
6
|
+
import json
|
|
7
|
+
import uuid
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _serialize_candidate_data(data: dict) -> dict:
|
|
11
|
+
"""Serialize list fields to JSON strings for database storage."""
|
|
12
|
+
for field in ("skills", "search_keywords"):
|
|
13
|
+
if field in data and data[field] is not None:
|
|
14
|
+
data[field] = json.dumps(data[field])
|
|
15
|
+
return data
|
|
16
|
+
|
|
17
|
+
class CandidateRepository:
|
|
18
|
+
def __init__(self, db_session: AsyncSession):
|
|
19
|
+
self.db_session = db_session
|
|
20
|
+
|
|
21
|
+
async def create(self, candidate_data: CandidateCreate) -> Candidate:
|
|
22
|
+
data = _serialize_candidate_data(candidate_data.model_dump())
|
|
23
|
+
db_candidate = Candidate(**data)
|
|
24
|
+
self.db_session.add(db_candidate)
|
|
25
|
+
await self.db_session.commit()
|
|
26
|
+
await self.db_session.refresh(db_candidate)
|
|
27
|
+
return db_candidate
|
|
28
|
+
|
|
29
|
+
async def get_by_id(self, candidate_id: uuid.UUID) -> Optional[Candidate]:
|
|
30
|
+
stmt = select(Candidate).where(Candidate.id == str(candidate_id))
|
|
31
|
+
result = await self.db_session.execute(stmt)
|
|
32
|
+
return result.scalar_one_or_none()
|
|
33
|
+
|
|
34
|
+
async def get_by_source_id(self, source: str, source_id: str) -> Optional[Candidate]:
|
|
35
|
+
stmt = select(Candidate).where(
|
|
36
|
+
(Candidate.source == source) & (Candidate.source_id == source_id)
|
|
37
|
+
)
|
|
38
|
+
result = await self.db_session.execute(stmt)
|
|
39
|
+
return result.scalar_one_or_none()
|
|
40
|
+
|
|
41
|
+
async def get_all(self, skip: int = 0, limit: int = 100, keyword: Optional[str] = None) -> List[Candidate]:
|
|
42
|
+
stmt = select(Candidate)
|
|
43
|
+
if keyword:
|
|
44
|
+
# Search in name, company, title, location, skills
|
|
45
|
+
stmt = stmt.where(
|
|
46
|
+
(Candidate.name.contains(keyword)) |
|
|
47
|
+
(Candidate.company.contains(keyword)) |
|
|
48
|
+
(Candidate.title.contains(keyword)) |
|
|
49
|
+
(Candidate.location.contains(keyword))
|
|
50
|
+
)
|
|
51
|
+
stmt = stmt.offset(skip).limit(limit)
|
|
52
|
+
result = await self.db_session.execute(stmt)
|
|
53
|
+
return result.scalars().all()
|
|
54
|
+
|
|
55
|
+
async def update(self, candidate_id: uuid.UUID, candidate_data: CandidateUpdate) -> Optional[Candidate]:
|
|
56
|
+
db_candidate = await self.get_by_id(candidate_id)
|
|
57
|
+
if db_candidate:
|
|
58
|
+
update_data = _serialize_candidate_data(candidate_data.model_dump(exclude_unset=True))
|
|
59
|
+
for field, value in update_data.items():
|
|
60
|
+
setattr(db_candidate, field, value)
|
|
61
|
+
await self.db_session.commit()
|
|
62
|
+
await self.db_session.refresh(db_candidate)
|
|
63
|
+
return db_candidate
|
|
64
|
+
|
|
65
|
+
async def delete(self, candidate_id: uuid.UUID) -> bool:
|
|
66
|
+
db_candidate = await self.get_by_id(candidate_id)
|
|
67
|
+
if db_candidate:
|
|
68
|
+
await self.db_session.delete(db_candidate)
|
|
69
|
+
await self.db_session.commit()
|
|
70
|
+
return True
|
|
71
|
+
return False
|
|
72
|
+
|
|
73
|
+
async def increment_appearance_count(self, source: str, source_id: str) -> Optional[Candidate]:
|
|
74
|
+
db_candidate = await self.get_by_source_id(source, source_id)
|
|
75
|
+
if db_candidate:
|
|
76
|
+
db_candidate.appearance_count += 1
|
|
77
|
+
# Update source weight based on appearance count
|
|
78
|
+
if db_candidate.appearance_count <= 2:
|
|
79
|
+
db_candidate.source_weight = float(db_candidate.appearance_count)
|
|
80
|
+
else:
|
|
81
|
+
db_candidate.source_weight = 2.0 # Max weight is 2.0
|
|
82
|
+
await self.db_session.commit()
|
|
83
|
+
await self.db_session.refresh(db_candidate)
|
|
84
|
+
return db_candidate
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
2
|
+
from sqlalchemy.future import select
|
|
3
|
+
from app.models.node_log import NodeLog
|
|
4
|
+
from app.schemas.node_log import NodeLogCreate, NodeLogUpdate
|
|
5
|
+
from typing import List, Optional
|
|
6
|
+
import uuid
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
|
|
9
|
+
class NodeLogRepository:
|
|
10
|
+
def __init__(self, db_session: AsyncSession):
|
|
11
|
+
self.db_session = db_session
|
|
12
|
+
|
|
13
|
+
async def create(self, node_log_data: NodeLogCreate) -> NodeLog:
|
|
14
|
+
db_node_log = NodeLog(**node_log_data.model_dump())
|
|
15
|
+
self.db_session.add(db_node_log)
|
|
16
|
+
await self.db_session.commit()
|
|
17
|
+
await self.db_session.refresh(db_node_log)
|
|
18
|
+
return db_node_log
|
|
19
|
+
|
|
20
|
+
async def get_by_id(self, node_log_id: uuid.UUID) -> Optional[NodeLog]:
|
|
21
|
+
stmt = select(NodeLog).where(NodeLog.id == node_log_id)
|
|
22
|
+
result = await self.db_session.execute(stmt)
|
|
23
|
+
return result.scalar_one_or_none()
|
|
24
|
+
|
|
25
|
+
async def get_by_run(self, run_id: uuid.UUID, skip: int = 0, limit: int = 100) -> List[NodeLog]:
|
|
26
|
+
stmt = select(NodeLog).where(NodeLog.run_id == run_id).order_by(NodeLog.started_at.desc()).offset(skip).limit(limit)
|
|
27
|
+
result = await self.db_session.execute(stmt)
|
|
28
|
+
return result.scalars().all()
|
|
29
|
+
|
|
30
|
+
async def get_by_node_name(self, run_id: uuid.UUID, node_name: str) -> List[NodeLog]:
|
|
31
|
+
stmt = select(NodeLog).where(
|
|
32
|
+
(NodeLog.run_id == run_id) & (NodeLog.node_name == node_name)
|
|
33
|
+
)
|
|
34
|
+
result = await self.db_session.execute(stmt)
|
|
35
|
+
return result.scalars().all()
|
|
36
|
+
|
|
37
|
+
async def get_all(self, skip: int = 0, limit: int = 100) -> List[NodeLog]:
|
|
38
|
+
stmt = select(NodeLog).offset(skip).limit(limit)
|
|
39
|
+
result = await self.db_session.execute(stmt)
|
|
40
|
+
return result.scalars().all()
|
|
41
|
+
|
|
42
|
+
async def update(self, node_log_id: uuid.UUID, node_log_data: NodeLogUpdate) -> Optional[NodeLog]:
|
|
43
|
+
db_node_log = await self.get_by_id(node_log_id)
|
|
44
|
+
if db_node_log:
|
|
45
|
+
for field, value in node_log_data.model_dump(exclude_unset=True).items():
|
|
46
|
+
setattr(db_node_log, field, value)
|
|
47
|
+
await self.db_session.commit()
|
|
48
|
+
await self.db_session.refresh(db_node_log)
|
|
49
|
+
return db_node_log
|
|
50
|
+
|
|
51
|
+
async def delete(self, node_log_id: uuid.UUID) -> bool:
|
|
52
|
+
db_node_log = await self.get_by_id(node_log_id)
|
|
53
|
+
if db_node_log:
|
|
54
|
+
await self.db_session.delete(db_node_log)
|
|
55
|
+
await self.db_session.commit()
|
|
56
|
+
return True
|
|
57
|
+
return False
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
2
|
+
from sqlalchemy.future import select
|
|
3
|
+
from app.models.outreach_log import OutreachLog
|
|
4
|
+
from app.schemas.outreach_log import OutreachLogCreate, OutreachLogUpdate
|
|
5
|
+
from typing import List, Optional
|
|
6
|
+
import uuid
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
|
|
9
|
+
class OutreachLogRepository:
|
|
10
|
+
def __init__(self, db_session: AsyncSession):
|
|
11
|
+
self.db_session = db_session
|
|
12
|
+
|
|
13
|
+
async def create(self, outreach_log_data: OutreachLogCreate) -> OutreachLog:
|
|
14
|
+
db_outreach_log = OutreachLog(**outreach_log_data.model_dump())
|
|
15
|
+
self.db_session.add(db_outreach_log)
|
|
16
|
+
await self.db_session.commit()
|
|
17
|
+
await self.db_session.refresh(db_outreach_log)
|
|
18
|
+
return db_outreach_log
|
|
19
|
+
|
|
20
|
+
async def get_by_id(self, outreach_log_id: uuid.UUID) -> Optional[OutreachLog]:
|
|
21
|
+
stmt = select(OutreachLog).where(OutreachLog.id == outreach_log_id)
|
|
22
|
+
result = await self.db_session.execute(stmt)
|
|
23
|
+
return result.scalar_one_or_none()
|
|
24
|
+
|
|
25
|
+
async def get_by_pipeline(self, pipeline_id: uuid.UUID, skip: int = 0, limit: int = 100) -> List[OutreachLog]:
|
|
26
|
+
stmt = select(OutreachLog).where(OutreachLog.pipeline_id == pipeline_id).offset(skip).limit(limit)
|
|
27
|
+
result = await self.db_session.execute(stmt)
|
|
28
|
+
return result.scalars().all()
|
|
29
|
+
|
|
30
|
+
async def get_all(self, skip: int = 0, limit: int = 100) -> List[OutreachLog]:
|
|
31
|
+
stmt = select(OutreachLog).offset(skip).limit(limit)
|
|
32
|
+
result = await self.db_session.execute(stmt)
|
|
33
|
+
return result.scalars().all()
|
|
34
|
+
|
|
35
|
+
async def update_status(self, outreach_log_id: uuid.UUID, status: str) -> Optional[OutreachLog]:
|
|
36
|
+
db_outreach_log = await self.get_by_id(outreach_log_id)
|
|
37
|
+
if db_outreach_log:
|
|
38
|
+
db_outreach_log.status = status
|
|
39
|
+
db_outreach_log.sent_at = datetime.utcnow()
|
|
40
|
+
await self.db_session.commit()
|
|
41
|
+
await self.db_session.refresh(db_outreach_log)
|
|
42
|
+
return db_outreach_log
|
|
43
|
+
|
|
44
|
+
async def update_status_with_error(self, outreach_log_id: uuid.UUID, status: str, error: str) -> Optional[OutreachLog]:
|
|
45
|
+
db_outreach_log = await self.get_by_id(outreach_log_id)
|
|
46
|
+
if db_outreach_log:
|
|
47
|
+
db_outreach_log.status = status
|
|
48
|
+
db_outreach_log.error = error
|
|
49
|
+
db_outreach_log.sent_at = datetime.utcnow()
|
|
50
|
+
await self.db_session.commit()
|
|
51
|
+
await self.db_session.refresh(db_outreach_log)
|
|
52
|
+
return db_outreach_log
|
|
53
|
+
|
|
54
|
+
async def delete(self, outreach_log_id: uuid.UUID) -> bool:
|
|
55
|
+
db_outreach_log = await self.get_by_id(outreach_log_id)
|
|
56
|
+
if db_outreach_log:
|
|
57
|
+
await self.db_session.delete(db_outreach_log)
|
|
58
|
+
await self.db_session.commit()
|
|
59
|
+
return True
|
|
60
|
+
return False
|