update from github stable code (#3)
Browse files- Clean initial commit for HuggingFace (f108b29491d90326f90f71fa07edef74d570194b)
- stable v (dbe2d5a13184387b222e127d2a35ab2e1dc948d4)
- Revert "Clean initial commit for HuggingFace" (b2b94e06694eceb7028a307909def1eb5638d81b)
- Update intro.md (5b7957111cd66bf284f360179a122c6b44409cca)
Co-authored-by: Dmitri Moscoglo <dimim@users.noreply.huggingface.co>
This view is limited to 50 files because it contains too many changes.
See raw diff
- .gitignore +4 -1
- README.md +153 -68
- docker/Dockerfile.candidates_db_init +5 -3
- docker/Dockerfile.supervisor_api +1 -1
- docker/docker-compose.yml +46 -52
- docker/info.md +6 -2
- docs/intro.md +457 -0
- docs/video/script.md +69 -0
- intro.md +1 -0
- requirements/agent.txt +0 -1
- scripts/db/list_candidates.py +13 -3
- scripts/db/setup_demo_state.py +65 -0
- scripts/db/test_connection.py +1 -1
- scripts/db/test_cv_upload.py +45 -0
- scripts/db/test_session.py +1 -1
- scripts/db/wipe.py +1 -1
- scripts/infra/reset_db.sh +12 -0
- src/backend/__init__.py +0 -0
- src/backend/agents/__init__.py +14 -0
- src/backend/agents/cv_screening/__init__.py +4 -0
- src/backend/agents/cv_screening/cv_screener.py +88 -0
- src/backend/agents/cv_screening/cv_screening_workflow.py +108 -0
- src/backend/agents/cv_screening/schemas/__init__.py +0 -0
- src/backend/agents/cv_screening/schemas/output_schema.py +12 -0
- src/backend/agents/cv_screening/tools/__init__.py +0 -0
- src/backend/agents/cv_screening/utils/__init__.py +5 -0
- src/backend/agents/cv_screening/utils/read_file.py +7 -0
- src/backend/agents/db_executor/__init__.py +5 -0
- src/backend/agents/db_executor/codeact/__init__.py +6 -0
- src/backend/agents/db_executor/codeact/core/codeact.py +545 -0
- src/backend/agents/db_executor/codeact/prompts/local_archive/original.txt +18 -0
- src/backend/agents/db_executor/codeact/prompts/local_archive/test.txt +25 -0
- src/backend/agents/db_executor/codeact/prompts/prompt_layer.py +162 -0
- src/backend/agents/db_executor/codeact/schemas/__init__.py +10 -0
- src/backend/agents/db_executor/codeact/schemas/openai_key.py +56 -0
- src/backend/agents/db_executor/codeact/schemas/stream.py +8 -0
- src/backend/agents/db_executor/codeact/states/state.py +10 -0
- src/backend/agents/db_executor/codeact/tools/__init__.py +0 -0
- src/backend/agents/db_executor/codeact/tools/tools.py +53 -0
- src/backend/agents/db_executor/codeact/utils/__init__.py +5 -0
- src/backend/agents/db_executor/codeact/utils/pretty_state.py +73 -0
- src/backend/agents/db_executor/db_executor.py +99 -0
- src/backend/agents/db_executor/info.md +22 -0
- src/backend/agents/example/info.md +66 -0
- src/backend/agents/example/react_agent.py +59 -0
- src/backend/agents/gcalendar/__init__.py +2 -0
- src/backend/agents/gcalendar/gcalendar_agent.py +94 -0
- src/backend/agents/gcalendar/schemas/__init__.py +0 -0
- src/backend/agents/gcalendar/tools/__init__.py +0 -0
- src/backend/agents/gmail/__init__.py +2 -0
.gitignore
CHANGED
|
@@ -67,4 +67,7 @@ src/database/cvs/tests/*.txt
|
|
| 67 |
.lgcache/
|
| 68 |
.langgraph_api/
|
| 69 |
|
| 70 |
-
.idea/
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
.lgcache/
|
| 68 |
.langgraph_api/
|
| 69 |
|
| 70 |
+
.idea/
|
| 71 |
+
|
| 72 |
+
# any .wav files
|
| 73 |
+
*.wav
|
README.md
CHANGED
|
@@ -1,14 +1,3 @@
|
|
| 1 |
-
---
|
| 2 |
-
license: mit
|
| 3 |
-
title: HR Assistant
|
| 4 |
-
sdk: docker
|
| 5 |
-
emoji: 🏢
|
| 6 |
-
colorFrom: green
|
| 7 |
-
colorTo: green
|
| 8 |
-
tags:
|
| 9 |
-
- mcp-in-action-track-enterprise
|
| 10 |
-
---
|
| 11 |
-
|
| 12 |
# ***`Recruitment Agent`***
|
| 13 |
<p align="left">
|
| 14 |
<img src="https://img.shields.io/badge/MCP%20Hackathon-Track%202%20%E2%80%94%20Enterprise-blue" />
|
|
@@ -22,16 +11,14 @@ tags:
|
|
| 22 |
<img src="https://img.shields.io/badge/Google%20Cloud-APIs%20%26%20MCP%20Tools-blue?logo=googlecloud" />
|
| 23 |
</p>
|
| 24 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
> This project was developed as part of the **[MCP 1st Birthday Hackathon](https://huggingface.co/MCP-1st-Birthday)** — submitted under
|
| 26 |
> **Track 2: MCP in Action (Enterprise)**, showcasing a real-world multi-agent application built on top of the Model Context Protocol.
|
| 27 |
|
| 28 |
-
## 👥 ***`Team`***
|
| 29 |
-
| Member |
|
| 30 |
-
| -------- |
|
| 31 |
-
| [Sebastian Wefers](https://huggingface.co/Basti-1995) |
|
| 32 |
-
| [Dmitri Moscoglo](https://huggingface.co/dimim) |
|
| 33 |
-
| [Owen Kaplinsky](https://huggingface.co/owenkaplinsky) |
|
| 34 |
-
| [SrikarMK](https://huggingface.co/Srikarmk) |
|
| 35 |
|
| 36 |
<details>
|
| 37 |
<summary><strong>📚 Table of Contents</strong> (click to expand)</summary>
|
|
@@ -61,14 +48,19 @@ tags:
|
|
| 61 |
|
| 62 |
## **Problem Statement**
|
| 63 |
|
| 64 |
-
Modern recruitment
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
|
| 66 |
-
|
| 67 |
-
* **Slow & Expensive**: Average time-to-hire is **44 days** [`1`], with costs reaching **$4,700 per hire** [`1`].
|
| 68 |
-
* **Inefficient Funnel**: While job posts attract hundreds of applicants, only **5%** complete the process [`1`], and **76%** of employers still struggle to find the right talent [`3`].
|
| 69 |
-
* **Burnout Risk**: **51%** of HR teams face high turnover risks [`2`], driven by the inability to scale manual screening against rising application volumes.
|
| 70 |
|
| 71 |
-
|
|
|
|
|
|
|
| 72 |
|
| 73 |
|
| 74 |
|
|
@@ -82,17 +74,37 @@ This agentic system automates high-volume screening tasks, allowing HR professio
|
|
| 82 |
|
| 83 |
4. [World Economic Forum — The Future of Jobs Report 2025](https://www.weforum.org/publications/the-future-of-jobs-report-2025/digest/)
|
| 84 |
|
| 85 |
-
|
| 86 |
## **Ethical & Regulatory Considerations**
|
| 87 |
|
| 88 |
-
This project
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 96 |
|
| 97 |
---
|
| 98 |
|
|
@@ -106,47 +118,110 @@ This project is an **experimental prototype** designed to demonstrate technical
|
|
| 106 |
|
| 107 |
8. [Clifford Chance — What Does the EU AI Act Mean for Employers?](https://www.cliffordchance.com/content/dam/cliffordchance/briefings/2024/08/what-does-the-eu-ai-act-mean-for-employers.pdf)
|
| 108 |
|
| 109 |
-
## **System Architecture**
|
| 110 |
|
| 111 |
-
1. **User Interfaces (Gradio)**: Serves both **HR Managers** (Supervisor Chat & Management) and **Candidates** (CV Upload & Voice Interface).
|
| 112 |
-
2. **Supervisor Agent**: The main planner that orchestrates the process by delegating to:
|
| 113 |
-
- **DB Executor**: Handles data queries/updates via code execution.
|
| 114 |
-
- **CV & Voice Screeners**: Specialized agents for assessment.
|
| 115 |
-
- **Gmail & Calendar Agents**: Manage communication and scheduling.
|
| 116 |
-
3. **MCP Servers**: Connect the Gmail and Calendar agents to external Google APIs.
|
| 117 |
-
4. **Database**: Central storage for candidate profiles and recruitment state.
|
| 118 |
|
| 119 |
-
|
|
|
|
| 120 |
|
| 121 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 122 |
|
| 123 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 124 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 125 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 126 |
|
| 127 |
### 1. The Recruitment Lifecycle
|
|
|
|
| 128 |
|
| 129 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 130 |
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
6. **Confirmation**: Interview is scheduled (`interview_scheduled`) upon candidate's response.
|
| 137 |
-
7. **Final Decision**: HR makes a decision (`hired` or `rejected`), and the outcome is communicated to the candidate.
|
| 138 |
|
| 139 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 140 |
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
|
|
|
|
|
|
|
|
|
| 146 |
|
| 147 |
-
|
| 148 |
|
| 149 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 150 |
|
| 151 |
---
|
| 152 |
|
|
@@ -156,7 +231,7 @@ The interaction between these entry points and the agentic workflow is visualize
|
|
| 156 |
|
| 157 |
To improve the reliability of complex evaluations (such as CV scoring and Voice Interview judging), we enforce **Chain-of-Thought (CoT)** reasoning within our structured outputs, inspired by [Wei et al. (2022)](https://arxiv.org/abs/2201.11903).
|
| 158 |
|
| 159 |
-
By requiring the model to generate a textual explanation *before* assigning numerical scores, we ensure the model "thinks" through the evidence before committing to a decision. This is implemented directly in our Pydantic schemas (e.g., `src/agents/cv_screening/schemas/output_schema.py`), where field order matters:
|
| 160 |
|
| 161 |
```mermaid
|
| 162 |
flowchart LR
|
|
@@ -287,14 +362,14 @@ A breakdown of the various LLMs, Agents, and Workflows powering the system.
|
|
| 287 |
|
| 288 |
| Component | Type | Model | Description | Location |
|
| 289 |
| :--- | :--- | :--- | :--- | :--- |
|
| 290 |
-
| **Supervisor Agent** | 🤖 **Agent** | `gpt-4o` | Orchestrates delegation, planning, and context management. | `src/agents/supervisor/supervisor_v2.py` |
|
| 291 |
-
| **Gmail Agent** | 🤖 **Agent** | `gpt-4o` | Autonomous email management via MCP (read/send/label). | `src/agents/gmail/gmail_agent.py` |
|
| 292 |
-
| **GCalendar Agent** | 🤖 **Agent** | `gpt-4o` | Autonomous calendar scheduling via MCP. | `src/agents/gcalendar/gcalendar_agent.py` |
|
| 293 |
-
| **DB Executor** | 🤖 **Agent** | `gpt-4o` | Writes SQL/Python to query the database (CodeAct). | `src/agents/db_executor/db_executor.py` |
|
| 294 |
-
| **CV Screening** | ⚙️ **Workflow** | `gpt-4o` | Deterministic pipeline: Fetch → Read → Evaluate → Save. | `src/agents/cv_screening/cv_screening_workflow.py` |
|
| 295 |
-
| **Voice Judge** | 🧠 **Simple LLM** | `gpt-4o-audio` | Evaluates audio/transcripts for sentiment & confidence. | `src/agents/voice_screening/judge.py` |
|
| 296 |
-
| **Doc Parser** | 🧠 **Simple LLM** | `gpt-4o-mini` | Vision-based PDF-to-Markdown conversion. | `src/doc_parser/pdf_to_markdown.py` |
|
| 297 |
-
| **History Manager** | 🧠 **Simple LLM** | `gpt-4o-mini` | Summarizes conversation history for context compaction. | `src/context_eng/history_manager.py` |
|
| 298 |
|
| 299 |
### 🔌 ***`Integrated MCP Servers`***
|
| 300 |
The system integrates **Model Context Protocol (MCP)** servers to securely and standardizedly connect agents to external tools.
|
|
@@ -316,3 +391,13 @@ This project utilizes code from:
|
|
| 316 |
*Integrated at:* `src/mcp_servers/calendar-mcp/`
|
| 317 |
|
| 318 |
We deeply acknowledge these original works and the great AI and Data Science community that makes such collaboration possible. We distribute our modifications under the compatible license terms.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
# ***`Recruitment Agent`***
|
| 2 |
<p align="left">
|
| 3 |
<img src="https://img.shields.io/badge/MCP%20Hackathon-Track%202%20%E2%80%94%20Enterprise-blue" />
|
|
|
|
| 11 |
<img src="https://img.shields.io/badge/Google%20Cloud-APIs%20%26%20MCP%20Tools-blue?logo=googlecloud" />
|
| 12 |
</p>
|
| 13 |
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
|
| 19 |
> This project was developed as part of the **[MCP 1st Birthday Hackathon](https://huggingface.co/MCP-1st-Birthday)** — submitted under
|
| 20 |
> **Track 2: MCP in Action (Enterprise)**, showcasing a real-world multi-agent application built on top of the Model Context Protocol.
|
| 21 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
|
| 23 |
<details>
|
| 24 |
<summary><strong>📚 Table of Contents</strong> (click to expand)</summary>
|
|
|
|
| 48 |
|
| 49 |
## **Problem Statement**
|
| 50 |
|
| 51 |
+
Modern recruitment processes remain **slow**, **resource-intensive**, and increasingly **unsustainable** for HR teams amid persistent talent shortages and evolving skill demands. Recent industry reports underscore structural bottlenecks that hinder efficient hiring.
|
| 52 |
+
|
| 53 |
+
High **applicant volumes overwhelm recruiters**, with a *typical job posting attracting hundreds of applications*, many *unqualified*, leading to administrative burdens and rushed evaluations. This results in *only about **5%** of viewers completing applications*, while teams waste time sifting through low-quality submissions. [`1`]
|
| 54 |
+
|
| 55 |
+
Screening and early-stage evaluation consume excessive recruiter time, with **35%** of their efforts dedicated to tasks like interview scheduling alone, exacerbating workload pressures. Talent acquisition leaders report unmanageable demands, with **27%** citing overload as a key issue, up from prior years. [`2`]
|
| 56 |
+
|
| 57 |
+
**Hiring timelines average 44 days across industries**, driven by skills mismatches and manual processes that delay filling critical roles. Globally, **76%** of employers struggle to fill positions due to talent gaps, particularly in tech and healthcare sectors. [`1`, `3`]
|
| 58 |
|
| 59 |
+
The financial toll is significant, with **average cost-per-hire reaching $4,700**, fueled by prolonged cycles, high turnover in recruitment teams (projected at **51%** as a top 2025 challenge), and inefficiencies in sourcing. [`1`, `2`]
|
|
|
|
|
|
|
|
|
|
| 60 |
|
| 61 |
+
HR professionals **face rising burnout** from these pressures, compounded by competition for diverse talent and the **need for more touchpoints in hiring**, which **45%** of leaders say adds complexity. Skills shortages, cited by **63%** of employers as the primary barrier to growth, further strain teams. [`2`, `4`]
|
| 62 |
+
|
| 63 |
+
These challenges reveal that **traditional manual recruitment fails to scale** in a competitive 2025 landscape. An AI-driven recruitment agent can alleviate bottlenecks by automating screening, accelerating timelines, enhancing consistency, and allowing HR to prioritize strategic decisions over repetitive tasks.
|
| 64 |
|
| 65 |
|
| 66 |
|
|
|
|
| 74 |
|
| 75 |
4. [World Economic Forum — The Future of Jobs Report 2025](https://www.weforum.org/publications/the-future-of-jobs-report-2025/digest/)
|
| 76 |
|
|
|
|
| 77 |
## **Ethical & Regulatory Considerations**
|
| 78 |
|
| 79 |
+
This project was developed as an **experimental prototype for a hackathon**, designed to showcase how language-model agents can automate structured workflows. It is **not intended for production deployment** as an autonomous hiring system. Because it touches on the automated assessment of humans, it must be approached with caution and interpreted within the correct ethical and regulatory context.
|
| 80 |
+
|
| 81 |
+
The risks of algorithmic profiling have been widely documented, most notably during the **Cambridge Analytica scandal**, where data from millions of users was harvested and used for psychographic targeting without consent. This episode demonstrated how data-driven models can be leveraged to manipulate individuals when used irresponsibly, and it significantly shaped today’s regulatory landscape. [`5`]
|
| 82 |
+
|
| 83 |
+
Given this history, any system that evaluates or ranks people—particularly in employment—must uphold **strict transparency, human oversight, and narrow scope**. In this prototype, all AI outputs are intended purely as **assistive signals**. The system must **never** be used to autonomously approve, reject, or shortlist candidates.
|
| 84 |
+
|
| 85 |
+
The **EU AI Act** classifies AI systems used for recruitment, CV screening, candidate ranking, promotion decisions, or termination as **High-Risk AI Systems** (Annex III). Such systems are permitted in the EU but must meet stringent requirements, including:
|
| 86 |
+
|
| 87 |
+
- **Human oversight** with the ability to override AI suggestions
|
| 88 |
+
- **Transparency** about the model’s role and limitations
|
| 89 |
+
- **Detailed logging and traceability** of system behavior
|
| 90 |
+
- **Bias monitoring and risk management**
|
| 91 |
+
- **High-quality and relevant training data**
|
| 92 |
+
- **Clear separation** between AI scoring and final human judgment
|
| 93 |
+
|
| 94 |
+
The Act also **prohibits** certain practices in hiring, such as emotion recognition in workplace settings, biometric inference of personality traits, and social-scoring-style ranking systems. [`6`, `7`, `8`]
|
| 95 |
|
| 96 |
+
This prototype **does not** conduct emotion recognition, sensitive-trait inference, biometric profiling, or psychographic prediction. It is a technical experiment focused on agent orchestration, workflow automation, and context management—not an end-to-end HR decision engine.
|
| 97 |
+
|
| 98 |
+
### **Human-in-the-Loop by Design**
|
| 99 |
+
To remain aligned with ethical expectations and regulatory requirements, this system must always operate with:
|
| 100 |
+
|
| 101 |
+
- **Human-in-the-Loop (HITL):** Recruiters make all decisions.
|
| 102 |
+
- **Explainability:** Agents produce structured rationales, not black-box judgments.
|
| 103 |
+
- **Data minimization:** Only job-relevant information is processed.
|
| 104 |
+
- **No profiling of protected traits:** No biometric, psychographic, or emotional inference.
|
| 105 |
+
|
| 106 |
+
### **Project Status**
|
| 107 |
+
This project remains a **research and demonstration artifact**, created to explore the technical viability of LLM-powered coordination between agents. It highlights what is technologically possible, but is **not a deployable HR solution** under the EU AI Act. Any real-world implementation would require extensive risk assessment, compliance measures, and human oversight to avoid replicating the harms demonstrated in past profiling scandals.
|
| 108 |
|
| 109 |
---
|
| 110 |
|
|
|
|
| 118 |
|
| 119 |
8. [Clifford Chance — What Does the EU AI Act Mean for Employers?](https://www.cliffordchance.com/content/dam/cliffordchance/briefings/2024/08/what-does-the-eu-ai-act-mean-for-employers.pdf)
|
| 120 |
|
|
|
|
| 121 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 122 |
|
| 123 |
+
## ***`Quick Start: Run Application`***
|
| 124 |
+
To spin up the entire platform including the database, agents, and UI dashboards, we use **Docker Compose**.
|
| 125 |
|
| 126 |
+
### ***Services & Ports***
|
| 127 |
+
| Service | Description | Host Port | Container Port |
|
| 128 |
+
|---------|-------------|-----------|----------------|
|
| 129 |
+
| `db` | PostgreSQL 15 database with persistent storage | **5433** | 5432 |
|
| 130 |
+
| `cv_upload_streamlit` | UI for uploading CVs | **8501** | 8501 |
|
| 131 |
+
| `voice_screening_streamlit` | UI for voice screening candidates | **8502** | 8501 |
|
| 132 |
+
| `supervisor_ui` | Main Chat UI for the Supervisor Agent | **8503** | 8501 |
|
| 133 |
+
| `websocket_proxy` | Proxy for OpenAI Realtime API | **8000** | 8000 |
|
| 134 |
|
| 135 |
+
### ***Infrastructure & Secrets***
|
| 136 |
+
This project requires Google Cloud credentials for the Gmail and Calendar agents.
|
| 137 |
+
|
| 138 |
+
- **Secrets:** Google tokens and credentials must be present in the `secrets/` directory.
|
| 139 |
+
- **Infrastructure:** You can provision the necessary GCP infrastructure using the code in `terraform/` or the scripts in `scripts/infra/`.
|
| 140 |
+
- **Documentation:** For detailed setup instructions, refer to the [MCP Docs](docs/mcp/).
|
| 141 |
|
| 142 |
+
### ***Run Command***
|
| 143 |
+
1. **Configure Environment:**
|
| 144 |
+
Copy the example environment file and fill in your API keys:
|
| 145 |
+
```bash
|
| 146 |
+
cp .env.example .env
|
| 147 |
+
```
|
| 148 |
|
| 149 |
+
2. **Start Services:**
|
| 150 |
+
```bash
|
| 151 |
+
docker compose --env-file .env -f docker/docker-compose.yml up --build
|
| 152 |
+
```
|
| 153 |
+
|
| 154 |
+
### 🧹 Resetting the Environment
|
| 155 |
+
If you need a clean slate (e.g., after modifying DB models):
|
| 156 |
+
```bash
|
| 157 |
+
# 1. Stop containers
|
| 158 |
+
docker compose -f docker/docker-compose.yml down
|
| 159 |
+
|
| 160 |
+
# 2. Remove persistent DB volume
|
| 161 |
+
docker volume rm docker_postgres_data
|
| 162 |
+
|
| 163 |
+
# 3. Rebuild & Start
|
| 164 |
+
docker compose --env-file .env -f docker/docker-compose.yml up --build
|
| 165 |
+
```
|
| 166 |
+
|
| 167 |
+
---
|
| 168 |
+
|
| 169 |
+
## ***`Application Flow & Entry Points`***
|
| 170 |
+
|
| 171 |
+
The platform orchestrates a complete recruitment pipeline, interacting with both Candidates and the HR Supervisor.
|
| 172 |
|
| 173 |
### 1. The Recruitment Lifecycle
|
| 174 |
+
The system tracks candidates through a defined state machine (see `src/backend/state/candidate.py` for the `CandidateStatus` enum).
|
| 175 |
|
| 176 |
+
```mermaid
|
| 177 |
+
graph TD
|
| 178 |
+
%% Actors
|
| 179 |
+
Candidate((Candidate))
|
| 180 |
+
HR((HR Supervisor))
|
| 181 |
+
|
| 182 |
+
%% System Components (Nodes)
|
| 183 |
+
CV_UI[CV Portal UI]
|
| 184 |
+
CV_Screen{CV Screening AI}
|
| 185 |
+
Voice_UI[Voice Portal UI]
|
| 186 |
+
Voice_Judge{Voice Judge AI}
|
| 187 |
+
Interview[Person-to-Person Interview]
|
| 188 |
+
Decision{Final Decision}
|
| 189 |
+
|
| 190 |
+
%% Flow & Actions (Edges)
|
| 191 |
+
Candidate -->|1. Uploads CV| CV_UI
|
| 192 |
+
CV_UI -->|2. Triggers Analysis| CV_Screen
|
| 193 |
+
|
| 194 |
+
CV_Screen -->|Pass: Sends Invite| Voice_UI
|
| 195 |
+
CV_Screen -->|Fail: Notify| Rejected((Rejected))
|
| 196 |
|
| 197 |
+
Voice_UI -->|3. Conducts Interview| Candidate
|
| 198 |
+
Candidate -->|4. Completes Session| Voice_Judge
|
| 199 |
+
|
| 200 |
+
Voice_Judge -->|Pass: Schedule| Interview
|
| 201 |
+
Voice_Judge -->|Fail: Notify| Rejected
|
|
|
|
|
|
|
| 202 |
|
| 203 |
+
Interview -->|5. Feedback| HR
|
| 204 |
+
HR -->|6. Updates Status| Decision
|
| 205 |
+
|
| 206 |
+
Decision -->|Hire| Hired((Hired))
|
| 207 |
+
Decision -->|Reject| Rejected
|
| 208 |
|
| 209 |
+
%% Styling
|
| 210 |
+
style CV_UI fill:#e3f2fd,stroke:#1565c0
|
| 211 |
+
style Voice_UI fill:#e3f2fd,stroke:#1565c0
|
| 212 |
+
style CV_Screen fill:#fff3e0,stroke:#ef6c00
|
| 213 |
+
style Voice_Judge fill:#fff3e0,stroke:#ef6c00
|
| 214 |
+
style Interview fill:#e8f5e9,stroke:#2e7d32
|
| 215 |
+
style Decision fill:#f3e5f5,stroke:#7b1fa2
|
| 216 |
+
```
|
| 217 |
|
| 218 |
+
### 2. User Entry Points
|
| 219 |
|
| 220 |
+
| User | Interface | Port | Description |
|
| 221 |
+
| :--- | :--- | :--- | :--- |
|
| 222 |
+
| **HR Manager** | **Supervisor UI** | `8503` | **The Command Center.** Chat with the Supervisor Agent to manage the pipeline, review candidates, query the DB, and schedule interviews. |
|
| 223 |
+
| **Candidate** | **CV Portal** | `8501` | Public-facing portal for candidates to register and upload their resumes to the system. |
|
| 224 |
+
| **Candidate** | **Voice Portal** | `8502` | AI-conducted voice interview interface. Candidates access this only after passing CV screening and receiving an invite. |
|
| 225 |
|
| 226 |
---
|
| 227 |
|
|
|
|
| 231 |
|
| 232 |
To improve the reliability of complex evaluations (such as CV scoring and Voice Interview judging), we enforce **Chain-of-Thought (CoT)** reasoning within our structured outputs, inspired by [Wei et al. (2022)](https://arxiv.org/abs/2201.11903).
|
| 233 |
|
| 234 |
+
By requiring the model to generate a textual explanation *before* assigning numerical scores, we ensure the model "thinks" through the evidence before committing to a decision. This is implemented directly in our Pydantic schemas (e.g., `src/backend/agents/cv_screening/schemas/output_schema.py`), where field order matters:
|
| 235 |
|
| 236 |
```mermaid
|
| 237 |
flowchart LR
|
|
|
|
| 362 |
|
| 363 |
| Component | Type | Model | Description | Location |
|
| 364 |
| :--- | :--- | :--- | :--- | :--- |
|
| 365 |
+
| **Supervisor Agent** | 🤖 **Agent** | `gpt-4o` | Orchestrates delegation, planning, and context management. | `src/backend/agents/supervisor/supervisor_v2.py` |
|
| 366 |
+
| **Gmail Agent** | 🤖 **Agent** | `gpt-4o` | Autonomous email management via MCP (read/send/label). | `src/backend/agents/gmail/gmail_agent.py` |
|
| 367 |
+
| **GCalendar Agent** | 🤖 **Agent** | `gpt-4o` | Autonomous calendar scheduling via MCP. | `src/backend/agents/gcalendar/gcalendar_agent.py` |
|
| 368 |
+
| **DB Executor** | 🤖 **Agent** | `gpt-4o` | Writes SQL/Python to query the database (CodeAct). | `src/backend/agents/db_executor/db_executor.py` |
|
| 369 |
+
| **CV Screening** | ⚙️ **Workflow** | `gpt-4o` | Deterministic pipeline: Fetch → Read → Evaluate → Save. | `src/backend/agents/cv_screening/cv_screening_workflow.py` |
|
| 370 |
+
| **Voice Judge** | 🧠 **Simple LLM** | `gpt-4o-audio` | Evaluates audio/transcripts for sentiment & confidence. | `src/backend/agents/voice_screening/judge.py` |
|
| 371 |
+
| **Doc Parser** | 🧠 **Simple LLM** | `gpt-4o-mini` | Vision-based PDF-to-Markdown conversion. | `src/backend/doc_parser/pdf_to_markdown.py` |
|
| 372 |
+
| **History Manager** | 🧠 **Simple LLM** | `gpt-4o-mini` | Summarizes conversation history for context compaction. | `src/backend/context_eng/history_manager.py` |
|
| 373 |
|
| 374 |
### 🔌 ***`Integrated MCP Servers`***
|
| 375 |
The system integrates **Model Context Protocol (MCP)** servers to securely and standardizedly connect agents to external tools.
|
|
|
|
| 391 |
*Integrated at:* `src/mcp_servers/calendar-mcp/`
|
| 392 |
|
| 393 |
We deeply acknowledge these original works and the great AI and Data Science community that makes such collaboration possible. We distribute our modifications under the compatible license terms.
|
| 394 |
+
|
| 395 |
+
---
|
| 396 |
+
|
| 397 |
+
## 👥 ***`Team`***
|
| 398 |
+
| Member |
|
| 399 |
+
| -------- |
|
| 400 |
+
| [Sebastian Wefers](https://github.com/Ocean-code-1995) |
|
| 401 |
+
| [Dmitri Moscoglo](https://github.com/DimiM99) |
|
| 402 |
+
| [Owen Kaplinsky](https://github.com/owenkaplinsky) |
|
| 403 |
+
| [SrikarMK](https://github.com/Srikarmk) |
|
docker/Dockerfile.candidates_db_init
CHANGED
|
@@ -15,8 +15,10 @@ COPY ../requirements/base.txt ./requirements/base.txt
|
|
| 15 |
COPY ../requirements/db.txt ./requirements/db.txt
|
| 16 |
RUN pip install --no-cache-dir -r requirements/db.txt
|
| 17 |
|
| 18 |
-
# Copy
|
| 19 |
-
COPY src/database/candidates ./src/database/candidates
|
|
|
|
|
|
|
| 20 |
|
| 21 |
# Default command - use dedicated init script to avoid circular import
|
| 22 |
-
CMD ["python", "-m", "src.database.candidates.init_db"]
|
|
|
|
| 15 |
COPY ../requirements/db.txt ./requirements/db.txt
|
| 16 |
RUN pip install --no-cache-dir -r requirements/db.txt
|
| 17 |
|
| 18 |
+
# Copy required source modules
|
| 19 |
+
COPY src/backend/database/candidates ./src/backend/database/candidates
|
| 20 |
+
COPY src/backend/state ./src/backend/state
|
| 21 |
+
COPY src/backend/configs ./src/backend/configs
|
| 22 |
|
| 23 |
# Default command - use dedicated init script to avoid circular import
|
| 24 |
+
CMD ["python", "-m", "src.backend.database.candidates.init_db"]
|
docker/Dockerfile.supervisor_api
CHANGED
|
@@ -39,5 +39,5 @@ COPY .env /app/.env
|
|
| 39 |
EXPOSE 8080
|
| 40 |
|
| 41 |
# Run FastAPI with uvicorn
|
| 42 |
-
CMD ["uvicorn", "src.api.app:app", "--host", "0.0.0.0", "--port", "8080"]
|
| 43 |
|
|
|
|
| 39 |
EXPOSE 8080
|
| 40 |
|
| 41 |
# Run FastAPI with uvicorn
|
| 42 |
+
CMD ["uvicorn", "src.backend.api.app:app", "--host", "0.0.0.0", "--port", "8080"]
|
| 43 |
|
docker/docker-compose.yml
CHANGED
|
@@ -19,6 +19,10 @@ services:
|
|
| 19 |
interval: 3s
|
| 20 |
timeout: 3s
|
| 21 |
retries: 5
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
environment:
|
| 23 |
POSTGRES_HOST: ${POSTGRES_HOST}
|
| 24 |
POSTGRES_PORT: ${POSTGRES_PORT}
|
|
@@ -34,18 +38,23 @@ services:
|
|
| 34 |
# Initializes the database or starts the API (depending on command).
|
| 35 |
container_name: candidates_db_init
|
| 36 |
build:
|
| 37 |
-
context: ..
|
| 38 |
dockerfile: docker/Dockerfile.candidates_db_init
|
| 39 |
depends_on:
|
| 40 |
db:
|
| 41 |
condition: service_healthy
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
environment:
|
| 43 |
-
POSTGRES_HOST
|
| 44 |
-
|
|
|
|
| 45 |
POSTGRES_USER: ${POSTGRES_USER}
|
| 46 |
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
| 47 |
POSTGRES_DB: ${POSTGRES_DB}
|
| 48 |
-
|
| 49 |
|
| 50 |
volumes:
|
| 51 |
# --- Local code mount (for development only) ---
|
|
@@ -53,7 +62,7 @@ services:
|
|
| 53 |
# into the container at /app.
|
| 54 |
# ✅ Enables live code changes without rebuilding the image.
|
| 55 |
# ⚠️ Do NOT use in production – overrides the built image code.
|
| 56 |
-
- ../:/app
|
| 57 |
|
| 58 |
networks:
|
| 59 |
- hrnet
|
|
@@ -69,15 +78,17 @@ services:
|
|
| 69 |
depends_on:
|
| 70 |
- db
|
| 71 |
- supervisor_api
|
|
|
|
|
|
|
| 72 |
environment:
|
| 73 |
# Database connection
|
| 74 |
-
POSTGRES_HOST:
|
| 75 |
-
POSTGRES_PORT:
|
| 76 |
POSTGRES_USER: ${POSTGRES_USER}
|
| 77 |
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
| 78 |
POSTGRES_DB: ${POSTGRES_DB}
|
| 79 |
-
DATABASE_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}
|
| 80 |
-
CV_UPLOAD_PATH: /app/src/database/cvs/uploads
|
| 81 |
# App specific
|
| 82 |
CV_UPLOAD_API_URL: http://supervisor_api:8080/api/v1/cv
|
| 83 |
PYTHONPATH: /app
|
|
@@ -85,15 +96,8 @@ services:
|
|
| 85 |
# Mount local code for live updates
|
| 86 |
- ../:/app
|
| 87 |
# Shared volume for CV uploads (persistent)
|
| 88 |
-
- ../src/database/cvs:/app/src/database/cvs
|
| 89 |
-
command:
|
| 90 |
-
[
|
| 91 |
-
"streamlit",
|
| 92 |
-
"run",
|
| 93 |
-
"src/frontend/streamlit/cv_ui/app.py",
|
| 94 |
-
"--server.port=8501",
|
| 95 |
-
"--server.address=0.0.0.0",
|
| 96 |
-
]
|
| 97 |
networks:
|
| 98 |
- hrnet
|
| 99 |
|
|
@@ -105,6 +109,8 @@ services:
|
|
| 105 |
dockerfile: docker/Dockerfile.voice_proxy
|
| 106 |
ports:
|
| 107 |
- "8000:8000"
|
|
|
|
|
|
|
| 108 |
depends_on:
|
| 109 |
- db
|
| 110 |
- candidates_db_init
|
|
@@ -112,20 +118,16 @@ services:
|
|
| 112 |
PYTHONPATH: /app
|
| 113 |
OPENAI_API_KEY: ${OPENAI_API_KEY}
|
| 114 |
BACKEND_API_URL: http://supervisor_api:8080
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 115 |
volumes:
|
| 116 |
# Mount local code for live updates
|
| 117 |
- ../:/app
|
| 118 |
-
command:
|
| 119 |
-
[
|
| 120 |
-
"python",
|
| 121 |
-
"-m",
|
| 122 |
-
"uvicorn",
|
| 123 |
-
"src.frontend.streamlit.voice_screening_ui.proxy:app",
|
| 124 |
-
"--host",
|
| 125 |
-
"0.0.0.0",
|
| 126 |
-
"--port",
|
| 127 |
-
"8000",
|
| 128 |
-
]
|
| 129 |
networks:
|
| 130 |
- hrnet
|
| 131 |
|
|
@@ -136,10 +138,12 @@ services:
|
|
| 136 |
context: ..
|
| 137 |
dockerfile: docker/Dockerfile.voice_screening
|
| 138 |
ports:
|
| 139 |
-
- "8502:8501"
|
| 140 |
depends_on:
|
| 141 |
- db
|
| 142 |
- websocket_proxy
|
|
|
|
|
|
|
| 143 |
environment:
|
| 144 |
DATABASE_URL: postgresql://agentic_user:password123@db:5432/agentic_hr
|
| 145 |
PYTHONPATH: /app
|
|
@@ -148,14 +152,7 @@ services:
|
|
| 148 |
volumes:
|
| 149 |
# Mount local code for live updates
|
| 150 |
- ../:/app
|
| 151 |
-
command:
|
| 152 |
-
[
|
| 153 |
-
"streamlit",
|
| 154 |
-
"run",
|
| 155 |
-
"src/frontend/streamlit/voice_screening_ui/app.py",
|
| 156 |
-
"--server.port=8501",
|
| 157 |
-
"--server.address=0.0.0.0",
|
| 158 |
-
]
|
| 159 |
networks:
|
| 160 |
- hrnet
|
| 161 |
|
|
@@ -166,13 +163,15 @@ services:
|
|
| 166 |
context: ..
|
| 167 |
dockerfile: docker/Dockerfile.supervisor_api
|
| 168 |
ports:
|
| 169 |
-
- "8080:8080"
|
| 170 |
depends_on:
|
| 171 |
- db
|
|
|
|
|
|
|
| 172 |
environment:
|
| 173 |
# We set POSTGRES_HOST to 'db' so the agent connects to the container internal network
|
| 174 |
-
POSTGRES_HOST:
|
| 175 |
-
POSTGRES_PORT:
|
| 176 |
POSTGRES_USER: ${POSTGRES_USER}
|
| 177 |
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
| 178 |
POSTGRES_DB: ${POSTGRES_DB}
|
|
@@ -180,19 +179,12 @@ services:
|
|
| 180 |
PROMPTLAYER_API_KEY: ${PROMPTLAYER_API_KEY}
|
| 181 |
OPENAI_API_KEY: ${OPENAI_API_KEY}
|
| 182 |
WEBSOCKET_PROXY_URL: ws://websocket_proxy:8000/ws/realtime
|
|
|
|
|
|
|
| 183 |
volumes:
|
| 184 |
# Mount local code for live updates
|
| 185 |
- ../:/app
|
| 186 |
-
command:
|
| 187 |
-
[
|
| 188 |
-
"uvicorn",
|
| 189 |
-
"src.api.app:app",
|
| 190 |
-
"--host",
|
| 191 |
-
"0.0.0.0",
|
| 192 |
-
"--port",
|
| 193 |
-
"8080",
|
| 194 |
-
"--reload",
|
| 195 |
-
]
|
| 196 |
networks:
|
| 197 |
- hrnet
|
| 198 |
|
|
@@ -203,10 +195,12 @@ services:
|
|
| 203 |
context: ..
|
| 204 |
dockerfile: docker/Dockerfile.supervisor
|
| 205 |
ports:
|
| 206 |
-
- "8503:8501"
|
| 207 |
depends_on:
|
| 208 |
- db
|
| 209 |
- supervisor_api
|
|
|
|
|
|
|
| 210 |
environment:
|
| 211 |
# We set POSTGRES_HOST to 'db' so the agent connects to the container internal network
|
| 212 |
PYTHONPATH: /app
|
|
|
|
| 19 |
interval: 3s
|
| 20 |
timeout: 3s
|
| 21 |
retries: 5
|
| 22 |
+
# Hey compose here is env file,
|
| 23 |
+
# pass it to container, but not the .env itself
|
| 24 |
+
env_file:
|
| 25 |
+
- ../.env
|
| 26 |
environment:
|
| 27 |
POSTGRES_HOST: ${POSTGRES_HOST}
|
| 28 |
POSTGRES_PORT: ${POSTGRES_PORT}
|
|
|
|
| 38 |
# Initializes the database or starts the API (depending on command).
|
| 39 |
container_name: candidates_db_init
|
| 40 |
build:
|
| 41 |
+
context: .. # build from the project root
|
| 42 |
dockerfile: docker/Dockerfile.candidates_db_init
|
| 43 |
depends_on:
|
| 44 |
db:
|
| 45 |
condition: service_healthy
|
| 46 |
+
# Hey compose here is env file,
|
| 47 |
+
# pass it to container, but not the .env itself
|
| 48 |
+
env_file:
|
| 49 |
+
- ../.env
|
| 50 |
environment:
|
| 51 |
+
# Explicitly set POSTGRES_HOST to the service name 'db' for Docker networking
|
| 52 |
+
POSTGRES_HOST: db
|
| 53 |
+
POSTGRES_PORT: 5432
|
| 54 |
POSTGRES_USER: ${POSTGRES_USER}
|
| 55 |
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
| 56 |
POSTGRES_DB: ${POSTGRES_DB}
|
| 57 |
+
command: ["python", "-m", "src.backend.database.candidates.init_db"]
|
| 58 |
|
| 59 |
volumes:
|
| 60 |
# --- Local code mount (for development only) ---
|
|
|
|
| 62 |
# into the container at /app.
|
| 63 |
# ✅ Enables live code changes without rebuilding the image.
|
| 64 |
# ⚠️ Do NOT use in production – overrides the built image code.
|
| 65 |
+
- ../:/app # optional: live reload for local dev
|
| 66 |
|
| 67 |
networks:
|
| 68 |
- hrnet
|
|
|
|
| 78 |
depends_on:
|
| 79 |
- db
|
| 80 |
- supervisor_api
|
| 81 |
+
env_file:
|
| 82 |
+
- ../.env
|
| 83 |
environment:
|
| 84 |
# Database connection
|
| 85 |
+
POSTGRES_HOST: db
|
| 86 |
+
POSTGRES_PORT: 5432
|
| 87 |
POSTGRES_USER: ${POSTGRES_USER}
|
| 88 |
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
| 89 |
POSTGRES_DB: ${POSTGRES_DB}
|
| 90 |
+
DATABASE_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
|
| 91 |
+
CV_UPLOAD_PATH: /app/src/backend/database/cvs/uploads
|
| 92 |
# App specific
|
| 93 |
CV_UPLOAD_API_URL: http://supervisor_api:8080/api/v1/cv
|
| 94 |
PYTHONPATH: /app
|
|
|
|
| 96 |
# Mount local code for live updates
|
| 97 |
- ../:/app
|
| 98 |
# Shared volume for CV uploads (persistent)
|
| 99 |
+
- ../src/backend/database/cvs:/app/src/backend/database/cvs
|
| 100 |
+
command: ["streamlit", "run", "src/frontend/streamlit/cv_ui/app.py", "--server.port=8501", "--server.address=0.0.0.0"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 101 |
networks:
|
| 102 |
- hrnet
|
| 103 |
|
|
|
|
| 109 |
dockerfile: docker/Dockerfile.voice_proxy
|
| 110 |
ports:
|
| 111 |
- "8000:8000"
|
| 112 |
+
env_file:
|
| 113 |
+
- ../.env
|
| 114 |
depends_on:
|
| 115 |
- db
|
| 116 |
- candidates_db_init
|
|
|
|
| 118 |
PYTHONPATH: /app
|
| 119 |
OPENAI_API_KEY: ${OPENAI_API_KEY}
|
| 120 |
BACKEND_API_URL: http://supervisor_api:8080
|
| 121 |
+
# Database connection
|
| 122 |
+
POSTGRES_HOST: db
|
| 123 |
+
POSTGRES_PORT: 5432
|
| 124 |
+
POSTGRES_USER: ${POSTGRES_USER}
|
| 125 |
+
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
| 126 |
+
POSTGRES_DB: ${POSTGRES_DB}
|
| 127 |
volumes:
|
| 128 |
# Mount local code for live updates
|
| 129 |
- ../:/app
|
| 130 |
+
command: ["python", "-m", "uvicorn", "src.frontend.streamlit.voice_screening_ui.proxy:app", "--host", "0.0.0.0", "--port", "8000"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 131 |
networks:
|
| 132 |
- hrnet
|
| 133 |
|
|
|
|
| 138 |
context: ..
|
| 139 |
dockerfile: docker/Dockerfile.voice_screening
|
| 140 |
ports:
|
| 141 |
+
- "8502:8501" # Map host port 8502 to container port 8501
|
| 142 |
depends_on:
|
| 143 |
- db
|
| 144 |
- websocket_proxy
|
| 145 |
+
env_file:
|
| 146 |
+
- ../.env
|
| 147 |
environment:
|
| 148 |
DATABASE_URL: postgresql://agentic_user:password123@db:5432/agentic_hr
|
| 149 |
PYTHONPATH: /app
|
|
|
|
| 152 |
volumes:
|
| 153 |
# Mount local code for live updates
|
| 154 |
- ../:/app
|
| 155 |
+
command: ["streamlit", "run", "src/frontend/streamlit/voice_screening_ui/app.py", "--server.port=8501", "--server.address=0.0.0.0"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 156 |
networks:
|
| 157 |
- hrnet
|
| 158 |
|
|
|
|
| 163 |
context: ..
|
| 164 |
dockerfile: docker/Dockerfile.supervisor_api
|
| 165 |
ports:
|
| 166 |
+
- "8080:8080" # Map host port 8080 to container port 8080
|
| 167 |
depends_on:
|
| 168 |
- db
|
| 169 |
+
env_file:
|
| 170 |
+
- ../.env
|
| 171 |
environment:
|
| 172 |
# We set POSTGRES_HOST to 'db' so the agent connects to the container internal network
|
| 173 |
+
POSTGRES_HOST: db
|
| 174 |
+
POSTGRES_PORT: 5432
|
| 175 |
POSTGRES_USER: ${POSTGRES_USER}
|
| 176 |
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
| 177 |
POSTGRES_DB: ${POSTGRES_DB}
|
|
|
|
| 179 |
PROMPTLAYER_API_KEY: ${PROMPTLAYER_API_KEY}
|
| 180 |
OPENAI_API_KEY: ${OPENAI_API_KEY}
|
| 181 |
WEBSOCKET_PROXY_URL: ws://websocket_proxy:8000/ws/realtime
|
| 182 |
+
CV_UPLOAD_PATH: /app/src/backend/database/cvs/uploads
|
| 183 |
+
CV_PARSED_PATH: /app/src/backend/database/cvs/parsed
|
| 184 |
volumes:
|
| 185 |
# Mount local code for live updates
|
| 186 |
- ../:/app
|
| 187 |
+
command: ["uvicorn", "src.backend.api.app:app", "--host", "0.0.0.0", "--port", "8080", "--reload"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 188 |
networks:
|
| 189 |
- hrnet
|
| 190 |
|
|
|
|
| 195 |
context: ..
|
| 196 |
dockerfile: docker/Dockerfile.supervisor
|
| 197 |
ports:
|
| 198 |
+
- "8503:8501" # Map host port 8503 to container port 8501
|
| 199 |
depends_on:
|
| 200 |
- db
|
| 201 |
- supervisor_api
|
| 202 |
+
env_file:
|
| 203 |
+
- ../.env
|
| 204 |
environment:
|
| 205 |
# We set POSTGRES_HOST to 'db' so the agent connects to the container internal network
|
| 206 |
PYTHONPATH: /app
|
docker/info.md
CHANGED
|
@@ -20,6 +20,7 @@
|
|
| 20 |
docker compose --env-file .env -f docker/docker-compose.yml up --build
|
| 21 |
```
|
| 22 |
|
|
|
|
| 23 |
---
|
| 24 |
|
| 25 |
### Resetting the Environment
|
|
@@ -30,11 +31,14 @@ To completely reset the environment and database:
|
|
| 30 |
|
| 31 |
```bash
|
| 32 |
# 1. Stop running containers
|
| 33 |
-
docker compose -f docker/docker-compose.yml down
|
| 34 |
|
| 35 |
# 2. Remove the persistent database volume
|
| 36 |
docker volume rm docker_postgres_data
|
| 37 |
|
| 38 |
-
# 3.
|
|
|
|
|
|
|
|
|
|
| 39 |
docker compose --env-file .env -f docker/docker-compose.yml up --build
|
| 40 |
```
|
|
|
|
| 20 |
docker compose --env-file .env -f docker/docker-compose.yml up --build
|
| 21 |
```
|
| 22 |
|
| 23 |
+
|
| 24 |
---
|
| 25 |
|
| 26 |
### Resetting the Environment
|
|
|
|
| 31 |
|
| 32 |
```bash
|
| 33 |
# 1. Stop running containers
|
| 34 |
+
docker compose -f docker/docker-compose.yml down --remove-orphans
|
| 35 |
|
| 36 |
# 2. Remove the persistent database volume
|
| 37 |
docker volume rm docker_postgres_data
|
| 38 |
|
| 39 |
+
# 3. Prune unused Docker resources (optional but recommended)
|
| 40 |
+
docker system prune -f
|
| 41 |
+
|
| 42 |
+
# 4. Rebuild and start fresh
|
| 43 |
docker compose --env-file .env -f docker/docker-compose.yml up --build
|
| 44 |
```
|
docs/intro.md
ADDED
|
@@ -0,0 +1,457 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ***`Gradio Agents & MCP Hackathon Winter Edition 2025`***
|
| 2 |
+
|
| 3 |
+
## 🏁 Overview
|
| 4 |
+
This repository hosts our team's submission for **Track 2: MCP in Action** in the [MCP's 1st Birthday Hackathon](https://huggingface.co/MCP-1st-Birthday).
|
| 5 |
+
|
| 6 |
+
Our goal is to build an **autonomous agentic system** that demonstrates:
|
| 7 |
+
- **Planning, reasoning, and execution**
|
| 8 |
+
- Integration of **custom tools, MCP tools, or external APIs**
|
| 9 |
+
- Effective **context engineering**
|
| 10 |
+
- Clear, practical **user value**
|
| 11 |
+
|
| 12 |
+
We'll use **LangGraph** as our orchestration backbone for building multi-turn, tool-using, and context-aware agents.
|
| 13 |
+
|
| 14 |
+
> ***`Check hackathon README for detilaed requirements.`***
|
| 15 |
+
|
| 16 |
+
## 🧠 ***`Tools & Frameworks`***
|
| 17 |
+
|
| 18 |
+
- 🧩 [LangGraph](https://docs.langchain.com/oss/python/langgraph/overview): for multi-agent orchestration and planning
|
| 19 |
+
- Why & how they built [LangGraph for production agents](https://blog.langchain.com/building-langgraph/)
|
| 20 |
+
- 🧠 **LLM Engines:** [OpenAI](https://openai.com) / [Anthropic](https://www.anthropic.com) — reasoning and planning models
|
| 21 |
+
- gpt-oss inference providers
|
| 22 |
+
- [Open Router](https://openrouter.ai/openai/gpt-oss-20b):
|
| 23 |
+
- LangChain Wrapper: https://github.com/langchain-ai/langchain/discussions/27964
|
| 24 |
+
- [TogetherAI](https://www.together.ai/openai)
|
| 25 |
+
- 💬 [Gradio](https://www.gradio.app/): for the UI and context-engineering demos
|
| 26 |
+
- ⚙️ [MCP](https://modelcontextprotocol.io/docs/getting-started/intro) Tools: standardized interfaces for Gmail, Google Calendar, Voice technologies and other APIs
|
| 27 |
+
- ☁️ [Google Cloud Platform](https://cloud.google.com): optional backend for hosting MCP servers and integrated services
|
| 28 |
+
- 📞 [Twilio](https://www.twilio.com/en-us): enables automated voice calls and candidate interactions
|
| 29 |
+
- 🔊 [ElevenLabs](https://elevenlabs.io): (optional) natural text-to-speech for realistic voice screenings
|
| 30 |
+
- 🎙️ [Whisper-based Transcription API](https://whisperapi.com) (or [OpenAI Whisper API](https://platform.openai.com/docs/guides/speech-to-text) ) — for speech-to-text functionality in voice interviews
|
| 31 |
+
- 🧭 [Langfuse](https://langfuse.com) or [LangSmith](https://docs.langchain.com/langsmith/quick-start-studio): debugging, observability, and trace visualization
|
| 32 |
+
- 📄 [Docling](https://www.docling.ai): for parsing and analyzing uploaded CV documents
|
| 33 |
+
- 🧱 [Pydantic](https://docs.pydantic.dev/latest/): for structured outputs and data validation
|
| 34 |
+
- 🔀 [Parlant](https://github.com/emcie-co/parlant): enables agents to handle multi-intent, free-form conversations by dynamically activating relevant guidelines instead of rigidly routing to a single sub-agent — solving the context fragmentation problem inherent in traditional LangGraph supervisor patterns.
|
| 35 |
+
|
| 36 |
+
## 📚 ***`References for Context Engineering`***
|
| 37 |
+
|
| 38 |
+
- [**Context Engineering for AI Agents — Manus Blog**](https://manus.im/blog/Context-Engineering-for-AI-Agents-Lessons-from-Building-Manus)
|
| 39 |
+
- [**YouTube Talk Manus**](https://www.youtube.com/watch?v=6_BcCthVvb8&start=2525)
|
| 40 |
+
- [**LangGraph Overview**](https://docs.langchain.com/oss/python/langgraph/overview)
|
| 41 |
+
- https://www.anthropic.com/engineering/effective-context-engineering-for-ai-agents
|
| 42 |
+
- https://medium.com/fundamentals-of-artificial-intelligence/mitigate-context-poisoning-in-ai-agents-using-context-engineering-96cf40dbb38d
|
| 43 |
+
- https://blog.langchain.com/context-engineering-for-agents/
|
| 44 |
+
- **langgraph implementations**
|
| 45 |
+
- [video]((https://www.youtube.com/watch?v=nyKvyRrpbyY))
|
| 46 |
+
- [good notebooks](https://github.com/langchain-ai/how_to_fix_your_context/blob/main/notebooks/utils.py)
|
| 47 |
+
- [Langgraph summary of what frontier labs and firms apply](https://www.youtube.com/watch?v=XFCkrYHHfpQ)
|
| 48 |
+
|
| 49 |
+
These resources guide our approach to **memory management, planning transparency, and tool orchestration** in autonomous agents.
|
| 50 |
+
|
| 51 |
+
## 🧾 ***`HR Candidate Screening Multi-Agent System`***
|
| 52 |
+
An autonomous HR assistant that streamlines early recruitment through five steps:
|
| 53 |
+
1. **CV Upload (Application)** — candidate applications uploaded and parsed
|
| 54 |
+
2. **CV Screening** — rank and shortlist candidates using LLM reasoning
|
| 55 |
+
3. **Voice Screening** — invite and coordinate interviews using a voice agent.
|
| 56 |
+
4. **Person-to-Person Screening** — schedule HR interviews via Google Calendar integration
|
| 57 |
+
5. **Decision** — generate a concise summary and notify HR
|
| 58 |
+
|
| 59 |
+
> **`NOTE`**
|
| 60 |
+
> - Final decision of whether candidate will be hired is made by human.
|
| 61 |
+
> - Just automate the boring, tedious stuff while keeping human final decision in the loop.
|
| 62 |
+
|
| 63 |
+
**Architecture:**
|
| 64 |
+
1. **Main Planner Agent**: orchestrates the workflow
|
| 65 |
+
2. **Subagents**:
|
| 66 |
+
- CV Screening Agent
|
| 67 |
+
- Voice Screening Agent
|
| 68 |
+
- Meeting Scheduler Agent
|
| 69 |
+
3. **Tools (via MCP)** connect to Gmail, Calendar, and Voice APIs.
|
| 70 |
+
4. **Database** stores both candidate info and persistent agent memory.
|
| 71 |
+
5. **Gradio UI** visualizes workflow, reasoning, and results.
|
| 72 |
+
```mermaid
|
| 73 |
+
flowchart TD
|
| 74 |
+
subgraph MainAgent["🧠 Main Planner Agent"]
|
| 75 |
+
A1["Plans • Reasons • Executes"]
|
| 76 |
+
end
|
| 77 |
+
|
| 78 |
+
subgraph Subagents["🤖 Subagents"]
|
| 79 |
+
S1["📄 CV Screening"]
|
| 80 |
+
S2["🎙️ Voice Screening"]
|
| 81 |
+
S3["📅 Scheduling"]
|
| 82 |
+
S4["🧾 Decision Summary"]
|
| 83 |
+
end
|
| 84 |
+
|
| 85 |
+
subgraph Tools["⚙️ MCP & External Tools"]
|
| 86 |
+
T1["📧 Gmail"]
|
| 87 |
+
T2["🗓️ Google Calendar"]
|
| 88 |
+
T3["🗣️ Voice API"]
|
| 89 |
+
end
|
| 90 |
+
|
| 91 |
+
subgraph Data["🗄️ Database"]
|
| 92 |
+
D1["Candidate Data"]
|
| 93 |
+
D2["Context Memory (Cognitive Offloading)"]
|
| 94 |
+
end
|
| 95 |
+
|
| 96 |
+
subgraph UI["💬 Gradio Dashboard"]
|
| 97 |
+
U1["HR View & Interaction"]
|
| 98 |
+
end
|
| 99 |
+
|
| 100 |
+
%% Connections
|
| 101 |
+
MainAgent --> Subagents
|
| 102 |
+
Subagents --> Tools
|
| 103 |
+
Subagents --> Data
|
| 104 |
+
MainAgent --> Data
|
| 105 |
+
MainAgent --> UI
|
| 106 |
+
```
|
| 107 |
+
|
| 108 |
+
**GCP Setup for Judges:**
|
| 109 |
+
A single demo Gmail/Calendar account (`scionhire.demo@gmail.com`) is pre-authorized via OAuth, with stored credentials in `.env`.
|
| 110 |
+
Judges can run or view the live demo without any credential setup, experiencing real Gmail + Calendar automation safely.
|
| 111 |
+
|
| 112 |
+
We use **hierarchical planning**:
|
| 113 |
+
- **Main Agent:** decides next step in the workflow (plan, adapt, replan)
|
| 114 |
+
- **Subagents:** specialized executors (screening, scheduling, summarization)
|
| 115 |
+
- **Memory State:** tracks plan progress and tool results
|
| 116 |
+
- **Dashboard Visualization:** shows active plan steps and reasoning traces for transparency
|
| 117 |
+
|
| 118 |
+
🧠 Why This Is an Agent (Not Just a Workflow)
|
| 119 |
+
|
| 120 |
+
| Criterion | Workflow | Our System |
|
| 121 |
+
|------------|-----------|-------------|
|
| 122 |
+
| **Autonomy** | Executes fixed sequence of steps | Main agent decides next actions without manual triggers |
|
| 123 |
+
| **Planning** | Predefined order (A → B → C) | Main agent generates and adapts a plan (e.g., skip, retry, re-order) |
|
| 124 |
+
| **Reasoning** | No decision logic | Uses LLM reasoning to evaluate outputs and choose next subagent |
|
| 125 |
+
| **Context Awareness** | Stateless | Maintains shared memory of candidates, progress, and outcomes |
|
| 126 |
+
| **Adaptation** | Fails or stops on error | Re-plans (e.g., if calendar slots full or candidate unresponsive) |
|
| 127 |
+
|
| 128 |
+
✅ **Therefore:** it qualifies as an *agentic system* because it **plans, reasons, and executes** autonomously rather than following a static workflow.
|
| 129 |
+
|
| 130 |
+
## ***`Project Structure`***
|
| 131 |
+
```
|
| 132 |
+
agentic-hr/
|
| 133 |
+
│
|
| 134 |
+
├── 📁 src/
|
| 135 |
+
│ │
|
| 136 |
+
│ ├── 📁 core/
|
| 137 |
+
│ │ │ ├── base_agent.py # Abstract BaseAgent (LangGraph-compatible)
|
| 138 |
+
│ │ │ ├── supervisor.py # Supervisor agent (LangGraph graph assembly)
|
| 139 |
+
│ │ │ ├── state.py # Shared AgentState + context window
|
| 140 |
+
│ │ │ ├── planner.py # High-level planning logic
|
| 141 |
+
│ │ │ └── executor.py # Graph executor / runner
|
| 142 |
+
│ │
|
| 143 |
+
│ ├── 📁 agents/
|
| 144 |
+
│ │ │
|
| 145 |
+
│ │ ├── 📁 cv_screening/
|
| 146 |
+
│ │ │ │ ├── agent.py # CVScreeningAgent implementation
|
| 147 |
+
│ │ │ │ ├── 📁 tools/
|
| 148 |
+
│ │ │ │ │ ├── doc_parser.py
|
| 149 |
+
│ │ │ │ │ ├── normalize_skills.py
|
| 150 |
+
│ │ │ │ │ ├── rank_candidates.py
|
| 151 |
+
│ │ │ │ │ └── match_to_jd.py
|
| 152 |
+
│ │ │ │ └── 📁 schemas/
|
| 153 |
+
│ │ │ │ ├── cv_schema.py # Parsed CV Pydantic schema
|
| 154 |
+
│ │ │ │ └── jd_schema.py # Job description schema
|
| 155 |
+
│ │ │
|
| 156 |
+
│ │ ├── 📁 voice_screening/
|
| 157 |
+
│ │ │ │ ├── agent.py # VoiceScreeningAgent
|
| 158 |
+
│ │ │ │ ├── 📁 tools/
|
| 159 |
+
│ │ │ │ │ ├── twilio_client.py
|
| 160 |
+
│ │ │ │ │ ├── whisper_transcribe.py
|
| 161 |
+
│ │ │ │ │ └── tts_service.py
|
| 162 |
+
│ │ │ │ └── 📁 schemas/
|
| 163 |
+
│ │ │ │ ├── call_result.py
|
| 164 |
+
│ │ │ │ └── transcript.py
|
| 165 |
+
│ │ │
|
| 166 |
+
│ │ ├── 📁 scheduler/
|
| 167 |
+
│ │ │ │ ├── agent.py # SchedulerAgent
|
| 168 |
+
│ │ │ │ ├── 📁 tools/
|
| 169 |
+
│ │ │ │ │ ├── calendar_tool.py
|
| 170 |
+
│ │ │ │ │ ├── gmail_tool.py
|
| 171 |
+
│ │ │ │ │ └── slot_optimizer.py
|
| 172 |
+
│ │ │ │ └── 📁 schemas/
|
| 173 |
+
│ │ │ │ └── meeting_schema.py
|
| 174 |
+
│ │ │
|
| 175 |
+
│ │ └── 📁 decision/
|
| 176 |
+
│ │ ├── agent.py # DecisionAgent (final summarizer/Reporter)
|
| 177 |
+
│ │ └── 📁 schemas/
|
| 178 |
+
│ │ └── decision_report.py
|
| 179 |
+
│ │
|
| 180 |
+
│ ├── 📁 mcp_server/
|
| 181 |
+
│ │ ├── main.py
|
| 182 |
+
│ │ ├── 📁 endpoints/
|
| 183 |
+
│ │ ├── auth.py
|
| 184 |
+
│ │ └── schemas.py
|
| 185 |
+
│ │
|
| 186 |
+
│ ├── 📁 gradio/
|
| 187 |
+
│ │ ├── app.py # Main Gradio app (Hugging Face Space entry)
|
| 188 |
+
│ │ ├── dashboard.py # Live agent graph & logs view
|
| 189 |
+
│ │ ├── candidate_portal.py # Candidate upload / screening status
|
| 190 |
+
│ │ ├── hr_portal.py # HR review + interview approval
|
| 191 |
+
│ │ ├── components.py # Shared Gradio components
|
| 192 |
+
│ │ └── 📁 assets/ # Logos, CSS, etc.
|
| 193 |
+
│ │
|
| 194 |
+
│ ├── 📁 cv_ui/
|
| 195 |
+
│ │ ├── app.py
|
| 196 |
+
│ │
|
| 197 |
+
��� ├── 📁 voice_screening_ui/
|
| 198 |
+
│ │ ├── app.py
|
| 199 |
+
│ │
|
| 200 |
+
│ │
|
| 201 |
+
│ ├── 📁 prompts/
|
| 202 |
+
│ │ ├── prompt_manager.py # Centralized prompt versioning
|
| 203 |
+
│ │ ├── cv_prompts.py
|
| 204 |
+
│ │ ├── voice_prompts.py
|
| 205 |
+
│ │ └── scheduler_prompts.py
|
| 206 |
+
│ │
|
| 207 |
+
│ ├── 📁 database/
|
| 208 |
+
│ │ ├── models.py # SQLAlchemy models
|
| 209 |
+
│ │ ├── db_client.py # Connection & CRUD
|
| 210 |
+
│ │ └── context_sync.py # Cognitive offloading (context ⇄ DB)
|
| 211 |
+
│ │
|
| 212 |
+
│ ├── main.py # CLI runner / local orchestrator entry
|
| 213 |
+
│ └── config.py # Environment configuration
|
| 214 |
+
│
|
| 215 |
+
├── 📁 tests/
|
| 216 |
+
│ │ ├── test_cv_agent.py
|
| 217 |
+
│ │ ├── test_voice_agent.py
|
| 218 |
+
│ │ ├── test_scheduler_agent.py
|
| 219 |
+
│ │ ├── test_mcp_server.py
|
| 220 |
+
│ │ └── test_integration.py
|
| 221 |
+
│
|
| 222 |
+
├── .env.example
|
| 223 |
+
├── requirements.txt
|
| 224 |
+
├── Dockerfile
|
| 225 |
+
├── app.py # Shortcut to src/ui/app.py
|
| 226 |
+
├── README.md
|
| 227 |
+
└── LICENSE
|
| 228 |
+
```
|
| 229 |
+
|
| 230 |
+
## ***`Multi Agent System Architecture`***
|
| 231 |
+
Below you will find an overview of the subagent components that mnake upo the entire system. More detailed information and brainstorming is decicated to the `docs/agents/..` directory.
|
| 232 |
+
|
| 233 |
+
### 1) ***`Orchestrator`***
|
| 234 |
+
#### Overview
|
| 235 |
+
|
| 236 |
+
The orchestrator agent is reponsible for **supervising** and **triggering** the ***tasks of the subagents***.
|
| 237 |
+
|
| 238 |
+
> For more planning and info, go to `docs/agents/agent_orchestrator.md`
|
| 239 |
+
|
| 240 |
+
### 2) ***`CV Screener`***
|
| 241 |
+
#### Overview
|
| 242 |
+
The cv screening agent deals with scanning the applicant's CV's, and deciding who are fruitful versus unpromising candidates as a first filtering step.
|
| 243 |
+
|
| 244 |
+
> For more planning and info, go to `docs/agents/cv_screening.md`
|
| 245 |
+
|
| 246 |
+
### 3) 🎙️ ***`Voice Screening Agent`***
|
| 247 |
+
|
| 248 |
+
#### Overview
|
| 249 |
+
The **Voice Screening Agent** conducts automated phone interviews and integrates with the **LangGraph HR Orchestrator**.
|
| 250 |
+
It uses **Twilio** for phone calls, **Whisper/ASR** for speech-to-text, **ElevenLabs** for natural voice output, and **LangGraph** for dialogue logic.
|
| 251 |
+
|
| 252 |
+
> For more planning and info, go to `docs/agents/voice_screening.md`
|
| 253 |
+
|
| 254 |
+
### 4) ***`Google MCP Agents`***
|
| 255 |
+
#### Overview
|
| 256 |
+
The google mcp agents will be resposnible to:
|
| 257 |
+
a) writing emails
|
| 258 |
+
b) scheduling and menaging google calendar events
|
| 259 |
+
|
| 260 |
+
It adviseable to break this up into two subagents, to get rid of `context poisoning`.
|
| 261 |
+
|
| 262 |
+
> For more planning and info, go to `docs/agents/google_mcp_agent.md`
|
| 263 |
+
|
| 264 |
+
### 4) ***`LLM as a Judge`***
|
| 265 |
+
#### Overview
|
| 266 |
+
LLM-as-a-judge will be leveraged to judge call screening results.
|
| 267 |
+
|
| 268 |
+
> For more planning and info, go to `docs/agents/judging_agent.md`
|
| 269 |
+
|
| 270 |
+
## 🗄️ ***`Data Layer`***
|
| 271 |
+
|
| 272 |
+
The system uses a unified **SQLAlchemy-based database** for both **candidate data management** and **context engineering**.
|
| 273 |
+
|
| 274 |
+
### 📦 Purpose
|
| 275 |
+
| Data Type | Description |
|
| 276 |
+
|------------|--------------|
|
| 277 |
+
| 🧾 **Candidates** | Stores CVs, parsed data, and screening results |
|
| 278 |
+
| 🎙️ **Voice Results** | Saves transcripts, evaluations, and tone analysis |
|
| 279 |
+
| 🗓️ **Scheduling** | Tracks HR availability and confirmed interviews |
|
| 280 |
+
| 🧠 **Agent Context Memory** | Enables **cognitive offloading** — storing reasoning traces and summaries so the active context stays uncluttered and information can be recalled when needed |
|
| 281 |
+
| 📚 **Logs / Tool History** | Archives tool interactions and results for transparency and reuse |
|
| 282 |
+
|
| 283 |
+
We use [**SQLAlchemy**](https://www.sqlalchemy.org) as the ORM layer to manage both structured candidate data and **persistent agent memory**, allowing the system to offload, summarize, and retrieve context efficiently across sessions.
|
| 284 |
+
|
| 285 |
+
## 🗃️ ***`Prompt Archive`***
|
| 286 |
+
|
| 287 |
+
To ensure consistent behavior and easy experimentation across subagents, the system includes a **centralized prompt management layer**.
|
| 288 |
+
|
| 289 |
+
### 📦 Purpose
|
| 290 |
+
| Component | Description |
|
| 291 |
+
|------------|--------------|
|
| 292 |
+
| 🧠 **Prompt Templates** | Stores standardized prompts for each subagent (CV screening, voice screening, scheduling) |
|
| 293 |
+
| 🔄 **Prompt Versioning** | Allows tracking and updating of prompt iterations without changing agent code |
|
| 294 |
+
| 🧩 **Dynamic Injection** | Enables context-dependent prompt construction using retrieved memory or database summaries |
|
| 295 |
+
| 📚 **Archive** | Keeps older prompt variants for reproducibility and ablation testing |
|
| 296 |
+
|
| 297 |
+
## 📺 ***`Gradio Interface`***
|
| 298 |
+
|
| 299 |
+
We use **Gradio** to demonstrate our agent's reasoning, planning, and tool use interactively — fully aligned with the **Agents & MCP Hackathon** focus on **context engineering** and **user value**.
|
| 300 |
+
|
| 301 |
+
### 🧩 Key Features
|
| 302 |
+
| Section | Purpose |
|
| 303 |
+
|----------|----------|
|
| 304 |
+
| 🧍 **Candidate Portal** | Upload CVs, submit applications, and view screening results |
|
| 305 |
+
| 🧑💼 **HR Portal** | Review shortlisted candidates, trigger voice screenings, and schedule interviews |
|
| 306 |
+
| 🧠 **Agent Dashboard** | Visualizes the current plan, tool calls, and reasoning traces in real time |
|
| 307 |
+
| ⚙️ **Tool Integration** | Shows live MCP actions (Gmail send, Calendar scheduling) with status updates |
|
| 308 |
+
| 📊 **Context View** | Displays agent memory, current workflow stage, and adaptive plan updates |
|
| 309 |
+
|
| 310 |
+
#### Context Engineering Visualization?
|
| 311 |
+
This is what judges really care about — it must show that the system is agentic (reasoning, memory, planning).
|
| 312 |
+
🧠 Agent Plan Viewer
|
| 313 |
+
gr.JSON() or custom visual showing the current plan state, e.g.:
|
| 314 |
+
```json
|
| 315 |
+
{
|
| 316 |
+
"plan": [
|
| 317 |
+
"1. Screen CVs ✅",
|
| 318 |
+
"2. Invite for voice screening 🔄",
|
| 319 |
+
"3. Schedule HR interview ⬜",
|
| 320 |
+
"4. Await HR decision ⬜"
|
| 321 |
+
]
|
| 322 |
+
}
|
| 323 |
+
```
|
| 324 |
+
🗺️ Live Plan Progress
|
| 325 |
+
- Use a progress bar or color-coded status list of steps.
|
| 326 |
+
- Judges must see autonomous transitions (from one step to another).
|
| 327 |
+
|
| 328 |
+
💬 Reasoning Log / Memory
|
| 329 |
+
- Stream or text box showing LLM thought traces or context summary:
|
| 330 |
+
- “Detected strong match for Data Scientist role.”
|
| 331 |
+
- “Candidate completed voice interview; confidence: 8.4/10.”
|
| 332 |
+
- “Next step: scheduling HR interview.”
|
| 333 |
+
|
| 334 |
+
⚙️ Tool Call Trace
|
| 335 |
+
- Small table showing:
|
| 336 |
+
|
| 337 |
+
| Time | Tool | Action | Result |
|
| 338 |
+
| ----- | -------- | ---------------- | --------- |
|
| 339 |
+
| 12:05 | Gmail | `send_invite()` | Sent |
|
| 340 |
+
| 12:06 | Calendar | `create_event()` | Confirmed |
|
| 341 |
+
|
| 342 |
+
## 🔗 ***`MCP Integration (Best Practice Setup)`***
|
| 343 |
+
|
| 344 |
+
To align fully with the **Agents & MCP Hackathon** standards, our system will use or extend a **standardized MCP server** for integrations such as **Gmail** and **Google Calendar** — and potentially **Scion Voice** in later stages.
|
| 345 |
+
|
| 346 |
+
**`Inspired by`** [Huggingface MCP Course](https://huggingface.co/learn/mcp-course/en/unit2/introduction): shows how to build an MCP app.
|
| 347 |
+
|
| 348 |
+
### 🧩 Why MCP?
|
| 349 |
+
| Benefit | Description |
|
| 350 |
+
|----------|--------------|
|
| 351 |
+
| ✅ **Standardized** | Exposes Gmail & Calendar as reusable MCP tools with a consistent schema |
|
| 352 |
+
| 🔐 **Secure** | OAuth handled once server-side — no tokens or secrets stored in the agent |
|
| 353 |
+
| 🧱 **Modular** | Clean separation between the agent's reasoning logic and the integration layer |
|
| 354 |
+
| 🔄 **Reusable** | Same MCP server can serve multiple projects or agents |
|
| 355 |
+
| 🚀 **Hackathon-Ready** | Directly fulfills the “use MCP tools or external APIs” requirement |
|
| 356 |
+
|
| 357 |
+
---
|
| 358 |
+
|
| 359 |
+
### ⚙️ Why Use MCP Instead of Just Defining Tools
|
| 360 |
+
| Approach | Limitation / Risk | MCP Advantage |
|
| 361 |
+
|-----------|-------------------|----------------|
|
| 362 |
+
| **Custom-defined tools** (e.g., direct Gmail API calls in code) | Each project must re-implement auth, rate limits, and API logic | MCP provides a *shared, pre-authorized* interface any agent can use |
|
| 363 |
+
| **Embedded credentials** in `.env` | Security risk, harder for judges to test | Credentials handled server-side — no secrets in the repo |
|
| 364 |
+
| **Tight coupling** between agent and tool | Hard to swap or extend integrations | MCP creates a plug-and-play API boundary between reasoning and execution |
|
| 365 |
+
| **Limited reuse** | Tools only exist in one codebase | MCP servers can expose many tools to multiple agents dynamically |
|
| 366 |
+
|
| 367 |
+
MCP turns these one-off integrations into **standardized, composable building blocks** that work across agents, organizations, or platforms — the same philosophy used by **Anthropic**, **LangChain**, and **Hugging Face** in 2025 agent ecosystems.
|
| 368 |
+
|
| 369 |
+
|
| 370 |
+
We will build or extend the open-source [**mcp-gsuite**](https://github.com/MarkusPfundstein/mcp-gsuite) server and host it securely on **Google Cloud Run**.
|
| 371 |
+
This server manages authentication, token refresh, and rate limiting — while exposing standardized MCP actions like:
|
| 372 |
+
```json
|
| 373 |
+
{
|
| 374 |
+
"action": "gmail.send",
|
| 375 |
+
"parameters": { "to": "candidate@example.com", "subject": "Interview Invite", "body": "..." }
|
| 376 |
+
}
|
| 377 |
+
```
|
| 378 |
+
|
| 379 |
+
and
|
| 380 |
+
|
| 381 |
+
```json
|
| 382 |
+
{
|
| 383 |
+
"action": "calendar.create_event",
|
| 384 |
+
"parameters": { "summary": "HR Interview", "start": "...", "end": "..." }
|
| 385 |
+
}
|
| 386 |
+
```
|
| 387 |
+
This architecture lets our HR agent (and future projects) perform real email and scheduling actions via secure MCP endpoints — giving judges a safe, live demo of true agentic behavior with no local credential setup required.
|
| 388 |
+
|
| 389 |
+
## 🧠 ***`Agent Supervisor — Why Parlant + LangGraph`***
|
| 390 |
+
|
| 391 |
+
LangGraph provides a powerful orchestration backbone for planning, reasoning, and executing multi-agent workflows.
|
| 392 |
+
However, its common **supervisor pattern** has a key limitation: the supervisor routes each user query to **only one sub-agent** at a time.
|
| 393 |
+
|
| 394 |
+
### ⚠️ Example Problem
|
| 395 |
+
> “I uploaded my CV yesterday. Can I also reschedule my interview — and how long is the voice call?”
|
| 396 |
+
|
| 397 |
+
A standard LangGraph supervisor would forward this entire message to, say, the **CV Screening Agent**,
|
| 398 |
+
missing the **scheduling** and **voice screening** parts — causing incomplete or fragmented responses.
|
| 399 |
+
|
| 400 |
+
### 💡 Parlant as the Fix
|
| 401 |
+
**[Parlant](https://github.com/emcie-co/parlant)** solves this by replacing single-route logic with **dynamic guideline activation**.
|
| 402 |
+
Instead of rigid routing, it loads multiple relevant *guidelines* into context simultaneously, allowing coherent handling of mixed intents.
|
| 403 |
+
|
| 404 |
+
```python
|
| 405 |
+
agent.create_guideline(
|
| 406 |
+
condition="User asks about rescheduling",
|
| 407 |
+
action="Call SchedulerAgent via LangGraph tool"
|
| 408 |
+
)
|
| 409 |
+
|
| 410 |
+
agent.create_guideline(
|
| 411 |
+
condition="User asks about voice screening duration",
|
| 412 |
+
action="Query VoiceScreeningAgent"
|
| 413 |
+
)
|
| 414 |
+
```
|
| 415 |
+
|
| 416 |
+
If a user blends both topics, ***both guidelines trigger***, producing a unified, context-aware response.
|
| 417 |
+
|
| 418 |
+
### ⚙️ Why Combine Them
|
| 419 |
+
| Layer | Framework | Role |
|
| 420 |
+
| ----------------------------- | ------------- | ----------------------------------------------------------------------- |
|
| 421 |
+
| 🧠 **Workflow Orchestration** | **LangGraph** | Executes structured agent workflows (CV → Voice → Schedule → Decision). |
|
| 422 |
+
| 💬 **Conversational Layer** | **Parlant** | Dynamically manages mixed intents using guideline-based reasoning. |
|
| 423 |
+
| 🔧 **Integration Layer** | **MCP Tools** | Provides standardized access to Gmail, Calendar, and Voice APIs. |
|
| 424 |
+
|
| 425 |
+
|
| 426 |
+
Together, ***Parlant + LangGraph*** merge structured planning with conversational adaptability —
|
| 427 |
+
enabling our HR agent to reason, plan, and respond naturally to complex, multi-topic interactions.
|
| 428 |
+
|
| 429 |
+
## ✨ ***`Agentic Enhancements [BONUS]`***
|
| 430 |
+
|
| 431 |
+
To make the system more **autonomous, interpretable, and resilient**, we integrated a few lightweight yet powerful improvements:
|
| 432 |
+
|
| 433 |
+
- 🧠 **Self-Reflection** – before executing a step, the agent briefly states *why* it's taking that action, improving reasoning transparency.
|
| 434 |
+
- 🔄 **Adaptive Re-Planning** – if a subagent or tool call fails (e.g., no calendar slot, missing response, or API timeout), the main planner automatically updates its plan — skipping, retrying, or re-ordering steps instead of stopping.
|
| 435 |
+
- 🧮 **LLM Self-Evaluation** – after each stage (CV, voice, scheduling), a lightweight judge model rates the result and adds feedback for the next step.
|
| 436 |
+
- 🗂️ **Context Summary** – the dashboard displays a live summary of all candidates, their current stage, and key outcomes.
|
| 437 |
+
- 🤝 **Human-in-the-Loop Checkpoint** – HR receives a short confirmation prompt before final scheduling to ensure responsible autonomy.
|
| 438 |
+
|
| 439 |
+
These enhancements demonstrate **true agentic behavior** — autonomous planning, adaptive execution, and transparent reasoning — in a simple, explainable way.
|
| 440 |
+
|
| 441 |
+
## 👥 ***`Team`***
|
| 442 |
+
| Member |
|
| 443 |
+
| -------- |
|
| 444 |
+
| [Sebastian Wefers](https://github.com/Ocean-code-1995) |
|
| 445 |
+
| [Owen Kaplinsky](https://github.com/owenkaplinsky) |
|
| 446 |
+
| [SrikarMK](https://github.com/Srikarmk) |
|
| 447 |
+
| [Dmitri Moscoglo](https://github.com/DimiM99) |
|
| 448 |
+
|
| 449 |
+
# ***`License`***
|
| 450 |
+
|
| 451 |
+
This project includes and builds upon [gmail-mcp](https://github.com/theposch/gmail-mcp),
|
| 452 |
+
which is licensed under the [GNU General Public License v3.0](https://www.gnu.org/licenses/gpl-3.0.en.html).
|
| 453 |
+
|
| 454 |
+
This repository extends gmail-mcp for experimental integration and automation with Claude Desktop.
|
| 455 |
+
All modifications are distributed under the same GPLv3 license.
|
| 456 |
+
|
| 457 |
+
> **Note:** The original gmail-mcp code has not been modified at this stage.
|
docs/video/script.md
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Video Demo Script
|
| 2 |
+
|
| 3 |
+
This script outlines the flow and queries for the demo video.
|
| 4 |
+
|
| 5 |
+
## Prerequisites / Setup
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
Manually create:
|
| 9 |
+
1. **Existing Candidate**: "Jane Doe"
|
| 10 |
+
- Status: `voice_passed` (Ready for final interview)
|
| 11 |
+
- Includes fake CV and Voice Screening results for the agent to analyze.
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
## Demo Flow
|
| 15 |
+
|
| 16 |
+
### 1. The New Applicant & Morning Check-in
|
| 17 |
+
*Action 1: Switch to the **CV Portal UI** and upload a new CV for "Alex Smith" (applying him to the system).*
|
| 18 |
+
|
| 19 |
+
*Goal: Show the agent's awareness of the current database state (both old and new candidates).*
|
| 20 |
+
|
| 21 |
+
**Query 1:**
|
| 22 |
+
```text
|
| 23 |
+
Hi! Can you give me a summary of the current recruitment status? Who are the active candidates and what stages are they in?
|
| 24 |
+
```
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
---
|
| 28 |
+
|
| 29 |
+
### 2. CV Screening
|
| 30 |
+
*Goal: Demonstrate the CV screening workflow.*
|
| 31 |
+
|
| 32 |
+
**Query 2:**
|
| 33 |
+
```text
|
| 34 |
+
I see PERSON X is a new applicant. Can you please screen his CV and summarize the feedback and his score?
|
| 35 |
+
```
|
| 36 |
+
|
| 37 |
+
**Query 3:**
|
| 38 |
+
```text
|
| 39 |
+
Send him an email invitation for the voice screening!
|
| 40 |
+
```
|
| 41 |
+
|
| 42 |
+
-> Then do voice screening!
|
| 43 |
+
|
| 44 |
+
---
|
| 45 |
+
|
| 46 |
+
### 3. Reviewing the Candidate
|
| 47 |
+
*Goal: Show the "Voice Judge" results and Calendar/Email integration.*
|
| 48 |
+
|
| 49 |
+
**Query 4:**
|
| 50 |
+
```text
|
| 51 |
+
I see she/he completed the voice screening. Can you analyze her interview transcript and tell me how she performed?
|
| 52 |
+
```
|
| 53 |
+
|
| 54 |
+
*(Expected Response: Agent reads the voice analysis/judge score and summarizes the candidate's strengths/weaknesses.)*
|
| 55 |
+
|
| 56 |
+
**Query 5:**
|
| 57 |
+
```text
|
| 58 |
+
That sounds promising. Let's move her to the final stage. Please schedule a person-to-person interview with her for next Tuesday at 10 AM. Add it to the HR calendar and send her a calendar invitation.
|
| 59 |
+
```
|
| 60 |
+
|
| 61 |
+
---
|
| 62 |
+
|
| 63 |
+
### 4. Final Status Check
|
| 64 |
+
*Goal: Confirm all actions were executed.*
|
| 65 |
+
|
| 66 |
+
**Query 6:**
|
| 67 |
+
```text
|
| 68 |
+
Thanks! Can you show me the updated pipeline status?
|
| 69 |
+
```
|
intro.md
CHANGED
|
@@ -455,3 +455,4 @@ This repository extends gmail-mcp for experimental integration and automation wi
|
|
| 455 |
All modifications are distributed under the same GPLv3 license.
|
| 456 |
|
| 457 |
> **Note:** The original gmail-mcp code has not been modified at this stage.
|
|
|
|
|
|
| 455 |
All modifications are distributed under the same GPLv3 license.
|
| 456 |
|
| 457 |
> **Note:** The original gmail-mcp code has not been modified at this stage.
|
| 458 |
+
>
|
requirements/agent.txt
CHANGED
|
@@ -1,4 +1,3 @@
|
|
| 1 |
langchain
|
| 2 |
langchain-openai
|
| 3 |
langgraph
|
| 4 |
-
uv
|
|
|
|
| 1 |
langchain
|
| 2 |
langchain-openai
|
| 3 |
langgraph
|
|
|
scripts/db/list_candidates.py
CHANGED
|
@@ -10,8 +10,8 @@ from sqlalchemy.exc import ProgrammingError
|
|
| 10 |
# Ensure project root is in path
|
| 11 |
import scripts.db # noqa: F401
|
| 12 |
|
| 13 |
-
from src.database.candidates.client import SessionLocal
|
| 14 |
-
from src.database.candidates.models import Candidate
|
| 15 |
|
| 16 |
|
| 17 |
def list_candidates(limit: int = 10) -> bool:
|
|
@@ -41,7 +41,17 @@ def list_candidates(limit: int = 10) -> bool:
|
|
| 41 |
.all()
|
| 42 |
)
|
| 43 |
for c in candidates:
|
| 44 |
-
print(f" -
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
|
| 46 |
return True
|
| 47 |
|
|
|
|
| 10 |
# Ensure project root is in path
|
| 11 |
import scripts.db # noqa: F401
|
| 12 |
|
| 13 |
+
from src.backend.database.candidates.client import SessionLocal
|
| 14 |
+
from src.backend.database.candidates.models import Candidate
|
| 15 |
|
| 16 |
|
| 17 |
def list_candidates(limit: int = 10) -> bool:
|
|
|
|
| 41 |
.all()
|
| 42 |
)
|
| 43 |
for c in candidates:
|
| 44 |
+
print(f" - ID: {c.id}")
|
| 45 |
+
print(f" Full Name: {c.full_name}")
|
| 46 |
+
print(f" Email: {c.email}")
|
| 47 |
+
print(f" Phone: {c.phone_number}")
|
| 48 |
+
print(f" CV Path: {c.cv_file_path}")
|
| 49 |
+
print(f" Parsed CV Path: {c.parsed_cv_file_path}")
|
| 50 |
+
print(f" Status: {c.status}")
|
| 51 |
+
print(f" Auth Code: {c.auth_code}")
|
| 52 |
+
print(f" Created At: {c.created_at}")
|
| 53 |
+
print(f" Updated At: {c.updated_at}")
|
| 54 |
+
print("-" * 40)
|
| 55 |
|
| 56 |
return True
|
| 57 |
|
scripts/db/setup_demo_state.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import uuid
|
| 2 |
+
from datetime import datetime, timedelta
|
| 3 |
+
from src.backend.database.candidates.client import SessionLocal
|
| 4 |
+
from src.backend.database.candidates.models import Candidate, CVScreeningResult, VoiceScreeningResult
|
| 5 |
+
from src.backend.state.candidate import CandidateStatus
|
| 6 |
+
|
| 7 |
+
def setup_demo_state():
|
| 8 |
+
print("🚀 Setting up demo state...")
|
| 9 |
+
session = SessionLocal()
|
| 10 |
+
|
| 11 |
+
# 1. Cleanup existing Jane Doe
|
| 12 |
+
existing = session.query(Candidate).filter(Candidate.email == "jane.doe@example.com").first()
|
| 13 |
+
if existing:
|
| 14 |
+
print(f"Creating clean slate: Deleting existing candidate {existing.full_name}...")
|
| 15 |
+
session.delete(existing)
|
| 16 |
+
session.commit()
|
| 17 |
+
|
| 18 |
+
# 2. Create Candidate: Jane Doe (Advanced Stage)
|
| 19 |
+
candidate_id = uuid.uuid4()
|
| 20 |
+
jane = Candidate(
|
| 21 |
+
id=candidate_id,
|
| 22 |
+
full_name="Jane Doe",
|
| 23 |
+
email="jane.doe@example.com",
|
| 24 |
+
phone_number="+15550101",
|
| 25 |
+
status=CandidateStatus.voice_passed, # Ready for final interview
|
| 26 |
+
created_at=datetime.utcnow() - timedelta(days=2)
|
| 27 |
+
)
|
| 28 |
+
session.add(jane)
|
| 29 |
+
|
| 30 |
+
# 3. Add CV Screening Result (She passed this previously)
|
| 31 |
+
cv_result = CVScreeningResult(
|
| 32 |
+
candidate_id=candidate_id,
|
| 33 |
+
job_title="Senior Product Manager",
|
| 34 |
+
skills_match_score=92.0,
|
| 35 |
+
experience_match_score=88.0,
|
| 36 |
+
education_match_score=95.0,
|
| 37 |
+
overall_fit_score=91.0,
|
| 38 |
+
llm_feedback="Candidate demonstrates exceptional strategic thinking and relevant experience in SaaS product management. Strong leadership background.",
|
| 39 |
+
timestamp=datetime.utcnow() - timedelta(days=2)
|
| 40 |
+
)
|
| 41 |
+
session.add(cv_result)
|
| 42 |
+
|
| 43 |
+
# 4. Add Voice Screening Result (She just completed this)
|
| 44 |
+
voice_result = VoiceScreeningResult(
|
| 45 |
+
candidate_id=candidate_id,
|
| 46 |
+
transcript_text="I have over 5 years of experience leading agile teams... I believe communication is key to product success... In my last role, I increased user retention by 20%...",
|
| 47 |
+
sentiment_score=0.8,
|
| 48 |
+
confidence_score=0.9,
|
| 49 |
+
communication_score=9.5,
|
| 50 |
+
llm_summary="Candidate spoke clearly and confidently. Provided concrete examples of past success (20% retention increase). demonstrated strong understanding of agile methodologies.",
|
| 51 |
+
llm_judgment_json={"decision": "pass", "reasoning": "High confidence and clear articulation of value."},
|
| 52 |
+
timestamp=datetime.utcnow() - timedelta(hours=1)
|
| 53 |
+
)
|
| 54 |
+
session.add(voice_result)
|
| 55 |
+
|
| 56 |
+
session.commit()
|
| 57 |
+
print(f"✅ Successfully created candidate: Jane Doe (ID: {candidate_id})")
|
| 58 |
+
print(" - Status: voice_passed")
|
| 59 |
+
print(" - Has CV Result: Yes")
|
| 60 |
+
print(" - Has Voice Result: Yes")
|
| 61 |
+
print("\nReady for demo video recording! 🎥")
|
| 62 |
+
|
| 63 |
+
if __name__ == "__main__":
|
| 64 |
+
setup_demo_state()
|
| 65 |
+
|
scripts/db/test_connection.py
CHANGED
|
@@ -11,7 +11,7 @@ from sqlalchemy import text
|
|
| 11 |
# Ensure project root is in path
|
| 12 |
import scripts.db # noqa: F401
|
| 13 |
|
| 14 |
-
from src.database.candidates.client import get_engine
|
| 15 |
|
| 16 |
|
| 17 |
def test_connection() -> bool:
|
|
|
|
| 11 |
# Ensure project root is in path
|
| 12 |
import scripts.db # noqa: F401
|
| 13 |
|
| 14 |
+
from src.backend.database.candidates.client import get_engine
|
| 15 |
|
| 16 |
|
| 17 |
def test_connection() -> bool:
|
scripts/db/test_cv_upload.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Test the CV upload functionality.
|
| 3 |
+
Run with:
|
| 4 |
+
>>> export PYTHONPATH=$PYTHONPATH:. && python3 scripts/db/test_cv_upload.py
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
import os
|
| 9 |
+
from src.sdk.cv_upload import CVUploadClient
|
| 10 |
+
|
| 11 |
+
def test_upload():
|
| 12 |
+
client = CVUploadClient(base_url="http://localhost:8080/api/v1/cv")
|
| 13 |
+
|
| 14 |
+
cv_path = "src/backend/database/cvs/uploads/Sebastian_Wefers_CV.pdf"
|
| 15 |
+
|
| 16 |
+
if not os.path.exists(cv_path):
|
| 17 |
+
print(f"❌ CV file not found at {cv_path}")
|
| 18 |
+
return
|
| 19 |
+
|
| 20 |
+
print(f"📤 Uploading {cv_path}...")
|
| 21 |
+
|
| 22 |
+
try:
|
| 23 |
+
with open(cv_path, "rb") as f:
|
| 24 |
+
response = client.submit(
|
| 25 |
+
full_name="Test Candidate",
|
| 26 |
+
email="test_candidate@example.com",
|
| 27 |
+
phone="+1234567890",
|
| 28 |
+
cv_file=f,
|
| 29 |
+
filename="test_candidate.pdf"
|
| 30 |
+
)
|
| 31 |
+
|
| 32 |
+
if response.success:
|
| 33 |
+
print(f"✅ Upload successful: {response.message}")
|
| 34 |
+
print(f"Details: {response}")
|
| 35 |
+
elif response.already_exists:
|
| 36 |
+
print(f"⚠️ Candidate already exists: {response.message}")
|
| 37 |
+
else:
|
| 38 |
+
print(f"❌ Upload failed: {response.message}")
|
| 39 |
+
|
| 40 |
+
except Exception as e:
|
| 41 |
+
print(f"❌ Error during upload: {e}")
|
| 42 |
+
|
| 43 |
+
if __name__ == "__main__":
|
| 44 |
+
test_upload()
|
| 45 |
+
|
scripts/db/test_session.py
CHANGED
|
@@ -10,7 +10,7 @@ from sqlalchemy import text
|
|
| 10 |
# Ensure project root is in path
|
| 11 |
import scripts.db # noqa: F401
|
| 12 |
|
| 13 |
-
from src.database.candidates.client import SessionLocal
|
| 14 |
|
| 15 |
|
| 16 |
def test_session_query() -> bool:
|
|
|
|
| 10 |
# Ensure project root is in path
|
| 11 |
import scripts.db # noqa: F401
|
| 12 |
|
| 13 |
+
from src.backend.database.candidates.client import SessionLocal
|
| 14 |
|
| 15 |
|
| 16 |
def test_session_query() -> bool:
|
scripts/db/wipe.py
CHANGED
|
@@ -11,7 +11,7 @@ from sqlalchemy import text
|
|
| 11 |
project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '../../'))
|
| 12 |
sys.path.append(project_root)
|
| 13 |
|
| 14 |
-
from src.database.candidates.client import get_engine
|
| 15 |
|
| 16 |
def wipe_database():
|
| 17 |
print("⚠️ WARNING: This will PERMANENTLY DELETE ALL RECORDS from the 'candidates' table and all related tables (CASCADE).")
|
|
|
|
| 11 |
project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '../../'))
|
| 12 |
sys.path.append(project_root)
|
| 13 |
|
| 14 |
+
from src.backend.database.candidates.client import get_engine
|
| 15 |
|
| 16 |
def wipe_database():
|
| 17 |
print("⚠️ WARNING: This will PERMANENTLY DELETE ALL RECORDS from the 'candidates' table and all related tables (CASCADE).")
|
scripts/infra/reset_db.sh
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
# Reset the database environment
|
| 3 |
+
|
| 4 |
+
echo "🛑 Stopping containers..."
|
| 5 |
+
docker compose -f docker/docker-compose.yml down
|
| 6 |
+
|
| 7 |
+
echo "🗑️ Removing database volume..."
|
| 8 |
+
docker volume rm docker_postgres_data
|
| 9 |
+
|
| 10 |
+
echo "🚀 Rebuilding and starting..."
|
| 11 |
+
docker compose --env-file .env -f docker/docker-compose.yml up --build
|
| 12 |
+
|
src/backend/__init__.py
ADDED
|
File without changes
|
src/backend/agents/__init__.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from .db_executor import db_executor
|
| 2 |
+
from .cv_screening import screen_cv, cv_screening_workflow
|
| 3 |
+
from .gcalendar import gcalendar_agent
|
| 4 |
+
from .gmail import gmail_agent
|
| 5 |
+
from .voice_screening import voice_judge
|
| 6 |
+
|
| 7 |
+
__all__ = [
|
| 8 |
+
"db_executor",
|
| 9 |
+
"screen_cv",
|
| 10 |
+
"cv_screening_workflow",
|
| 11 |
+
"gcalendar_agent",
|
| 12 |
+
"gmail_agent",
|
| 13 |
+
"voice_judge",
|
| 14 |
+
]
|
src/backend/agents/cv_screening/__init__.py
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from .cv_screener import screen_cv
|
| 2 |
+
from .cv_screening_workflow import cv_screening_workflow
|
| 3 |
+
|
| 4 |
+
__all__ = ["screen_cv", "cv_screening_workflow"]
|
src/backend/agents/cv_screening/cv_screener.py
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""CV Screening Agent Module
|
| 2 |
+
|
| 3 |
+
Run as follows:
|
| 4 |
+
>>> docker compose up --build
|
| 5 |
+
>>> docker compose run --rm candidates_db_init python -m src.agents.cv_screening.screener
|
| 6 |
+
"""
|
| 7 |
+
import json
|
| 8 |
+
from langchain_openai import ChatOpenAI
|
| 9 |
+
from langchain.messages import SystemMessage, HumanMessage
|
| 10 |
+
|
| 11 |
+
from dotenv import load_dotenv
|
| 12 |
+
from src.backend.agents.cv_screening.schemas.output_schema import CVScreeningOutput
|
| 13 |
+
from src.backend.agents.cv_screening.utils import read_file
|
| 14 |
+
from src.backend.database.candidates import write_cv_results_to_db
|
| 15 |
+
from src.backend.prompts import get_prompt
|
| 16 |
+
|
| 17 |
+
load_dotenv()
|
| 18 |
+
|
| 19 |
+
SYSTEM_PROMPT = get_prompt(
|
| 20 |
+
template_name="CV_Screener",
|
| 21 |
+
latest_version=True
|
| 22 |
+
)
|
| 23 |
+
|
| 24 |
+
# --- The evaluator function ---
|
| 25 |
+
def screen_cv(cv_text: str, jd_text: str) -> CVScreeningOutput:
|
| 26 |
+
"""
|
| 27 |
+
Evaluate a candidate's CV against a job description using an LLM.
|
| 28 |
+
|
| 29 |
+
Args:
|
| 30 |
+
cv_text (str): The text content of the candidate's CV.
|
| 31 |
+
jd_text (str): The text content of the Job Description.
|
| 32 |
+
|
| 33 |
+
Returns:
|
| 34 |
+
CVScreeningOutput: The structured screening result.
|
| 35 |
+
Makes model write feedback before scoring, leading to better calibration
|
| 36 |
+
and genuine reasoning that leads to more balanced scores.
|
| 37 |
+
|
| 38 |
+
**NOTE**:
|
| 39 |
+
>>> The model generates feedback first (Chain-of-Thought)
|
| 40 |
+
>>> to ensure calibrated scores.
|
| 41 |
+
|
| 42 |
+
"""
|
| 43 |
+
llm = (
|
| 44 |
+
ChatOpenAI(
|
| 45 |
+
model="gpt-4o-mini",
|
| 46 |
+
temperature=0,
|
| 47 |
+
max_tokens=1500,
|
| 48 |
+
)
|
| 49 |
+
.with_structured_output(CVScreeningOutput)
|
| 50 |
+
)
|
| 51 |
+
# payload
|
| 52 |
+
messages = [
|
| 53 |
+
# Instruction
|
| 54 |
+
SystemMessage(
|
| 55 |
+
content=SYSTEM_PROMPT
|
| 56 |
+
),
|
| 57 |
+
# Payload
|
| 58 |
+
HumanMessage(
|
| 59 |
+
content=(
|
| 60 |
+
f"Job Description:\n{jd_text}\n\n"
|
| 61 |
+
f"Candidate CV:\n{cv_text}\n"
|
| 62 |
+
)
|
| 63 |
+
),
|
| 64 |
+
]
|
| 65 |
+
|
| 66 |
+
return llm.invoke(messages)
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
# --- Main execution for testing ---
|
| 71 |
+
if __name__ == "__main__":
|
| 72 |
+
from pathlib import Path
|
| 73 |
+
#BASE_PATH = Path("/Users/sebastianwefers/Desktop/projects/recruitment-agent/src/database")
|
| 74 |
+
BASE_PATH = Path(__file__).resolve().parents[2] / "database"
|
| 75 |
+
|
| 76 |
+
cv_text = read_file(BASE_PATH / "cvs/parsed/c762271c-af8f-49db-acbb-e37e5f0f0f98_SWefers_CV-sections.txt")
|
| 77 |
+
jd_text = read_file(BASE_PATH / "cvs/job_postings/ai_engineer.txt")
|
| 78 |
+
|
| 79 |
+
# trigger evaluation
|
| 80 |
+
result = screen_cv(cv_text, jd_text)
|
| 81 |
+
print(json.dumps(result.model_dump(), indent=2))
|
| 82 |
+
|
| 83 |
+
# optionally write to DB
|
| 84 |
+
write_cv_results_to_db(
|
| 85 |
+
candidate_email="sebastianwefersnz@gmail.com",
|
| 86 |
+
result=result,
|
| 87 |
+
job_title="AI Engineer"
|
| 88 |
+
)
|
src/backend/agents/cv_screening/cv_screening_workflow.py
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pathlib import Path
|
| 2 |
+
from langchain_core.tools import tool
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
from src.backend.agents.cv_screening.cv_screener import screen_cv
|
| 6 |
+
from src.backend.agents.cv_screening.utils import read_file
|
| 7 |
+
from src.backend.database.candidates import (
|
| 8 |
+
write_cv_results_to_db,
|
| 9 |
+
get_candidate_by_name,
|
| 10 |
+
)
|
| 11 |
+
|
| 12 |
+
@tool
|
| 13 |
+
def cv_screening_workflow(candidate_full_name: str = "") -> str:
|
| 14 |
+
"""
|
| 15 |
+
Runs the deterministic CV screening workflow for a candidate.
|
| 16 |
+
This is a fixed sequential process, not a reasoning agent.
|
| 17 |
+
|
| 18 |
+
Steps:
|
| 19 |
+
1. Retrieve candidate info from DB
|
| 20 |
+
2. Read files (CV & Job Description)
|
| 21 |
+
3. Evaluate CV
|
| 22 |
+
4. Store results in DB & update status
|
| 23 |
+
|
| 24 |
+
Args:
|
| 25 |
+
candidate_full_name (str): The full name of the candidate to screen.
|
| 26 |
+
|
| 27 |
+
Returns:
|
| 28 |
+
str: A message indicating the outcome of the workflow. (✅ or ❌)
|
| 29 |
+
"""
|
| 30 |
+
if not candidate_full_name:
|
| 31 |
+
return "❌ Candidate name is required."
|
| 32 |
+
|
| 33 |
+
# 1️⃣ Retrieve candidate info from DB
|
| 34 |
+
print(f"🔍 Looking up candidate: {candidate_full_name}")
|
| 35 |
+
candidate = get_candidate_by_name(candidate_full_name)
|
| 36 |
+
|
| 37 |
+
if not candidate:
|
| 38 |
+
return f"❌ Candidate '{candidate_full_name}' not found in database."
|
| 39 |
+
|
| 40 |
+
candidate_email = candidate["email"]
|
| 41 |
+
cv_path_str = candidate["parsed_cv_file_path"]
|
| 42 |
+
|
| 43 |
+
if not cv_path_str:
|
| 44 |
+
return f"❌ No parsed CV path recorded for '{candidate_full_name}'."
|
| 45 |
+
|
| 46 |
+
# Resolve paths
|
| 47 |
+
# Assuming the parsed path in DB is relative to project root (e.g., src/backend/database/cvs/parsed/...)
|
| 48 |
+
# We need to ensure we can find it.
|
| 49 |
+
|
| 50 |
+
# Calculate project root from this file location
|
| 51 |
+
# src/backend/agents/cv_screening/cv_screening_workflow.py -> 4 levels up to src -> 5 to root
|
| 52 |
+
root_dir = Path(__file__).resolve().parents[4]
|
| 53 |
+
|
| 54 |
+
cv_path = root_dir / cv_path_str
|
| 55 |
+
if not cv_path.exists():
|
| 56 |
+
# Try treating it as absolute or check if the path in DB was absolute
|
| 57 |
+
cv_path = Path(cv_path_str)
|
| 58 |
+
if not cv_path.exists():
|
| 59 |
+
# Fallback: check legacy path just in case
|
| 60 |
+
legacy_path = root_dir / "src/database/cvs/parsed" / Path(cv_path_str).name
|
| 61 |
+
if legacy_path.exists():
|
| 62 |
+
cv_path = legacy_path
|
| 63 |
+
else:
|
| 64 |
+
return f"❌ CV file not found at: {cv_path_str} or {legacy_path}"
|
| 65 |
+
|
| 66 |
+
# JD path is constant for this MVP
|
| 67 |
+
jd_path = root_dir / "src/backend/database/job_postings/ai_engineer.txt"
|
| 68 |
+
|
| 69 |
+
if not jd_path.exists():
|
| 70 |
+
return f"❌ Job description not found at: {jd_path}"
|
| 71 |
+
|
| 72 |
+
# 2️⃣ Read files
|
| 73 |
+
print(f"📄 Reading Job Description from: {jd_path}")
|
| 74 |
+
jd_text = read_file(jd_path)
|
| 75 |
+
|
| 76 |
+
print(f"📄 Reading CV from: {cv_path}")
|
| 77 |
+
cv_text = read_file(cv_path)
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
# 3️⃣ Evaluate CV
|
| 81 |
+
print("🧠 Running LLM screening...")
|
| 82 |
+
try:
|
| 83 |
+
result = screen_cv(cv_text, jd_text)
|
| 84 |
+
except Exception as e:
|
| 85 |
+
return f"❌ Error during LLM screening: {str(e)}"
|
| 86 |
+
|
| 87 |
+
# 4️⃣ Store results in DB & update status
|
| 88 |
+
print("💾 Saving results to database...")
|
| 89 |
+
try:
|
| 90 |
+
write_cv_results_to_db(
|
| 91 |
+
candidate_email=candidate_email,
|
| 92 |
+
result=result,
|
| 93 |
+
job_title="AI Engineer"
|
| 94 |
+
)
|
| 95 |
+
except Exception as e:
|
| 96 |
+
return f"❌ Error saving results to DB: {str(e)}"
|
| 97 |
+
|
| 98 |
+
return f"✅ CV Screening Workflow completed successfully for {candidate_full_name}. Scores and feedback have been saved to the database."
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
if __name__ == "__main__":
|
| 104 |
+
# Example usage for testing
|
| 105 |
+
# You can run this directly if you have a candidate in the DB
|
| 106 |
+
import sys
|
| 107 |
+
name = sys.argv[1] if len(sys.argv) > 1 else "Ada Lovelace"
|
| 108 |
+
cv_screening_workflow(name)
|
src/backend/agents/cv_screening/schemas/__init__.py
ADDED
|
File without changes
|
src/backend/agents/cv_screening/schemas/output_schema.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pydantic import BaseModel, Field
|
| 2 |
+
from typing import Optional, Dict, Any
|
| 3 |
+
|
| 4 |
+
class CVScreeningOutput(BaseModel):
|
| 5 |
+
# CRITICAL: Keep llm_feedback as the first field.
|
| 6 |
+
# This enforces Chain-of-Thought reasoning: the model must explain its assessment
|
| 7 |
+
# BEFORE assigning scores, leading to better calibration. DO NOT REORDER.
|
| 8 |
+
llm_feedback: str
|
| 9 |
+
skills_match_score: float = Field(..., ge=0, le=1)
|
| 10 |
+
experience_match_score: float = Field(..., ge=0, le=1)
|
| 11 |
+
education_match_score: float = Field(..., ge=0, le=1)
|
| 12 |
+
overall_fit_score: float = Field(..., ge=0, le=1)
|
src/backend/agents/cv_screening/tools/__init__.py
ADDED
|
File without changes
|
src/backend/agents/cv_screening/utils/__init__.py
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from .read_file import read_file
|
| 2 |
+
|
| 3 |
+
__all__ = [
|
| 4 |
+
"read_file",
|
| 5 |
+
]
|
src/backend/agents/cv_screening/utils/read_file.py
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pathlib import Path
|
| 2 |
+
|
| 3 |
+
def read_file(path: Path) -> str:
|
| 4 |
+
"""Read the contents of a file and return as a string.
|
| 5 |
+
"""
|
| 6 |
+
with open(path, "r", encoding="utf-8") as f:
|
| 7 |
+
return f.read()
|
src/backend/agents/db_executor/__init__.py
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from .db_executor import db_executor
|
| 2 |
+
|
| 3 |
+
__all__ = [
|
| 4 |
+
"db_executor",
|
| 5 |
+
]
|
src/backend/agents/db_executor/codeact/__init__.py
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
This agent coding agent based `CodeAct`agent pattern, see:
|
| 3 |
+
- https://arxiv.org/abs/2408.02193
|
| 4 |
+
- https://www.google.com/url?sa=t&source=web&rct=j&opi=89978449&url=https://huggingface.co/collections/DataImaginations/codeact-interaction-framework-for-llm-agents&ved=2ahUKEwjpkvKpwIWRAxWJ1wIHHY3KEzAQFnoECCEQAQ&usg=AOvVaw2IxMEyHwZPI7MfLSlFMyqN
|
| 5 |
+
- https://github.com/langchain-ai/langgraph-codeact
|
| 6 |
+
"""
|
src/backend/agents/db_executor/codeact/core/codeact.py
ADDED
|
@@ -0,0 +1,545 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import re
|
| 2 |
+
import io
|
| 3 |
+
import builtins
|
| 4 |
+
import contextlib
|
| 5 |
+
from collections.abc import Generator
|
| 6 |
+
import inspect
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
from typing import Any, Awaitable, Callable, Optional, Sequence, Type, TypeVar, Union, Literal
|
| 9 |
+
import types
|
| 10 |
+
import json
|
| 11 |
+
|
| 12 |
+
from langchain.chat_models import init_chat_model
|
| 13 |
+
from langchain_core.language_models import BaseChatModel
|
| 14 |
+
from langchain_core.tools import StructuredTool
|
| 15 |
+
from langchain_core.tools import tool as create_tool
|
| 16 |
+
from langchain_core.messages import AIMessageChunk, AIMessage
|
| 17 |
+
from langgraph.graph import END, START, StateGraph, MessagesState
|
| 18 |
+
from langgraph.types import Command
|
| 19 |
+
from langgraph.checkpoint.memory import MemorySaver
|
| 20 |
+
|
| 21 |
+
from ..schemas import TokenStream
|
| 22 |
+
from ..schemas.openai_key import OpenAIApiKey
|
| 23 |
+
from ..utils import pretty_print_state
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
class CodeActState(MessagesState):
|
| 29 |
+
"""State for CodeAct agent."""
|
| 30 |
+
|
| 31 |
+
script: Optional[str]
|
| 32 |
+
"""The Python code script to be executed."""
|
| 33 |
+
context: dict[str, Any]
|
| 34 |
+
"""Dictionary containing the execution context with available tools and variables."""
|
| 35 |
+
|
| 36 |
+
EvalFunction = Callable[[str, dict[str, Any]], tuple[str, dict[str, Any]]]
|
| 37 |
+
EvalCoroutine = Callable[[str, dict[str, Any]], Awaitable[tuple[str, dict[str, Any]]]]
|
| 38 |
+
|
| 39 |
+
StateSchema = TypeVar("StateSchema", bound=CodeActState)
|
| 40 |
+
StateSchemaType = Type[StateSchema]
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
import inspect
|
| 44 |
+
from pathlib import Path
|
| 45 |
+
import tiktoken
|
| 46 |
+
from typing import Any, Optional, Union, Sequence
|
| 47 |
+
from langchain_core.tools import StructuredTool
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
class CodeActAgent:
|
| 51 |
+
def __init__(
|
| 52 |
+
self,
|
| 53 |
+
model_name: str,
|
| 54 |
+
model_provider: str,
|
| 55 |
+
tools: Optional[Sequence] = None,
|
| 56 |
+
eval_fn=None,
|
| 57 |
+
system_prompt: Union[str, Path] = None,
|
| 58 |
+
bind_tools: bool = False,
|
| 59 |
+
memory: bool = True,
|
| 60 |
+
) -> None:
|
| 61 |
+
"""
|
| 62 |
+
Parameters
|
| 63 |
+
----------
|
| 64 |
+
- model_name : str
|
| 65 |
+
The name of the chat model to use (e.g., "gpt-4o").
|
| 66 |
+
- model_provider : str
|
| 67 |
+
The model provider (e.g., "openai").
|
| 68 |
+
- tools : Optional[Sequence], optional
|
| 69 |
+
A list of tools (functions or StructuredTool) available to the agent.
|
| 70 |
+
- eval_fn : Optional[EvalFunction or EvalCoroutine], optional
|
| 71 |
+
The function or coroutine to evaluate generated code. If None, uses default_eval.
|
| 72 |
+
- system_prompt : Union[str, Path], optional
|
| 73 |
+
The system prompt as a file path or raw string.
|
| 74 |
+
- bind_tools : bool, optional
|
| 75 |
+
Whether to bind tool signatures and docstrings into the system prompt.
|
| 76 |
+
- memory : bool, optional
|
| 77 |
+
Whether to enable memory checkpointing.
|
| 78 |
+
"""
|
| 79 |
+
self.model_name = model_name
|
| 80 |
+
self.model_provider = model_provider
|
| 81 |
+
self.tools = tools or []
|
| 82 |
+
self.eval_fn = eval_fn or self.default_eval
|
| 83 |
+
self.system_prompt = system_prompt
|
| 84 |
+
self.bind_tools = bind_tools
|
| 85 |
+
self.memory = memory
|
| 86 |
+
|
| 87 |
+
# Initialize components
|
| 88 |
+
self.model = init_chat_model(model_name, model_provider=model_provider)
|
| 89 |
+
self.prompt = self._create_system_prompt()
|
| 90 |
+
self.agent = self._create_codeact(self.model, self.tools, self.eval_fn)
|
| 91 |
+
|
| 92 |
+
checkpointer = MemorySaver() if memory else None
|
| 93 |
+
self.compiled_agent = self.agent.compile(checkpointer=checkpointer)
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
def _create_system_prompt(self) -> tuple[str, dict[str, int]]:
|
| 97 |
+
"""Build the final system prompt and compute token counts.
|
| 98 |
+
"""
|
| 99 |
+
system_text = self._load_prompt(self.system_prompt)
|
| 100 |
+
if not system_text:
|
| 101 |
+
raise ValueError("`system_prompt` must be provided as a file path or string.")
|
| 102 |
+
|
| 103 |
+
system_text = system_text.strip()
|
| 104 |
+
|
| 105 |
+
# Base version (without tools)
|
| 106 |
+
prompt_text = system_text
|
| 107 |
+
|
| 108 |
+
# If bind_tools enabled, build and append
|
| 109 |
+
if self.bind_tools:
|
| 110 |
+
if not self.tools:
|
| 111 |
+
print("[⚠️] bind_tools=True but no tools provided. Skipping tool injection.")
|
| 112 |
+
else:
|
| 113 |
+
tools_text = self._build_tool_context()
|
| 114 |
+
prompt_text = f"{system_text.strip()}\n\n{tools_text.strip()}"
|
| 115 |
+
|
| 116 |
+
# Compute token counts
|
| 117 |
+
tokens_without_tools = self._count_tokens(system_text)
|
| 118 |
+
tokens_with_tools = self._count_tokens(prompt_text)
|
| 119 |
+
|
| 120 |
+
# Print summary neatly
|
| 121 |
+
print(
|
| 122 |
+
f"🧮 System prompt token count:\n"
|
| 123 |
+
f" - Without tools: {tokens_without_tools}\n"
|
| 124 |
+
f" - With tools: {tokens_with_tools}"
|
| 125 |
+
)
|
| 126 |
+
|
| 127 |
+
return prompt_text
|
| 128 |
+
|
| 129 |
+
|
| 130 |
+
def _build_tool_context(self) -> str:
|
| 131 |
+
"""Constructs the tool context block with docstrings and signatures.
|
| 132 |
+
"""
|
| 133 |
+
tool_strings = []
|
| 134 |
+
for t in self.tools:
|
| 135 |
+
func = t.func if isinstance(t, StructuredTool) else t
|
| 136 |
+
sig = inspect.signature(func)
|
| 137 |
+
doc = (func.__doc__ or "").strip()
|
| 138 |
+
tool_strings.append(
|
| 139 |
+
f"def {func.__name__}{sig}:\n \"\"\"{doc}\"\"\"\n ..."
|
| 140 |
+
)
|
| 141 |
+
|
| 142 |
+
joined_tools = "\n\n".join(tool_strings)
|
| 143 |
+
return (
|
| 144 |
+
"\n\nNote that you have access to the following predefined tools:\n\n"
|
| 145 |
+
f"{joined_tools}"
|
| 146 |
+
)
|
| 147 |
+
|
| 148 |
+
@staticmethod
|
| 149 |
+
def _load_prompt(p: Optional[Union[str, Path]]) -> Optional[str]:
|
| 150 |
+
"""Load a prompt from file path or treat as raw string."""
|
| 151 |
+
if p is None:
|
| 152 |
+
return None
|
| 153 |
+
|
| 154 |
+
# If it's already multiline or contains newlines, it's almost certainly a literal string
|
| 155 |
+
if isinstance(p, str) and ("\n" in p or len(p) > 200):
|
| 156 |
+
return p
|
| 157 |
+
|
| 158 |
+
# Otherwise, check if it's an actual file path
|
| 159 |
+
path = Path(p)
|
| 160 |
+
if path.exists() and path.is_file():
|
| 161 |
+
return path.read_text(encoding="utf-8")
|
| 162 |
+
|
| 163 |
+
# Fallback: just return as string
|
| 164 |
+
return str(p)
|
| 165 |
+
|
| 166 |
+
|
| 167 |
+
def _count_tokens(self, text: str) -> int:
|
| 168 |
+
"""Count tokens for a given text.
|
| 169 |
+
"""
|
| 170 |
+
try:
|
| 171 |
+
enc = tiktoken.encoding_for_model(self.model_name)
|
| 172 |
+
except Exception:
|
| 173 |
+
enc = tiktoken.get_encoding("cl100k_base")
|
| 174 |
+
return len(enc.encode(text))
|
| 175 |
+
|
| 176 |
+
|
| 177 |
+
|
| 178 |
+
def _extract_and_combine_codeblocks(self, text: str) -> str:
|
| 179 |
+
"""
|
| 180 |
+
Extract and combine code blocks from the model completion.
|
| 181 |
+
Helper function to execute extracted code in sandbox environment.
|
| 182 |
+
"""
|
| 183 |
+
pattern = r"(?:^|\n)```(.*?)(?:```(?:\n|$))" #r"(?:^|\n)```(.*?)(?:```(?:\n|$))"
|
| 184 |
+
code_blocks = re.findall(pattern, text, re.DOTALL)
|
| 185 |
+
if not code_blocks:
|
| 186 |
+
return ""
|
| 187 |
+
processed = []
|
| 188 |
+
for block in code_blocks:
|
| 189 |
+
lines = block.strip().split("\n")
|
| 190 |
+
if lines and (not lines[0].strip() or " " not in lines[0].strip()):
|
| 191 |
+
block = "\n".join(lines[1:])
|
| 192 |
+
processed.append(block)
|
| 193 |
+
return "\n\n".join(processed)
|
| 194 |
+
|
| 195 |
+
|
| 196 |
+
@staticmethod
|
| 197 |
+
def default_eval(code: str, _locals: dict[str, Any]) -> tuple[str, dict[str, Any]]:
|
| 198 |
+
"""Evaluate the code in the sandbox.
|
| 199 |
+
"""
|
| 200 |
+
original_keys = set(_locals.keys())
|
| 201 |
+
try:
|
| 202 |
+
with contextlib.redirect_stdout(io.StringIO()) as f:
|
| 203 |
+
exec(code, builtins.__dict__, _locals)
|
| 204 |
+
result = f.getvalue() or "<code ran, no output printed to stdout>"
|
| 205 |
+
except Exception as e:
|
| 206 |
+
result = f"Error during execution: {repr(e)}"
|
| 207 |
+
new_keys = set(_locals.keys()) - original_keys
|
| 208 |
+
new_vars = {key: _locals[key] for key in new_keys}
|
| 209 |
+
return result, new_vars
|
| 210 |
+
|
| 211 |
+
@staticmethod
|
| 212 |
+
def _filter_serializable(d: dict[str, Any]) -> dict[str, Any]:
|
| 213 |
+
"""Keep only JSON/msgpack-serializable values (basic Python types).
|
| 214 |
+
"""
|
| 215 |
+
serializable_types = (
|
| 216 |
+
str, int, float, bool, list, dict, type(None)
|
| 217 |
+
)
|
| 218 |
+
return {
|
| 219 |
+
k: v for k, v in d.items() if isinstance(v, serializable_types)
|
| 220 |
+
}
|
| 221 |
+
|
| 222 |
+
|
| 223 |
+
def _create_codeact(
|
| 224 |
+
self,
|
| 225 |
+
model: BaseChatModel,
|
| 226 |
+
tools: Sequence[Union[StructuredTool, Callable]],
|
| 227 |
+
eval_fn: Union[EvalFunction, EvalCoroutine],
|
| 228 |
+
*,
|
| 229 |
+
state_schema: StateSchemaType = CodeActState,
|
| 230 |
+
) -> StateGraph:
|
| 231 |
+
"""Create a LangGraph state graph for the CodeAct agent.
|
| 232 |
+
"""
|
| 233 |
+
tools = [
|
| 234 |
+
t if isinstance(t, StructuredTool) else create_tool(t)
|
| 235 |
+
for t in tools
|
| 236 |
+
]
|
| 237 |
+
self.tools_context = {tool.name: tool.func for tool in tools}
|
| 238 |
+
|
| 239 |
+
def call_model_stream(state: StateSchema):
|
| 240 |
+
messages = [{"role": "system", "content": self.prompt}] + state["messages"]
|
| 241 |
+
|
| 242 |
+
# Accumulate into one combined chunk
|
| 243 |
+
accumulated: AIMessageChunk | None = None
|
| 244 |
+
|
| 245 |
+
# stream partial tokens as AIMessagesChunks wioth .content = "Hel",
|
| 246 |
+
for delta in self.model.stream(messages):
|
| 247 |
+
if accumulated is None:
|
| 248 |
+
accumulated = delta
|
| 249 |
+
else:
|
| 250 |
+
accumulated = accumulated + delta # merge chunks
|
| 251 |
+
|
| 252 |
+
# yield partial update immediately (for streaming UI)
|
| 253 |
+
yield Command(update={"messages": [delta], "script": None})
|
| 254 |
+
|
| 255 |
+
# after streaming completes
|
| 256 |
+
if accumulated is None:
|
| 257 |
+
yield Command(update={"messages": [], "script": None})
|
| 258 |
+
return # nothing came back
|
| 259 |
+
|
| 260 |
+
# Convert merged chunks into a final message
|
| 261 |
+
full_text = accumulated.content or ""
|
| 262 |
+
|
| 263 |
+
# Check for code blocks
|
| 264 |
+
code = self._extract_and_combine_codeblocks(full_text)
|
| 265 |
+
|
| 266 |
+
if code:
|
| 267 |
+
# Create a fake tool call entry
|
| 268 |
+
tool_call_id = "sandbox"
|
| 269 |
+
fake_tool_call = {
|
| 270 |
+
"id": tool_call_id,
|
| 271 |
+
"type": "function",
|
| 272 |
+
"function": {
|
| 273 |
+
"name": "sandbox",
|
| 274 |
+
"arguments": code
|
| 275 |
+
}
|
| 276 |
+
}
|
| 277 |
+
# Patch the assistant message with tool_calls
|
| 278 |
+
accumulated.additional_kwargs = {"tool_calls": [fake_tool_call]}
|
| 279 |
+
|
| 280 |
+
# Pass both the patched assistant message and code to sandbox
|
| 281 |
+
yield Command(
|
| 282 |
+
goto="sandbox",
|
| 283 |
+
update={
|
| 284 |
+
"messages": [accumulated],
|
| 285 |
+
"script": code
|
| 286 |
+
}
|
| 287 |
+
)
|
| 288 |
+
else:
|
| 289 |
+
yield Command(
|
| 290 |
+
update={
|
| 291 |
+
"messages": [accumulated],
|
| 292 |
+
"script": None
|
| 293 |
+
}
|
| 294 |
+
)
|
| 295 |
+
|
| 296 |
+
|
| 297 |
+
if inspect.iscoroutinefunction(eval_fn):
|
| 298 |
+
|
| 299 |
+
async def sandbox(state: StateSchema):
|
| 300 |
+
"""Run the code in the sandbox and return a proper OpenAI tool message.
|
| 301 |
+
"""
|
| 302 |
+
existing_context = state.get("context", {})
|
| 303 |
+
|
| 304 |
+
# Combine persistent context with runtime-only tools
|
| 305 |
+
exec_context = {**existing_context, **self.tools_context}
|
| 306 |
+
|
| 307 |
+
# Get tool_call_id for traceability
|
| 308 |
+
prev_msgs = state.get("messages", [])
|
| 309 |
+
tool_call_id = "sandbox"
|
| 310 |
+
for msg in reversed(prev_msgs):
|
| 311 |
+
if hasattr(msg, "additional_kwargs") and msg.additional_kwargs.get("tool_calls"):
|
| 312 |
+
tool_call_id = msg.additional_kwargs["tool_calls"][0]["id"]
|
| 313 |
+
break
|
| 314 |
+
|
| 315 |
+
# Execute user code
|
| 316 |
+
output, new_vars = await eval_fn(state["script"], exec_context)
|
| 317 |
+
|
| 318 |
+
# Only persist serializable data
|
| 319 |
+
serializable_new_vars = self._filter_serializable(new_vars)
|
| 320 |
+
new_context = {**existing_context, **serializable_new_vars}
|
| 321 |
+
|
| 322 |
+
# Format output properly
|
| 323 |
+
content_str = (
|
| 324 |
+
f"Sandbox result of your executed code:\n{json.dumps(output, default=str)}"
|
| 325 |
+
if not isinstance(output, str)
|
| 326 |
+
else f"Sandbox result of your executed code:\n{output}"
|
| 327 |
+
)
|
| 328 |
+
|
| 329 |
+
# Return OpenAI-compliant tool result
|
| 330 |
+
return {
|
| 331 |
+
"messages": [
|
| 332 |
+
{
|
| 333 |
+
"role": "tool",
|
| 334 |
+
"tool_call_id": tool_call_id,
|
| 335 |
+
"name": "sandbox",
|
| 336 |
+
"content": content_str
|
| 337 |
+
}
|
| 338 |
+
],
|
| 339 |
+
"context": new_context,
|
| 340 |
+
}
|
| 341 |
+
|
| 342 |
+
|
| 343 |
+
else:
|
| 344 |
+
def sandbox(state: StateSchema):
|
| 345 |
+
"""Run the code in the sandbox and return a proper OpenAI tool message.
|
| 346 |
+
"""
|
| 347 |
+
existing_context = state.get("context", {})
|
| 348 |
+
|
| 349 |
+
# Combine persistent context with runtime-only tools
|
| 350 |
+
exec_context = {**existing_context, **self.tools_context}
|
| 351 |
+
|
| 352 |
+
# Get tool_call_id for traceability
|
| 353 |
+
prev_msgs = state.get("messages", [])
|
| 354 |
+
tool_call_id = "sandbox"
|
| 355 |
+
for msg in reversed(prev_msgs):
|
| 356 |
+
if hasattr(msg, "additional_kwargs") and msg.additional_kwargs.get("tool_calls"):
|
| 357 |
+
tool_call_id = msg.additional_kwargs["tool_calls"][0]["id"]
|
| 358 |
+
break
|
| 359 |
+
|
| 360 |
+
# Execute user code
|
| 361 |
+
output, new_vars = eval_fn(state["script"], exec_context)
|
| 362 |
+
|
| 363 |
+
# Only persist serializable data
|
| 364 |
+
serializable_new_vars = self._filter_serializable(new_vars)
|
| 365 |
+
new_context = {**existing_context, **serializable_new_vars}
|
| 366 |
+
|
| 367 |
+
# Format output properly
|
| 368 |
+
content_str = ( # NOTE: before "json.dumps(output)"
|
| 369 |
+
f"Sandbox result of your executed code:\n{json.dumps(output, default=str)}"
|
| 370 |
+
if not isinstance(output, str)
|
| 371 |
+
else f"Sandbox result of your executed code:\n{output}"
|
| 372 |
+
)
|
| 373 |
+
|
| 374 |
+
# Return OpenAI-compliant tool result
|
| 375 |
+
return {
|
| 376 |
+
"messages": [
|
| 377 |
+
{
|
| 378 |
+
"role": "tool",
|
| 379 |
+
"tool_call_id": tool_call_id,
|
| 380 |
+
"name": "sandbox",
|
| 381 |
+
"content": content_str,
|
| 382 |
+
# Keep as string if already string else JSON serialize
|
| 383 |
+
}
|
| 384 |
+
],
|
| 385 |
+
"context": new_context,
|
| 386 |
+
}
|
| 387 |
+
|
| 388 |
+
# --- Build the state graph ---
|
| 389 |
+
agent = StateGraph(state_schema)
|
| 390 |
+
agent.add_node(call_model_stream, destinations=(END, "sandbox"))
|
| 391 |
+
agent.add_node(sandbox)
|
| 392 |
+
agent.add_edge(START, "call_model_stream")
|
| 393 |
+
agent.add_edge("sandbox", "call_model_stream")
|
| 394 |
+
return agent
|
| 395 |
+
|
| 396 |
+
|
| 397 |
+
def stream(
|
| 398 |
+
self,
|
| 399 |
+
messages: list[dict],
|
| 400 |
+
thread_id: int = 1
|
| 401 |
+
) -> Generator[
|
| 402 |
+
TokenStream,
|
| 403 |
+
None,
|
| 404 |
+
None
|
| 405 |
+
]:
|
| 406 |
+
"""
|
| 407 |
+
Generator yielding agent outputs during execution.
|
| 408 |
+
|
| 409 |
+
Yields
|
| 410 |
+
------
|
| 411 |
+
tuple[str, Any]
|
| 412 |
+
- "messages": list of chat message objects (e.g. AIMessage)
|
| 413 |
+
- "values": dict of current agent state (messages, script, context)
|
| 414 |
+
|
| 415 |
+
Example
|
| 416 |
+
-------
|
| 417 |
+
messages [AIMessage(content="```python\nresult = 3*7+5\nprint(result)\n```")]
|
| 418 |
+
values {"messages": [...], "script": "result = 3*7+5\nprint(result)", "context": {}}
|
| 419 |
+
messages [AIMessage(content="26")]
|
| 420 |
+
values {"messages": [...], "script": None, "context": {"result": 26}}
|
| 421 |
+
"""
|
| 422 |
+
|
| 423 |
+
config = {
|
| 424 |
+
"configurable": {
|
| 425 |
+
"thread_id": thread_id
|
| 426 |
+
}
|
| 427 |
+
}
|
| 428 |
+
for typ, chunk in self.compiled_agent.stream(
|
| 429 |
+
{"messages": messages},
|
| 430 |
+
stream_mode=["values", "messages"],
|
| 431 |
+
config=config,
|
| 432 |
+
):
|
| 433 |
+
yield TokenStream(type=typ, data=chunk)
|
| 434 |
+
|
| 435 |
+
#------- BEFORE DB AGENT EXECUTOR -------#
|
| 436 |
+
#def generate(
|
| 437 |
+
# self,
|
| 438 |
+
# messages: list[dict],
|
| 439 |
+
# thread_id: int = 1
|
| 440 |
+
#) -> dict[str, Any]:
|
| 441 |
+
# """
|
| 442 |
+
# Run the agent to completion and return final state.#
|
| 443 |
+
|
| 444 |
+
# Returns
|
| 445 |
+
# -------
|
| 446 |
+
# dict
|
| 447 |
+
# Final agent state containing messages, script, context.
|
| 448 |
+
# """
|
| 449 |
+
# config = {
|
| 450 |
+
# "configurable": {
|
| 451 |
+
# "thread_id": thread_id
|
| 452 |
+
# }
|
| 453 |
+
# }
|
| 454 |
+
# final_state = self.compiled_agent.generate(
|
| 455 |
+
# {"messages": messages},
|
| 456 |
+
# config=config,
|
| 457 |
+
# )
|
| 458 |
+
# return final_state
|
| 459 |
+
#------- BEFORE DB AGENT EXECUTOR -------#
|
| 460 |
+
def generate(
|
| 461 |
+
self,
|
| 462 |
+
messages: list[dict],
|
| 463 |
+
thread_id: int = 1,
|
| 464 |
+
context: Optional[dict[str, Any]] = None,
|
| 465 |
+
) -> dict[str, Any]:
|
| 466 |
+
"""
|
| 467 |
+
*** Test method for db executor ***
|
| 468 |
+
"""
|
| 469 |
+
config = {
|
| 470 |
+
"configurable": {"thread_id": thread_id}
|
| 471 |
+
}
|
| 472 |
+
state = {
|
| 473 |
+
"messages": messages, "context": context or {}
|
| 474 |
+
}
|
| 475 |
+
return self.compiled_agent.invoke( #TODO: note changed from generate to invoke, hope it works
|
| 476 |
+
state, config=config
|
| 477 |
+
)
|
| 478 |
+
|
| 479 |
+
|
| 480 |
+
|
| 481 |
+
|
| 482 |
+
if __name__ == "__main__":
|
| 483 |
+
"""
|
| 484 |
+
Run the CodeActAgent in different modes:
|
| 485 |
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
| 486 |
+
- python -m agent.core.codeact --mode chat
|
| 487 |
+
- python -m agent.core.codeact --mode debug
|
| 488 |
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
| 489 |
+
"""
|
| 490 |
+
import argparse
|
| 491 |
+
import json
|
| 492 |
+
from rich.console import Console
|
| 493 |
+
|
| 494 |
+
# Validate environment (api key) before doing *anything* else
|
| 495 |
+
OpenAIApiKey.validate_environment()
|
| 496 |
+
|
| 497 |
+
# --- Parse args ---
|
| 498 |
+
parser = argparse.ArgumentParser(description="Run CodeActAgent in different modes")
|
| 499 |
+
parser.add_argument(
|
| 500 |
+
"--mode",
|
| 501 |
+
choices=["chat", "debug"],
|
| 502 |
+
default="chat",
|
| 503 |
+
help="Mode: 'chat' for normal conversation, 'debug' to also show state values."
|
| 504 |
+
)
|
| 505 |
+
args = parser.parse_args()
|
| 506 |
+
|
| 507 |
+
# --- Instantiate agent ---
|
| 508 |
+
agent = CodeActAgent(
|
| 509 |
+
model_name="gpt-4o",
|
| 510 |
+
model_provider="openai",
|
| 511 |
+
tools=[],
|
| 512 |
+
eval_fn=CodeActAgent.default_eval, # built-in evaluator
|
| 513 |
+
system_prompt="agent/prompts/local_archive/original.txt",
|
| 514 |
+
bind_tools=False,
|
| 515 |
+
memory=True
|
| 516 |
+
)
|
| 517 |
+
#~~~~~~~~~~~~~~~~~~~~~~~~~~#
|
| 518 |
+
# --- Conversation loop ---#
|
| 519 |
+
#~~~~~~~~~~~~~~~~~~~~~~~~~~#
|
| 520 |
+
# --- Rich console setup ---
|
| 521 |
+
console = Console(width=100, soft_wrap=False)
|
| 522 |
+
|
| 523 |
+
while True:
|
| 524 |
+
user_query = input("\n😎 USER:\n››› ")
|
| 525 |
+
if user_query.lower() == "exit":
|
| 526 |
+
break
|
| 527 |
+
|
| 528 |
+
messages = [{"role": "user", "content": user_query}]
|
| 529 |
+
|
| 530 |
+
# --- Dynamic assistant header (chat only) ---
|
| 531 |
+
if args.mode == "chat":
|
| 532 |
+
console.print("\n🧠 [bold magenta]Assistant[/]:\n››› ", end="")
|
| 533 |
+
|
| 534 |
+
# --- Stream agent responses ---
|
| 535 |
+
for typ, chunk in agent.stream(messages):
|
| 536 |
+
if args.mode == "chat" and typ == "messages":
|
| 537 |
+
print(chunk[0].content, end="", flush=True)
|
| 538 |
+
|
| 539 |
+
elif args.mode == "debug":
|
| 540 |
+
if typ == "values":
|
| 541 |
+
# Print only the nicely formatted message + optional context
|
| 542 |
+
pretty_print_state(chunk, show_context=False)
|
| 543 |
+
|
| 544 |
+
print("\n")
|
| 545 |
+
|
src/backend/agents/db_executor/codeact/prompts/local_archive/original.txt
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
You are a helpful assistant. You are encouraged to generate Python code for calculations.
|
| 3 |
+
|
| 4 |
+
You will be given a task to perform. You should output either
|
| 5 |
+
- a Python code snippet that provides the solution to the task, or a step towards the solution. Any output you want
|
| 6 |
+
to extract from the code should be printed to the console. Code should be output in a fenced code block.
|
| 7 |
+
- text to be shown directly to the user, if you want to ask for more information or provide the final answer.
|
| 8 |
+
|
| 9 |
+
In addition to the Python Standard Library, you can use the following functions:
|
| 10 |
+
|
| 11 |
+
{tools}
|
| 12 |
+
|
| 13 |
+
Variables defined at the top level of previous code snippets can be referenced in your code.
|
| 14 |
+
|
| 15 |
+
When you include a code block, put a blank line after the closing triple backticks
|
| 16 |
+
before any further text.
|
| 17 |
+
|
| 18 |
+
Reminder: use Python code snippets to call tools.
|
src/backend/agents/db_executor/codeact/prompts/local_archive/test.txt
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
You are a helpful assistant that can solve tasks using Python code and a set of predefined tools.
|
| 2 |
+
|
| 3 |
+
=== RULES ===
|
| 4 |
+
1. CODE BLOCKS:
|
| 5 |
+
- Always use triple backticks: ```python ... ```
|
| 6 |
+
- Never include natural language inside code blocks.
|
| 7 |
+
- Comments (#) are allowed but should be minimal.
|
| 8 |
+
before any further text.
|
| 9 |
+
|
| 10 |
+
2. OUTPUT EXPLANATION:
|
| 11 |
+
- After each code block, provide a brief natural language explanation.
|
| 12 |
+
- Use code outputs in your response.
|
| 13 |
+
- Keep explanations separate from code.
|
| 14 |
+
|
| 15 |
+
Note:
|
| 16 |
+
When you include a code block, put a blank line after the closing triple backticks
|
| 17 |
+
before any further text.
|
| 18 |
+
|
| 19 |
+
=== VALID EXAMPLE ===
|
| 20 |
+
```python
|
| 21 |
+
# Calculate the product
|
| 22 |
+
result = multiply(15, 23)
|
| 23 |
+
print(result)
|
| 24 |
+
```
|
| 25 |
+
The calculation shows that 15 multiplied by 23 equals 345.
|
src/backend/agents/db_executor/codeact/prompts/prompt_layer.py
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
PromptLayer Integration for Prompt Management
|
| 4 |
+
==============================================
|
| 5 |
+
|
| 6 |
+
This module provides a centralized way to manage prompts using PromptLayer platform.
|
| 7 |
+
Allows for versioned, labeled prompts that can be easily updated without code changes.
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
import promptlayer
|
| 11 |
+
from promptlayer import PromptLayer
|
| 12 |
+
from dotenv import load_dotenv
|
| 13 |
+
import os
|
| 14 |
+
from typing import Dict, Any, Optional
|
| 15 |
+
from functools import lru_cache
|
| 16 |
+
|
| 17 |
+
load_dotenv()
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
class PromptManager:
|
| 21 |
+
"""
|
| 22 |
+
Centralized prompt management using PromptLayer platform.
|
| 23 |
+
link:
|
| 24 |
+
- https://www.promptlayer.com
|
| 25 |
+
|
| 26 |
+
Features:
|
| 27 |
+
- Version control for prompts
|
| 28 |
+
- Environment-based prompt labels (dev, staging, production)
|
| 29 |
+
- Caching for performance
|
| 30 |
+
- Fallback to local files if PromptLayer unavailable
|
| 31 |
+
"""
|
| 32 |
+
|
| 33 |
+
def __init__(self, api_key: Optional[str] = None, environment: str = "production"):
|
| 34 |
+
"""
|
| 35 |
+
Initialize PromptManager.
|
| 36 |
+
|
| 37 |
+
Args:
|
| 38 |
+
api_key: PromptLayer API key (defaults to PROMPTLAYER_API_KEY env var)
|
| 39 |
+
environment: Environment label for prompts (dev, staging, production)
|
| 40 |
+
"""
|
| 41 |
+
self.api_key = api_key or os.getenv("PROMPTLAYER_API_KEY")
|
| 42 |
+
self.environment = environment
|
| 43 |
+
self.client = None
|
| 44 |
+
|
| 45 |
+
# Initialize client if API key is available
|
| 46 |
+
if self.api_key:
|
| 47 |
+
try:
|
| 48 |
+
self.client = PromptLayer(api_key=self.api_key)
|
| 49 |
+
print(f"✅ PromptLayer connected (environment: {environment})")
|
| 50 |
+
|
| 51 |
+
except Exception as e:
|
| 52 |
+
print(f"⚠️ PromptLayer connection failed: {e}")
|
| 53 |
+
self.client = None
|
| 54 |
+
else:
|
| 55 |
+
print("⚠️ No PROMPTLAYER_API_KEY found, using local fallback")
|
| 56 |
+
|
| 57 |
+
@lru_cache(maxsize=128)
|
| 58 |
+
def get_prompt(
|
| 59 |
+
self,
|
| 60 |
+
template_name: str,
|
| 61 |
+
version: Optional[int] = None,
|
| 62 |
+
label: Optional[str] = None,
|
| 63 |
+
fallback_path: Optional[str] = None
|
| 64 |
+
) -> str:
|
| 65 |
+
"""
|
| 66 |
+
Get a prompt from PromptLayer with fallback to local file.
|
| 67 |
+
|
| 68 |
+
Args:
|
| 69 |
+
template_name: Name of the prompt template
|
| 70 |
+
version: Specific version number (defaults to latest)
|
| 71 |
+
label: Environment label (defaults to instance environment)
|
| 72 |
+
fallback_path: Local file path if PromptLayer unavailable
|
| 73 |
+
|
| 74 |
+
Returns:
|
| 75 |
+
Prompt content as string
|
| 76 |
+
|
| 77 |
+
Raises:
|
| 78 |
+
ValueError: If prompt cannot be found and no fallback provided
|
| 79 |
+
"""
|
| 80 |
+
# Use provided label or instance default
|
| 81 |
+
label = label or self.environment
|
| 82 |
+
|
| 83 |
+
# Try PromptLayer first
|
| 84 |
+
if self.client:
|
| 85 |
+
try:
|
| 86 |
+
template_config = {
|
| 87 |
+
"label": label
|
| 88 |
+
}
|
| 89 |
+
if version:
|
| 90 |
+
template_config["version"] = version
|
| 91 |
+
|
| 92 |
+
prompttemplate = self.client.templates.get(
|
| 93 |
+
template_name,
|
| 94 |
+
template_config
|
| 95 |
+
)
|
| 96 |
+
# Extract prompt content from response
|
| 97 |
+
prompt_content = prompttemplate["llm_kwargs"]["messages"][0]["content"]
|
| 98 |
+
print(f"📋 Loaded prompt '{template_name}' from PromptLayer (v{prompttemplate.get('version', 'latest')}, {label})")
|
| 99 |
+
return prompt_content
|
| 100 |
+
|
| 101 |
+
except Exception as e:
|
| 102 |
+
print(f"⚠️ PromptLayer failed: {e}, trying fallback...")
|
| 103 |
+
# Fall through to fallback instead of raising
|
| 104 |
+
|
| 105 |
+
# Fallback to local file
|
| 106 |
+
if fallback_path:
|
| 107 |
+
try:
|
| 108 |
+
with open(fallback_path, 'r') as f:
|
| 109 |
+
content = f.read()
|
| 110 |
+
print(f"📂 Loaded prompt '{template_name}' from local file: {fallback_path}")
|
| 111 |
+
return content
|
| 112 |
+
except Exception as e:
|
| 113 |
+
raise ValueError(
|
| 114 |
+
f"❌ Failed to load fallback file '{fallback_path}': {e}"
|
| 115 |
+
)
|
| 116 |
+
|
| 117 |
+
# Only raise if both PromptLayer AND fallback fail
|
| 118 |
+
raise ValueError(
|
| 119 |
+
f"Could not load prompt '{template_name}' from any source"
|
| 120 |
+
)
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
def list_available_prompts(self) -> Dict[str, Any]:
|
| 124 |
+
"""
|
| 125 |
+
List all available prompts from PromptLayer.
|
| 126 |
+
|
| 127 |
+
Returns:
|
| 128 |
+
Dictionary of available prompts with metadata
|
| 129 |
+
"""
|
| 130 |
+
if not self.client:
|
| 131 |
+
return {"error": "PromptLayer client not available"}
|
| 132 |
+
|
| 133 |
+
try:
|
| 134 |
+
# This would depend on PromptLayer's API for listing templates
|
| 135 |
+
# Placeholder implementation
|
| 136 |
+
return {
|
| 137 |
+
"message": "PromptLayer template listing not implemented in this version",
|
| 138 |
+
"available_methods": [
|
| 139 |
+
"get_judge_prompt(simple=True/False)",
|
| 140 |
+
"get_agent_prompt(version=int)",
|
| 141 |
+
"get_prompt(template_name, version, label, fallback_path)"
|
| 142 |
+
]
|
| 143 |
+
}
|
| 144 |
+
except Exception as e:
|
| 145 |
+
return {"error": f"Failed to list prompts: {e}"}
|
| 146 |
+
|
| 147 |
+
def clear_cache(self):
|
| 148 |
+
"""Clear the prompt cache."""
|
| 149 |
+
self.get_prompt.cache_clear()
|
| 150 |
+
print("🗑️ Prompt cache cleared")
|
| 151 |
+
|
| 152 |
+
def set_environment(self, environment: str):
|
| 153 |
+
"""
|
| 154 |
+
Change the environment label for subsequent prompt requests.
|
| 155 |
+
|
| 156 |
+
Args:
|
| 157 |
+
environment: New environment (dev, staging, production)
|
| 158 |
+
"""
|
| 159 |
+
self.environment = environment
|
| 160 |
+
self.clear_cache() # Clear cache since environment changed
|
| 161 |
+
print(f"🔄 Environment changed to: {environment}")
|
| 162 |
+
|
src/backend/agents/db_executor/codeact/schemas/__init__.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Init file for pydantic schemas.
|
| 2 |
+
"""
|
| 3 |
+
|
| 4 |
+
from .openai_key import OpenAIApiKey
|
| 5 |
+
from .stream import TokenStream
|
| 6 |
+
|
| 7 |
+
__all__ = [
|
| 8 |
+
"OpenAIApiKey",
|
| 9 |
+
"TokenStream",
|
| 10 |
+
]
|
src/backend/agents/db_executor/codeact/schemas/openai_key.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from pydantic import Field, ConfigDict, field_validator
|
| 3 |
+
from pydantic_settings import BaseSettings
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
from dotenv import load_dotenv
|
| 6 |
+
from pydantic import ValidationError
|
| 7 |
+
import sys
|
| 8 |
+
|
| 9 |
+
# Load environment variables
|
| 10 |
+
load_dotenv()
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class OpenAIApiKey(BaseSettings):
|
| 14 |
+
"""Schema for validating and loading the OpenAI API key configuration.
|
| 15 |
+
"""
|
| 16 |
+
model_config = ConfigDict(
|
| 17 |
+
title="OpenAI API Key Schema",
|
| 18 |
+
description="Validates and loads the OpenAI API key from environment variables.",
|
| 19 |
+
)
|
| 20 |
+
api_key: str = Field(
|
| 21 |
+
..., # >>> required field
|
| 22 |
+
title="OpenAI API Key",
|
| 23 |
+
description="API key for OpenAI authentication.",
|
| 24 |
+
examples=["sk-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"],
|
| 25 |
+
alias="OPENAI_API_KEY",
|
| 26 |
+
)
|
| 27 |
+
|
| 28 |
+
@field_validator("api_key")
|
| 29 |
+
@classmethod
|
| 30 |
+
def validate_openai_api_key(cls, v: str) -> str:
|
| 31 |
+
"""Validate that the API key is present and has the correct format.
|
| 32 |
+
"""
|
| 33 |
+
if not v:
|
| 34 |
+
raise ValueError(
|
| 35 |
+
"💥 Missing `OPENAI_API_KEY` environment variable."
|
| 36 |
+
)
|
| 37 |
+
if not v.startswith("sk-"):
|
| 38 |
+
raise ValueError(
|
| 39 |
+
"💥 Invalid `OPENAI_API_KEY` — must start with 'sk-'."
|
| 40 |
+
)
|
| 41 |
+
return v
|
| 42 |
+
|
| 43 |
+
@classmethod
|
| 44 |
+
def validate_environment(cls) -> "OpenAIApiKey":
|
| 45 |
+
"""
|
| 46 |
+
Load .env from the root directory
|
| 47 |
+
and validate that the API key is present and valid.
|
| 48 |
+
"""
|
| 49 |
+
try:
|
| 50 |
+
# Pydantic auto-loads .env and validates
|
| 51 |
+
config = cls()
|
| 52 |
+
os.environ["OPENAI_API_KEY"] = config.api_key # Set for runtime access
|
| 53 |
+
return config
|
| 54 |
+
except ValidationError as e:
|
| 55 |
+
print(f"💥 OpenAI API key misconfiguration:\n{e}")
|
| 56 |
+
sys.exit(1)
|
src/backend/agents/db_executor/codeact/schemas/stream.py
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import NamedTuple, Literal, Union, Any
|
| 2 |
+
from langchain_core.messages import AIMessage
|
| 3 |
+
|
| 4 |
+
class TokenStream(NamedTuple):
|
| 5 |
+
"""Represents a single streamed update emitted by the agent.
|
| 6 |
+
"""
|
| 7 |
+
type: Literal["messages", "values"]
|
| 8 |
+
data: Union[list[AIMessage], dict[str, Any]]
|
src/backend/agents/db_executor/codeact/states/state.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from langgraph.graph import END, START, MessagesState
|
| 2 |
+
from typing import Optional, Any
|
| 3 |
+
|
| 4 |
+
class CodeActState(MessagesState):
|
| 5 |
+
"""State for CodeAct agent."""
|
| 6 |
+
|
| 7 |
+
script: Optional[str]
|
| 8 |
+
"""The Python code script to be executed."""
|
| 9 |
+
context: dict[str, Any]
|
| 10 |
+
"""Dictionary containing the execution context with available tools and variables."""
|
src/backend/agents/db_executor/codeact/tools/__init__.py
ADDED
|
File without changes
|
src/backend/agents/db_executor/codeact/tools/tools.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import inspect
|
| 2 |
+
from langchain_core.tools import StructuredTool
|
| 3 |
+
from typing import Optional
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
|
| 6 |
+
# Example tools
|
| 7 |
+
def add(a: float, b: float) -> float:
|
| 8 |
+
"""Add two numbers together."""
|
| 9 |
+
return a + b
|
| 10 |
+
|
| 11 |
+
def multiply(a: float, b: float) -> float:
|
| 12 |
+
"""Multiply two numbers together."""
|
| 13 |
+
return a * b
|
| 14 |
+
|
| 15 |
+
def divide(a: float, b: float) -> float:
|
| 16 |
+
"""Divide two numbers."""
|
| 17 |
+
return a / b
|
| 18 |
+
|
| 19 |
+
def subtract(a: float, b: float) -> float:
|
| 20 |
+
"""Subtract two numbers."""
|
| 21 |
+
return a - b
|
| 22 |
+
|
| 23 |
+
# Prompt creation
|
| 24 |
+
def create_default_prompt(
|
| 25 |
+
tools: list,
|
| 26 |
+
system_prompt: Optional[str] = None,
|
| 27 |
+
base_prompt: str = "original.txt",
|
| 28 |
+
) -> str:
|
| 29 |
+
template_path = Path(__file__).parent.parent / "prompts" / base_prompt
|
| 30 |
+
template = template_path.read_text()
|
| 31 |
+
|
| 32 |
+
tool_strings = []
|
| 33 |
+
for t in tools:
|
| 34 |
+
func = t.func if isinstance(t, StructuredTool) else t
|
| 35 |
+
sig = inspect.signature(func)
|
| 36 |
+
doc = (func.__doc__ or "").strip()
|
| 37 |
+
tool_strings.append(
|
| 38 |
+
f"def {func.__name__}{sig}:\n \"\"\"{doc}\"\"\"\n ..."
|
| 39 |
+
)
|
| 40 |
+
tools_str = "\n\n".join(tool_strings)
|
| 41 |
+
|
| 42 |
+
prompt = template.replace("{tools}", tools_str)
|
| 43 |
+
|
| 44 |
+
if system_prompt:
|
| 45 |
+
prompt = f"{system_prompt}\n\n{prompt}"
|
| 46 |
+
|
| 47 |
+
return prompt
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
if __name__ == "__main__":
|
| 52 |
+
tools = [multiply, divide, subtract]
|
| 53 |
+
print(create_default_prompt(tools, system_prompt="You are a coding agent."))
|
src/backend/agents/db_executor/codeact/utils/__init__.py
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Utility functions for the agent."""
|
| 2 |
+
|
| 3 |
+
from .pretty_state import pretty_print_state
|
| 4 |
+
|
| 5 |
+
__all__ = ["pretty_print_state"]
|
src/backend/agents/db_executor/codeact/utils/pretty_state.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
from rich.console import Console
|
| 3 |
+
from rich.syntax import Syntax
|
| 4 |
+
from rich.panel import Panel
|
| 5 |
+
from langchain_core.messages import HumanMessage, AIMessage, ToolMessage
|
| 6 |
+
|
| 7 |
+
console = Console(width=100, soft_wrap=False)
|
| 8 |
+
|
| 9 |
+
_last_context_snapshot = None # used to suppress repeated context
|
| 10 |
+
_last_message_ids = set() # track printed messages
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def serialize_message(msg) -> dict:
|
| 15 |
+
"""Convert LangChain message objects into serializable dicts."""
|
| 16 |
+
if hasattr(msg, "dict"):
|
| 17 |
+
return msg.dict()
|
| 18 |
+
elif hasattr(msg, "__dict__"):
|
| 19 |
+
return {k: serialize_message(v) for k, v in msg.__dict__.items()}
|
| 20 |
+
elif isinstance(msg, list):
|
| 21 |
+
return [serialize_message(v) for v in msg]
|
| 22 |
+
elif isinstance(msg, dict):
|
| 23 |
+
return {k: serialize_message(v) for k, v in msg.items()}
|
| 24 |
+
else:
|
| 25 |
+
return msg
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def pretty_print_state(state: dict, show_context: bool = True) -> None:
|
| 29 |
+
"""
|
| 30 |
+
Pretty-print the agent's state in a clean, color-coded way.
|
| 31 |
+
|
| 32 |
+
Parameters
|
| 33 |
+
----------
|
| 34 |
+
state : dict
|
| 35 |
+
The LangGraph agent state chunk (from the stream).
|
| 36 |
+
show_context : bool, optional
|
| 37 |
+
Whether to display the context (default True).
|
| 38 |
+
If True, only shows context when it has changed since last call.
|
| 39 |
+
"""
|
| 40 |
+
global _last_context_snapshot
|
| 41 |
+
|
| 42 |
+
# --- Display message chunks ---
|
| 43 |
+
for msg in state.get("messages", []):
|
| 44 |
+
|
| 45 |
+
msg_id = getattr(msg, "id", id(msg))
|
| 46 |
+
if msg_id in _last_message_ids:
|
| 47 |
+
continue # skip duplicates
|
| 48 |
+
_last_message_ids.add(msg_id)
|
| 49 |
+
|
| 50 |
+
msg_dict = serialize_message(msg)
|
| 51 |
+
msg_json = json.dumps(msg_dict, indent=2)
|
| 52 |
+
|
| 53 |
+
if isinstance(msg, HumanMessage):
|
| 54 |
+
color, title = "cyan", "🧑 HumanMessage"
|
| 55 |
+
elif isinstance(msg, ToolMessage):
|
| 56 |
+
color, title = "yellow", f"🧰 ToolMessage ({msg_dict.get('name','?')})"
|
| 57 |
+
elif isinstance(msg, AIMessage):
|
| 58 |
+
color, title = "magenta", "🤖 AIMessage"
|
| 59 |
+
else:
|
| 60 |
+
color, title = "white", "Other"
|
| 61 |
+
|
| 62 |
+
syntax = Syntax(msg_json, "json", theme="monokai", line_numbers=False)
|
| 63 |
+
console.print(Panel(syntax, title=title, border_style=color))
|
| 64 |
+
|
| 65 |
+
# --- Optional context view ---
|
| 66 |
+
#if show_context:
|
| 67 |
+
# context = state.get("context", {})
|
| 68 |
+
# if context and context != _last_context_snapshot:
|
| 69 |
+
# _last_context_snapshot = context.copy() # cache for next comparison
|
| 70 |
+
|
| 71 |
+
# context_json = json.dumps(context, indent=2, default=str)
|
| 72 |
+
# syntax = Syntax(context_json, "json", theme="monokai", line_numbers=False)
|
| 73 |
+
# console.print(Panel(syntax, title="🧠 Context (updated)", border_style="green"))
|
src/backend/agents/db_executor/db_executor.py
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from .codeact.core.codeact import CodeActAgent
|
| 2 |
+
from src.backend.database.candidates.client import SessionLocal
|
| 3 |
+
from src.backend.database.candidates.models import (
|
| 4 |
+
Candidate,
|
| 5 |
+
CVScreeningResult,
|
| 6 |
+
VoiceScreeningResult,
|
| 7 |
+
InterviewScheduling,
|
| 8 |
+
FinalDecision,
|
| 9 |
+
)
|
| 10 |
+
from src.backend.state.candidate import CandidateStatus, InterviewStatus, DecisionStatus
|
| 11 |
+
from langchain_core.tools import tool
|
| 12 |
+
from typing import Dict, Any
|
| 13 |
+
from src.backend.database.candidates import evaluate_cv_screening_decision
|
| 14 |
+
from src.backend.prompts import get_prompt
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
SYSTEM_PROMPT = get_prompt(
|
| 18 |
+
template_name="DB_Executor",
|
| 19 |
+
local_prompt_path="db_executor/v2.txt",
|
| 20 |
+
)
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
@tool
|
| 24 |
+
def db_executor(query: str) -> str:
|
| 25 |
+
"""
|
| 26 |
+
Consumes a natural-language query as input which is being translated into
|
| 27 |
+
SQLAlchemy ORM code by the coding agent. Finally, the code is executed against
|
| 28 |
+
the database and the result is returned.
|
| 29 |
+
|
| 30 |
+
Args:
|
| 31 |
+
query (str): Natural-language database query.
|
| 32 |
+
Returns:
|
| 33 |
+
str: The natural language summary of the result or error.
|
| 34 |
+
"""
|
| 35 |
+
# 1. Initialize DB session and ORM context
|
| 36 |
+
session = SessionLocal()
|
| 37 |
+
context = {
|
| 38 |
+
"session": session,
|
| 39 |
+
"Candidate": Candidate,
|
| 40 |
+
"CVScreeningResult": CVScreeningResult,
|
| 41 |
+
"VoiceScreeningResult": VoiceScreeningResult,
|
| 42 |
+
"InterviewScheduling": InterviewScheduling,
|
| 43 |
+
"FinalDecision": FinalDecision,
|
| 44 |
+
"CandidateStatus": CandidateStatus,
|
| 45 |
+
"InterviewStatus": InterviewStatus,
|
| 46 |
+
"DecisionStatus": DecisionStatus,
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
try:
|
| 50 |
+
# 2. Initialize CodeAct agent with system prompt
|
| 51 |
+
agent = CodeActAgent(
|
| 52 |
+
model_name="gpt-4o",
|
| 53 |
+
model_provider="openai",
|
| 54 |
+
tools=[evaluate_cv_screening_decision], # Passed as a tool
|
| 55 |
+
eval_fn=CodeActAgent.default_eval,
|
| 56 |
+
system_prompt=SYSTEM_PROMPT,
|
| 57 |
+
bind_tools=True, # Enable tool binding so agent sees signature
|
| 58 |
+
memory=False, # optional — can enable if you want persistent thread context
|
| 59 |
+
)
|
| 60 |
+
|
| 61 |
+
# 3. Run natural-language query
|
| 62 |
+
messages = [{"role": "user", "content": query}]
|
| 63 |
+
final_state = agent.generate(messages, context=context)
|
| 64 |
+
|
| 65 |
+
# 4. Extract model output
|
| 66 |
+
# Return the final natural language response from the assistant
|
| 67 |
+
output_msg = final_state["messages"][-1].content if final_state.get("messages") else ""
|
| 68 |
+
|
| 69 |
+
return output_msg
|
| 70 |
+
|
| 71 |
+
except Exception as e:
|
| 72 |
+
import traceback
|
| 73 |
+
error_trace = traceback.format_exc()
|
| 74 |
+
print(f"\n❌ Error in db_executor: {e}\n{error_trace}")
|
| 75 |
+
|
| 76 |
+
# Return a clear text error message
|
| 77 |
+
return f"The DB Executor encountered an internal error: {str(e)}"
|
| 78 |
+
|
| 79 |
+
finally:
|
| 80 |
+
session.close()
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
if __name__ == "__main__":
|
| 85 |
+
from rich.console import Console
|
| 86 |
+
from rich.panel import Panel
|
| 87 |
+
|
| 88 |
+
console = Console()
|
| 89 |
+
query = "Fetch all candidates and their status."
|
| 90 |
+
|
| 91 |
+
console.rule("[bold magenta]DB Executor Test Run[/bold magenta]")
|
| 92 |
+
console.print(f"[cyan]Query:[/] {query}\n")
|
| 93 |
+
|
| 94 |
+
result = db_executor(query)
|
| 95 |
+
|
| 96 |
+
# 🧠 Show model result nicely
|
| 97 |
+
console.print(Panel.fit(result, title="🧠 Model Output", border_style="blue"))
|
| 98 |
+
|
| 99 |
+
console.rule("[bold green]End of Execution[/bold green]")
|
src/backend/agents/db_executor/info.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
This agent coding agent based `CodeAct`agent pattern, see:
|
| 2 |
+
https://github.com/langchain-ai/langgraph-codeact
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
Test as follows:
|
| 6 |
+
|
| 7 |
+
>>> cd /Users/sebastianwefers/Desktop/projects/recruitment-agent
|
| 8 |
+
|
| 9 |
+
>>> docker compose -f docker/docker-compose.yml up --build candidates_db_init
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
# Make sure your OpenAI key is available to the process
|
| 13 |
+
>>> export OPENAI_API_KEY=sk-... # or however you normally set it
|
| 14 |
+
|
| 15 |
+
# Override host so the Python code connects to localhost, not 'db' and run "db_executor"
|
| 16 |
+
>>> POSTGRES_HOST=localhost POSTGRES_PORT=5433 python -m src.agents.db_executor.db_executor
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
# DEBUG attempt
|
| 20 |
+
------------------------------------------------------------------------------------
|
| 21 |
+
- works:
|
| 22 |
+
POSTGRES_HOST=localhost POSTGRES_PORT=5433 python src/agents/db_executor/debug_db_connection.py
|
src/backend/agents/example/info.md
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
### How to Run the LangGraph Reasoning Monitoring Demo Agent
|
| 2 |
+
|
| 3 |
+
1. Make sure to have the follwijg installed
|
| 4 |
+
```bash
|
| 5 |
+
pip install -r requriements/dev.txt
|
| 6 |
+
```
|
| 7 |
+
|
| 8 |
+
2. Set TAVILY_API_KEY:
|
| 9 |
+
- link: https://www.tavily.com
|
| 10 |
+
|
| 11 |
+
3. Run the following from repo root:
|
| 12 |
+
```bash
|
| 13 |
+
export PYTHONPATH=./src
|
| 14 |
+
langgraph dev
|
| 15 |
+
```
|
| 16 |
+
This loads the root-level `langgraph.json` and makes all agents available in LangGraph Studio.
|
| 17 |
+
|
| 18 |
+
4 Open the Studio UI
|
| 19 |
+
After the server starts, open:
|
| 20 |
+
```bash
|
| 21 |
+
https://smith.langchain.com/studio/?baseUrl=http://127.0.0.1:2024
|
| 22 |
+
```
|
| 23 |
+
**NOTE:** Open it in anything, but safari!
|
| 24 |
+
|
| 25 |
+
Select the agent named react_agent (or whichever your config specifies).
|
| 26 |
+
|
| 27 |
+
---
|
| 28 |
+
|
| 29 |
+
### Demo Prompt to Use
|
| 30 |
+
Paste the following into the Studio console:
|
| 31 |
+
```txt
|
| 32 |
+
First search for the current temperature in Fahrenheit in Cape Town, South Africa.
|
| 33 |
+
Then convert that temperature to Celsius using the conversion tool.
|
| 34 |
+
```
|
| 35 |
+
|
| 36 |
+
***This triggers:***
|
| 37 |
+
1. A Tavily search for the current Fahrenheit temperature
|
| 38 |
+
2. A tool call to convert Fahrenheit → Celsius
|
| 39 |
+
3. Full ReAct reasoning + tool trace in the UI
|
| 40 |
+
|
| 41 |
+
---
|
| 42 |
+
|
| 43 |
+
### ⚙️ Multiple Agents in langgraph.json
|
| 44 |
+
You can expose multiple agents to LangGraph Studio by listing them under the graphs section of your root `langgraph.json`.
|
| 45 |
+
|
| 46 |
+
Example:
|
| 47 |
+
```json
|
| 48 |
+
{
|
| 49 |
+
"dependencies": ["src"],
|
| 50 |
+
"graphs": {
|
| 51 |
+
"react_agent": "agents.example.react_agent:agent",
|
| 52 |
+
"cv_screener": "agents.cv_screening.screener:agent",
|
| 53 |
+
"supervisor": "agents.supervisor.supervisor:agent"
|
| 54 |
+
}
|
| 55 |
+
}
|
| 56 |
+
```
|
| 57 |
+
Each entry maps:
|
| 58 |
+
```bash
|
| 59 |
+
"graph_name": "module.path:object_name"
|
| 60 |
+
```
|
| 61 |
+
|
| 62 |
+
Where:
|
| 63 |
+
- `graph_name` → appears in LangGraph Studio
|
| 64 |
+
- `module.path` → Python import path under `src/`
|
| 65 |
+
- `object_name` → the variable that contains the graph/agent
|
| 66 |
+
This allows one project to host many agents simultaneously (e.g., supervisor, tools agent, CV-screening agent, etc.).
|
src/backend/agents/example/react_agent.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Simple React Agent implementation with monitoring capabilities.
|
| 3 |
+
|
| 4 |
+
- React agent:
|
| 5 |
+
- https://docs.langchain.com/oss/python/langchain/agents
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
install:
|
| 9 |
+
- langgraph-cli
|
| 10 |
+
|
| 11 |
+
Run as follows:
|
| 12 |
+
>>> cd src/agents/example/
|
| 13 |
+
>>> langgraph dev
|
| 14 |
+
|
| 15 |
+
"""
|
| 16 |
+
from langchain.agents import create_agent
|
| 17 |
+
from langchain_tavily import TavilySearch
|
| 18 |
+
from langchain_core.tools import tool
|
| 19 |
+
from dotenv import load_dotenv
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
load_dotenv()
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
# --- Tools ---
|
| 28 |
+
@tool
|
| 29 |
+
def convert_fahrenheit_celsius(fahrenheit: float) -> float:
|
| 30 |
+
"""
|
| 31 |
+
Convert fahrenheit to celsius.
|
| 32 |
+
Args:
|
| 33 |
+
fahrenheit (float): Temperature in fahrenheit.
|
| 34 |
+
Returns:
|
| 35 |
+
float: Temperature in celsius.
|
| 36 |
+
"""
|
| 37 |
+
return (fahrenheit - 32) * 5.0/9.0
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
web_search = TavilySearch(
|
| 42 |
+
max_results = 5,
|
| 43 |
+
topic = "general",
|
| 44 |
+
# include_answer = False,
|
| 45 |
+
# include_raw_content = False,
|
| 46 |
+
# ...
|
| 47 |
+
)
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
tools = [
|
| 51 |
+
web_search,
|
| 52 |
+
convert_fahrenheit_celsius
|
| 53 |
+
]
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
agent = create_agent(
|
| 57 |
+
"gpt-5",
|
| 58 |
+
tools=tools
|
| 59 |
+
)
|
src/backend/agents/gcalendar/__init__.py
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from .gcalendar_agent import gcalendar_agent
|
| 2 |
+
|
src/backend/agents/gcalendar/gcalendar_agent.py
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import asyncio
|
| 2 |
+
import sys
|
| 3 |
+
from langchain_core.tools import tool
|
| 4 |
+
from langchain_mcp_adapters.client import MultiServerMCPClient
|
| 5 |
+
from langchain.agents import create_agent
|
| 6 |
+
from langchain_openai import ChatOpenAI
|
| 7 |
+
from src.mcp_servers.examples.gcalendar.settings import GoogleCalendarSettings
|
| 8 |
+
from src.backend.prompts import get_prompt
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
SYSTEM_PROMPT = get_prompt(
|
| 12 |
+
template_name="GCalendar",
|
| 13 |
+
latest_version=True
|
| 14 |
+
)
|
| 15 |
+
|
| 16 |
+
@tool
|
| 17 |
+
def gcalendar_agent(query: str) -> str:
|
| 18 |
+
"""
|
| 19 |
+
A tool that acts as a Google Calendar agent.
|
| 20 |
+
It can list, create, and analyze calendar events using the Google Calendar MCP server.
|
| 21 |
+
|
| 22 |
+
Args:
|
| 23 |
+
query (str): The natural language request for the calendar (e.g., "Schedule a meeting with X on Friday at 3pm").
|
| 24 |
+
|
| 25 |
+
Returns:
|
| 26 |
+
str: The natural language response from the agent confirming the action or providing the requested information.
|
| 27 |
+
|
| 28 |
+
Example output:
|
| 29 |
+
"I have successfully scheduled the meeting with X for Friday at 3pm. The event ID is 1234567890."
|
| 30 |
+
"""
|
| 31 |
+
try:
|
| 32 |
+
import asyncio
|
| 33 |
+
async def _run_async():
|
| 34 |
+
# Load settings
|
| 35 |
+
settings = GoogleCalendarSettings()
|
| 36 |
+
CALENDAR_MCP_DIR = settings.calendar_mcp_dir
|
| 37 |
+
CREDS = settings.creds
|
| 38 |
+
TOKEN = settings.token
|
| 39 |
+
|
| 40 |
+
# Initialize model
|
| 41 |
+
model = ChatOpenAI(model="gpt-4o", temperature=0)
|
| 42 |
+
|
| 43 |
+
# Connect to MCP server
|
| 44 |
+
# Note: This spawns a new process for each call.
|
| 45 |
+
# In a production environment, you might want to manage a persistent connection.
|
| 46 |
+
client = MultiServerMCPClient({
|
| 47 |
+
"calendar": {
|
| 48 |
+
"command": sys.executable,
|
| 49 |
+
"args": [
|
| 50 |
+
f"{CALENDAR_MCP_DIR}/run_server.py",
|
| 51 |
+
"--creds-file-path", str(CREDS),
|
| 52 |
+
"--token-path", str(TOKEN),
|
| 53 |
+
],
|
| 54 |
+
"transport": "stdio",
|
| 55 |
+
}
|
| 56 |
+
})
|
| 57 |
+
|
| 58 |
+
# Fetch tools
|
| 59 |
+
try:
|
| 60 |
+
tools = await client.get_tools()
|
| 61 |
+
except Exception as e:
|
| 62 |
+
return f"❌ Failed to connect to Calendar MCP server: {str(e)}"
|
| 63 |
+
|
| 64 |
+
if not tools:
|
| 65 |
+
return "❌ No tools available from Calendar MCP server."
|
| 66 |
+
|
| 67 |
+
# Create agent
|
| 68 |
+
agent = create_agent(model, tools)
|
| 69 |
+
|
| 70 |
+
# Run agent
|
| 71 |
+
# We wrap the user query in a system/user message structure
|
| 72 |
+
result = await agent.ainvoke({
|
| 73 |
+
"messages": [
|
| 74 |
+
{
|
| 75 |
+
"role": "system",
|
| 76 |
+
"content": SYSTEM_PROMPT,
|
| 77 |
+
},
|
| 78 |
+
{
|
| 79 |
+
"role": "user",
|
| 80 |
+
"content": query,
|
| 81 |
+
},
|
| 82 |
+
]
|
| 83 |
+
})
|
| 84 |
+
|
| 85 |
+
# Extract result
|
| 86 |
+
output = result["messages"][-1].content
|
| 87 |
+
return output
|
| 88 |
+
|
| 89 |
+
return asyncio.run(_run_async())
|
| 90 |
+
|
| 91 |
+
except Exception as e:
|
| 92 |
+
import traceback
|
| 93 |
+
return f"❌ Error in gcalendar_agent: {str(e)}\n{traceback.format_exc()}"
|
| 94 |
+
|
src/backend/agents/gcalendar/schemas/__init__.py
ADDED
|
File without changes
|
src/backend/agents/gcalendar/tools/__init__.py
ADDED
|
File without changes
|
src/backend/agents/gmail/__init__.py
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from .gmail_agent import gmail_agent
|
| 2 |
+
|