Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
# LLM Provider Configuration
# Options: "ollama" or "gemini"
# Options: "ollama", "gemini", or "claude_agent"
LLM_PROVIDER=ollama

# Default model to use
# For Ollama: "gemma3:4b", "qwen3:4b", "mistral:7b", etc.
# For Gemini: "gemini-2.5-pro", "gemini-2.5-flash", etc.
# For Claude Agent: "sonnet", "opus", "fable", etc.
DEFAULT_MODEL=gemma3:4b

# Google Gemini API Key (required if using Gemini provider)
Expand Down
24 changes: 16 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@

## Overview

Hiring Agent parses a resume PDF to Markdown, extracts sectioned JSON using a local or hosted LLM, augments the data with GitHub profile and repository signals, then produces an objective evaluation with category scores, evidence, bonus points, and deductions. You can run fully local with Ollama or use Google Gemini.
Hiring Agent parses a resume PDF to Markdown, extracts sectioned JSON using a local or hosted LLM, augments the data with GitHub profile and repository signals, then produces an objective evaluation with category scores, evidence, bonus points, and deductions. You can run fully local with Ollama, use Google Gemini, or your Claude Pro / Max subscription with Claude Code.

---

Expand Down Expand Up @@ -85,11 +85,12 @@ Hiring Agent parses a resume PDF to Markdown, extracts sectioned JSON using a lo

The repository pins `.python-version` to 3.11.13.

- **One LLM backend** (either of them)
- **One LLM backend** (any one of these)

- **Ollama** for local models
Install from the [official site](https://ollama.com/), then run `ollama serve`.
- **Google Gemini** if you have an API key, get it from [here](https://aistudio.google.com/api-keys).
- **Claude Agent SDK** if you have a Claude Pro or Max subscription. Install Claude Code and log in once; the provider then reuses that session, so no API key is needed.

### Quick setup with pip

Expand Down Expand Up @@ -136,12 +137,12 @@ $ cp .env.example .env

**Environment variables**

| Variable | Values | Description |
| ---------------- | ------------------------------------------- | ---------------------------------------------------------------------- |
| `LLM_PROVIDER` | `ollama` or `gemini` | Chooses provider. Defaults to Ollama. |
| `DEFAULT_MODEL` | for example `gemma3:4b` or `gemini-2.5-pro` | Model name passed to the provider. |
| `GEMINI_API_KEY` | string | Required when `LLM_PROVIDER=gemini`. |
| `GITHUB_TOKEN` | optional | Inherits from your shell environment, improves GitHub API rate limits. |
| Variable | Values | Description |
| ---------------- | ------------------------------------------------------ | ---------------------------------------------------------------------- |
| `LLM_PROVIDER` | `ollama`, `gemini`, or `claude_agent` | Chooses provider. Defaults to Ollama. |
| `DEFAULT_MODEL` | for example `gemma3:4b`, `gemini-2.5-pro`, or `sonnet` | Model name passed to the provider. |
| `GEMINI_API_KEY` | string | Required when `LLM_PROVIDER=gemini`. |
| `GITHUB_TOKEN` | optional | Inherits from your shell environment, improves GitHub API rate limits. |

Provider mapping lives in `prompt.py` and `models.py`. The `config.py` file has a single flag:

Expand Down Expand Up @@ -266,6 +267,13 @@ What happens:
- Provide `GEMINI_API_KEY`
- The wrapper in `models.GeminiProvider` adapts responses to a unified format

### Claude Agent SDK

- Set `LLM_PROVIDER=claude_agent`
- Install Claude Code and log in (no API key required)
- Set `DEFAULT_MODEL` to a supported Claude model, for example `sonnet`
- The wrapper in `models.ClaudeAgentProvider` runs a single-turn `query` and returns the text in a unified format

---

## Contributing
Expand Down
2 changes: 1 addition & 1 deletion evaluator.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ def evaluate_resume(self, resume_text: str) -> EvaluationData:

response_text = response["message"]["content"]
response_text = extract_json_from_response(response_text)
logger.error(f"🔤 Prompt response: {response_text}")
logger.debug(f"🔤 Prompt response: {response_text}")

evaluation_dict = json.loads(response_text)
evaluation_data = EvaluationData(**evaluation_dict)
Expand Down
7 changes: 5 additions & 2 deletions llm_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import logging
from typing import Any, Dict, Optional
from models import ModelProvider, OllamaProvider, GeminiProvider
from models import ModelProvider, OllamaProvider, GeminiProvider, ClaudeAgentProvider
from prompt import MODEL_PROVIDER_MAPPING, GEMINI_API_KEY

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -45,7 +45,7 @@ def initialize_llm_provider(model_name: str) -> Any:
model_name: The name of the model to use

Returns:
An initialized LLM provider (either OllamaProvider or GeminiProvider)
An initialized LLM provider (OllamaProvider, GeminiProvider, or ClaudeAgentProvider)
"""
# Default to Ollama provider
provider = OllamaProvider()
Expand All @@ -57,6 +57,9 @@ def initialize_llm_provider(model_name: str) -> Any:
else:
logger.info(f"🔄 Using Google Gemini API provider with model {model_name}")
provider = GeminiProvider(api_key=GEMINI_API_KEY)
elif model_provider == ModelProvider.CLAUDE_AGENT:
logger.info("Using Claude Agent SDK (assuming logged in to Claude Code)")
provider = ClaudeAgentProvider()
else:
logger.info(f"🔄 Using Ollama provider with model {model_name}")
return provider
63 changes: 63 additions & 0 deletions models.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,18 @@
from pydantic import BaseModel, Field, field_validator
from enum import Enum

import asyncio
from claude_agent_sdk import (
query, ClaudeAgentOptions, AssistantMessage, ResultMessage, TextBlock,
)


class ModelProvider(Enum):
"""Enum for supported model providers."""

OLLAMA = "ollama"
GEMINI = "gemini"
CLAUDE_AGENT = "claude_agent"


@runtime_checkable
Expand Down Expand Up @@ -389,3 +395,60 @@ def chat(
f"Retrying in {sleep_time}s..."
)
time.sleep(sleep_time)

class ClaudeAgentProvider:
"""Anthropic Claude Agent provider implementation. It assumes you have installed Claude Code and authenticated with your account in Claude Code."""

def chat(
self,
model: str,
messages: List[Dict[str, str]],
options: Dict[str, Any] = None,
**kwargs
) -> Dict[str, Any]:
"""Send a chat request to Claude Agent. Options parameter is not used."""
return asyncio.run(self._chat_claude_async(model, messages, **kwargs))

async def _chat_claude_async(
self,
model: str,
messages: List[Dict[str, str]],
**kwargs
) -> Dict[str, Any]:
system_prompt = None
transcript = []
for m in messages:
if m["role"] == "system":
system_prompt = m["content"]
elif m["role"] == "user":
transcript.append(f"User: {m['content']}")
elif m["role"] == "assistant":
transcript.append(f"Assistant: {m['content']}")

if len(transcript) == 1 and messages[-1]["role"] == "user":
prompt = messages[-1]["content"]
else:
prompt = (
"Below is the conversation so far. Reply as the Assistant "
"to the last message.\n\n" + "\n\n".join(transcript)
)

options = ClaudeAgentOptions(
model=model,
system_prompt=system_prompt,
tools=[],
max_turns=1
)

text_parts = []
async for msg in query(prompt=prompt, options=options):
if isinstance(msg, AssistantMessage):
for block in msg.content:
if isinstance(block, TextBlock):
text_parts.append(block.text)
return {
"message": {
"role": "assistant",
"content": "".join(text_parts)
}
}
4 changes: 4 additions & 0 deletions prompt.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@
"gemini-2.5-pro": ModelProvider.GEMINI,
"gemini-3.5-flash": ModelProvider.GEMINI,
"gemini-3.1-flash-lite": ModelProvider.GEMINI,
# Claude Agent models
"sonnet": ModelProvider.CLAUDE_AGENT,
"opus": ModelProvider.CLAUDE_AGENT,
"fable": ModelProvider.CLAUDE_AGENT
}

# Get API keys from environment
Expand Down
60 changes: 55 additions & 5 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,9 +1,59 @@
PyMuPDF==1.26.3
annotated-types==0.7.0
anyio==4.14.1
attrs==26.1.0
black==25.9.0
certifi==2026.6.17
cffi==2.1.0
charset-normalizer==3.4.9
claude-agent-sdk==0.2.114
click==8.4.2
colorama==0.4.6
cryptography==49.0.0
google-ai-generativelanguage==0.4.0
google-api-core==2.30.3
google-auth==2.55.2
google-generativeai==0.4.0
googleapis-common-protos==1.75.0
grpcio==1.82.1
grpcio-status==1.62.3
h11==0.16.0
httpcore==1.0.9
httpx==0.28.1
httpx-sse==0.4.3
idna==3.18
Jinja2==3.1.6
jsonschema==4.26.0
jsonschema-specifications==2025.9.1
MarkupSafe==3.0.3
mcp==1.28.1
mypy_extensions==1.1.0
ollama==0.5.1
packaging==26.2
pathspec==1.1.1
platformdirs==4.10.0
proto-plus==1.28.1
protobuf==4.25.9
pyasn1==0.6.3
pyasn1_modules==0.4.2
pycparser==3.0
pydantic==2.11.7
requests==2.32.4
pydantic-settings==2.14.2
pydantic_core==2.33.2
PyJWT==2.13.0
PyMuPDF==1.26.3
pymupdf4llm==0.0.27
Jinja2==3.1.6
google-generativeai==0.4.0
python-dotenv==1.0.1
black==25.9.0
python-multipart==0.0.32
pytokens==0.4.1
pywin32==312
referencing==0.37.0
requests==2.32.4
rpds-py==2026.6.3
sniffio==1.3.1
sse-starlette==3.4.5
starlette==1.3.1
tqdm==4.68.4
typing-inspection==0.4.2
typing_extensions==4.16.0
urllib3==2.7.0
uvicorn==0.51.0