owenkaplinsky dimim commited on
Commit
3370983
·
verified ·
1 Parent(s): 0f9214d

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
Files changed (50) hide show
  1. .gitignore +4 -1
  2. README.md +153 -68
  3. docker/Dockerfile.candidates_db_init +5 -3
  4. docker/Dockerfile.supervisor_api +1 -1
  5. docker/docker-compose.yml +46 -52
  6. docker/info.md +6 -2
  7. docs/intro.md +457 -0
  8. docs/video/script.md +69 -0
  9. intro.md +1 -0
  10. requirements/agent.txt +0 -1
  11. scripts/db/list_candidates.py +13 -3
  12. scripts/db/setup_demo_state.py +65 -0
  13. scripts/db/test_connection.py +1 -1
  14. scripts/db/test_cv_upload.py +45 -0
  15. scripts/db/test_session.py +1 -1
  16. scripts/db/wipe.py +1 -1
  17. scripts/infra/reset_db.sh +12 -0
  18. src/backend/__init__.py +0 -0
  19. src/backend/agents/__init__.py +14 -0
  20. src/backend/agents/cv_screening/__init__.py +4 -0
  21. src/backend/agents/cv_screening/cv_screener.py +88 -0
  22. src/backend/agents/cv_screening/cv_screening_workflow.py +108 -0
  23. src/backend/agents/cv_screening/schemas/__init__.py +0 -0
  24. src/backend/agents/cv_screening/schemas/output_schema.py +12 -0
  25. src/backend/agents/cv_screening/tools/__init__.py +0 -0
  26. src/backend/agents/cv_screening/utils/__init__.py +5 -0
  27. src/backend/agents/cv_screening/utils/read_file.py +7 -0
  28. src/backend/agents/db_executor/__init__.py +5 -0
  29. src/backend/agents/db_executor/codeact/__init__.py +6 -0
  30. src/backend/agents/db_executor/codeact/core/codeact.py +545 -0
  31. src/backend/agents/db_executor/codeact/prompts/local_archive/original.txt +18 -0
  32. src/backend/agents/db_executor/codeact/prompts/local_archive/test.txt +25 -0
  33. src/backend/agents/db_executor/codeact/prompts/prompt_layer.py +162 -0
  34. src/backend/agents/db_executor/codeact/schemas/__init__.py +10 -0
  35. src/backend/agents/db_executor/codeact/schemas/openai_key.py +56 -0
  36. src/backend/agents/db_executor/codeact/schemas/stream.py +8 -0
  37. src/backend/agents/db_executor/codeact/states/state.py +10 -0
  38. src/backend/agents/db_executor/codeact/tools/__init__.py +0 -0
  39. src/backend/agents/db_executor/codeact/tools/tools.py +53 -0
  40. src/backend/agents/db_executor/codeact/utils/__init__.py +5 -0
  41. src/backend/agents/db_executor/codeact/utils/pretty_state.py +73 -0
  42. src/backend/agents/db_executor/db_executor.py +99 -0
  43. src/backend/agents/db_executor/info.md +22 -0
  44. src/backend/agents/example/info.md +66 -0
  45. src/backend/agents/example/react_agent.py +59 -0
  46. src/backend/agents/gcalendar/__init__.py +2 -0
  47. src/backend/agents/gcalendar/gcalendar_agent.py +94 -0
  48. src/backend/agents/gcalendar/schemas/__init__.py +0 -0
  49. src/backend/agents/gcalendar/tools/__init__.py +0 -0
  50. 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 is buckling under high volumes and inefficiency, creating a critical bottleneck for organizational growth.
 
 
 
 
 
 
65
 
66
- * **Overwhelmed Teams**: **35%** of recruiter time is lost to admin tasks like scheduling [`2`], with **27%** of leaders citing workload overload [`2`].
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
- This agentic system automates high-volume screening tasks, allowing HR professionals to focus on strategic decision-making.
 
 
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 is an **experimental prototype** designed to demonstrate technical orchestration of LLM agents, **not a production-ready HR system**.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
 
90
- * **Human-in-the-Loop (HITL)**: The system is purely assistive. All final decisions (approvals/rejections) must be made by human recruiters.
91
- * **EU AI Act Compliance**: Recruitment AI is classified as **High-Risk**. This prototype addresses key requirements via:
92
- * **Transparency**: Clear logs of agent reasoning.
93
- * **Oversight**: No autonomous final judgments.
94
- * **Prohibited Practices**: No emotion recognition, biometric inference, or psychographic profiling.
95
- * **Scope**: Limited to workflow automation and initial screening support. It does not replace human judgment.
 
 
 
 
 
 
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
- ![System Architecture](./architecture.png)
 
120
 
121
- ## ***`Application Flow & Entry Points`***
 
 
 
 
 
 
 
122
 
123
- The platform orchestrates a complete recruitment pipeline, interacting with both Candidates and the HR Supervisor.
 
 
 
 
 
124
 
 
 
 
 
 
 
125
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
126
 
127
  ### 1. The Recruitment Lifecycle
 
128
 
129
- The candidate application flow follows these key stages:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
130
 
131
- 1. **Application Submission**: Candidate applies; status set to `applied`.
132
- 2. **CV Screening**: AI analyzes CV (`cv_screened`) and evaluates it (`cv_passed` or `cv_rejected`).
133
- 3. **Voice Invitation**: Qualified candidates receive an email with an auth code for the AI voice interview (`voice_invitation_sent`).
134
- 4. **Voice Screening**: Candidate completes the AI interview (`voice_done`); AI judge evaluates performance (`voice_passed` or `voice_rejected`).
135
- 5. **Human Interview Scheduling**: Successful candidates are offered available time slots for a person-to-person interview based on HR calendar availability.
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
- ### 2. User Entry Points
 
 
 
 
140
 
141
- | User | Interface | Description |
142
- | :--- | :--- | :--- |
143
- | **HR Manager** | **Supervisor UI** | **The Command Center.** Chat with the Supervisor Agent to manage the pipeline, review candidates, query the DB, and schedule interviews. |
144
- | **Candidate** | **CV Portal** | Public-facing portal for candidates to register and upload their resumes to the system. |
145
- | **Candidate** | **Voice Portal** | AI-conducted voice interview interface. Candidates access this only after passing CV screening and receiving an invite. |
 
 
 
146
 
147
- The interaction between these entry points and the agentic workflow is visualized in the state machine below:
148
 
149
- ![](/architecture.png)
 
 
 
 
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 *only* the candidate database module
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: .. # build from the project root
38
  dockerfile: docker/Dockerfile.candidates_db_init
39
  depends_on:
40
  db:
41
  condition: service_healthy
 
 
 
 
42
  environment:
43
- POSTGRES_HOST: ${POSTGRES_HOST}
44
- POSTGRES_PORT: ${POSTGRES_PORT}
 
45
  POSTGRES_USER: ${POSTGRES_USER}
46
  POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
47
  POSTGRES_DB: ${POSTGRES_DB}
48
- # command: ["python", "-m", "src.database.candidates.init_db"]
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 # optional: live reload for local dev
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: ${POSTGRES_HOST}
75
- POSTGRES_PORT: ${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}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}
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" # Map host port 8502 to container port 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" # Map host port 8080 to container port 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: ${POSTGRES_HOST}
175
- POSTGRES_PORT: ${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" # Map host port 8503 to container port 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. Rebuild and start fresh
 
 
 
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" - {c.full_name} | {c.email} | Status: {c.status}")
 
 
 
 
 
 
 
 
 
 
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
+