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/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")
File without changes
app/database/base.py ADDED
@@ -0,0 +1,4 @@
1
+ from sqlalchemy.ext.declarative import declarative_base
2
+ from sqlalchemy.orm import DeclarativeMeta
3
+
4
+ Base: DeclarativeMeta = declarative_base()
@@ -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
@@ -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
@@ -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