Skip to content
Open
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
21 changes: 21 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
POSTGRES_USER=appuser
POSTGRES_PASSWORD=secret
POSTGRES_DB=appdb
POSTGRES_HOST=postgres
POSTGRES_PORT=5432

DATABASE_URL=postgresql+asyncpg://appuser:secret@postgres:5432/appdb
SYNC_DATABASE_URL=postgresql://appuser:secret@postgres:5432/appdb

REDIS_URL=redis://redis:6379/0

GAPGPT_API_KEY=
OPENAI_API_KEY=

TELEGRAM_BOT_TOKEN=

RESUME_PATH=/data/resume.txt

BACKEND_PORT=8000
FRONTEND_PORT=3000
TELBOT_PORT=8080
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
.env
extracted_for_ai.text
__pycache__/

Binary file removed commands/__pycache__/__init__.cpython-310.pyc
Binary file not shown.
Binary file removed commands/__pycache__/query_handler.cpython-310.pyc
Binary file not shown.
62 changes: 62 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
version: '3.8'
services:
postgres:
image: postgres:15-alpine
env_file: .env
volumes:
- db_data:/var/lib/postgresql/data

pgbouncer:
image: edoburu/pgbouncer:latest
volumes:
- ./services/pgbouncer/pgbouncer.ini:/etc/pgbouncer/pgbouncer.ini
- ./services/pgbouncer/userlist.txt:/etc/pgbouncer/userlist.txt
env_file: .env
depends_on:
- postgres

redis:
image: redis:7-alpine
command: ["redis-server","--maxmemory","256mb","--maxmemory-policy","allkeys-lru"]

backend:
build: ./services/backend
env_file: .env
ports:
- "${BACKEND_PORT}:8000"
depends_on:
- pgbouncer
- redis

rq-worker:
build: ./services/backend
env_file: .env
entrypoint: ["rq","worker","--url","${REDIS_URL}","default"]
depends_on:
- backend
- redis

frontend:
build: ./services/frontend
env_file: .env
ports:
- "${FRONTEND_PORT}:3000"
depends_on:
- backend

telbot:
build: ./services/telbot
env_file: .env
depends_on:
- postgres
command: ["python","bot.py"]

resume-pipeline:
build: ./services/resume_pipeline
env_file: .env
volumes:
- ./data:/data
command: ["python","run_pipeline.py"]

volumes:
db_data:
3 changes: 3 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,6 @@ langchain_openai
sqlalchemy
tenacity
matplotlib
psycopg2-binary
requests
beautifulsoup4
File renamed without changes.
10 changes: 10 additions & 0 deletions services/backend/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
FROM python:3.11-slim AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

FROM python:3.11-slim
WORKDIR /app
COPY --from=builder /usr/local/lib/python3.11/site-packages /usr/local/lib/python3.11/site-packages
COPY . .
CMD ["uvicorn","main:app","--host","0.0.0.0","--port","8000"]
Empty file added services/backend/__init__.py
Empty file.
11 changes: 11 additions & 0 deletions services/backend/db.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import os
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker

DATABASE_URL = os.getenv("DATABASE_URL")
engine = create_async_engine(DATABASE_URL, echo=False)
AsyncSessionLocal = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)

async def get_db():
async with AsyncSessionLocal() as session:
yield session
11 changes: 11 additions & 0 deletions services/backend/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from fastapi import FastAPI, Depends
from sqlalchemy import select
from .db import get_db
from .models import Item

app = FastAPI()

@app.get("/items")
async def list_items(db=Depends(get_db)):
result = await db.execute(select(Item))
return result.scalars().all()
11 changes: 11 additions & 0 deletions services/backend/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from sqlalchemy.orm import declarative_base
from sqlalchemy import Column, Integer, String, Text

Base = declarative_base()

class Item(Base):
__tablename__ = "items"
id = Column(Integer, primary_key=True)
name = Column(String(100))
description = Column(Text)
status = Column(String(20))
7 changes: 7 additions & 0 deletions services/backend/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
fastapi
uvicorn[standard]
asyncpg
sqlalchemy
psycopg2-binary
redis
rq
11 changes: 11 additions & 0 deletions services/backend/rq_worker.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import os
from rq import Worker, Queue, Connection
import redis

redis_url = os.getenv("REDIS_URL")
conn = redis.from_url(redis_url)

if __name__ == "__main__":
with Connection(conn):
worker = Worker(["default"])
worker.work()
9 changes: 9 additions & 0 deletions services/frontend/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
FROM node:18-alpine AS builder
WORKDIR /app
COPY package.json yarn.lock ./
RUN yarn install
COPY . .
RUN yarn build

FROM nginx:alpine
COPY --from=builder /app/build /usr/share/nginx/html
11 changes: 11 additions & 0 deletions services/frontend/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"name": "frontend",
"version": "1.0.0",
"dependencies": {
"react": "^18.0.0",
"react-dom": "^18.0.0"
},
"scripts": {
"build": "echo building frontend"
}
}
3 changes: 3 additions & 0 deletions services/frontend/src/components/ItemsTable.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function ItemsTable() {
return null;
}
1 change: 1 addition & 0 deletions services/frontend/src/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
console.log('frontend placeholder');
1 change: 1 addition & 0 deletions services/frontend/yarn.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# lockfile
6 changes: 6 additions & 0 deletions services/pgbouncer/pgbouncer.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[databases]
appdb = host=postgres port=5432 dbname=appdb user=appuser password=secret

[pgbouncer]
listen_port = 6432
listen_addr = 0.0.0.0
1 change: 1 addition & 0 deletions services/pgbouncer/userlist.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"appuser" "secret"
13 changes: 13 additions & 0 deletions services/postgres/init.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
-- optional schema
CREATE TABLE IF NOT EXISTS jobs (
id SERIAL PRIMARY KEY,
url TEXT,
page_html TEXT,
requirements TEXT
);
CREATE TABLE IF NOT EXISTS items (
id SERIAL PRIMARY KEY,
name TEXT,
description TEXT,
status TEXT
);
6 changes: 6 additions & 0 deletions services/resume_pipeline/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["python","run_pipeline.py"]
Empty file.
35 changes: 35 additions & 0 deletions services/resume_pipeline/database.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import os
from contextlib import contextmanager
import psycopg2
from psycopg2.pool import SimpleConnectionPool

DATABASE_URL = os.getenv("DATABASE_URL")

if not DATABASE_URL:
raise RuntimeError("DATABASE_URL environment variable is required")

pool = SimpleConnectionPool(minconn=1, maxconn=5, dsn=DATABASE_URL)

@contextmanager
def get_conn():
conn = pool.getconn()
try:
yield conn
finally:
pool.putconn(conn)

def fetch_job_urls(limit=10):
"""Return a list of (id, url) tuples from the jobs table."""
query = "SELECT id, url FROM jobs WHERE url IS NOT NULL LIMIT %s"
with get_conn() as conn:
with conn.cursor() as cur:
cur.execute(query, (limit,))
return cur.fetchall()

def store_job_page(job_id, html, requirements_text):
"""Store downloaded HTML and extracted requirements for a job."""
query = "UPDATE jobs SET page_html=%s, requirements=%s WHERE id=%s"
with get_conn() as conn:
with conn.cursor() as cur:
cur.execute(query, (html, requirements_text, job_id))
conn.commit()
4 changes: 4 additions & 0 deletions services/resume_pipeline/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
psycopg2-binary
requests
beautifulsoup4
openai
17 changes: 17 additions & 0 deletions services/resume_pipeline/resume_generator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import os
import openai

openai.api_key = os.getenv("OPENAI_API_KEY")

PROMPT = (
"You are a helpful assistant that rewrites a resume to match a job description."
"Return the improved resume in plain text."
)

def generate_resume(resume_text: str, job_requirements: str) -> str:
messages = [
{"role": "system", "content": PROMPT},
{"role": "user", "content": f"Resume:\n{resume_text}\n\nJob requirements:\n{job_requirements}"},
]
resp = openai.ChatCompletion.create(model="gpt-3.5-turbo", messages=messages)
return resp.choices[0].message.content.strip()
8 changes: 8 additions & 0 deletions services/resume_pipeline/resume_parser.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from pathlib import Path

def load_resume(path: str) -> str:
"""Load resume text from a file."""
p = Path(path)
if not p.exists():
raise FileNotFoundError(path)
return p.read_text()
25 changes: 25 additions & 0 deletions services/resume_pipeline/run_pipeline.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import os
from database import fetch_job_urls, store_job_page
from scraper import download_job_page, parse_requirements
from resume_parser import load_resume
from resume_generator import generate_resume

RESUME_PATH = os.getenv("RESUME_PATH", "resume.txt")


def main():
resume_text = load_resume(RESUME_PATH)
jobs = fetch_job_urls(limit=1)
for job_id, url in jobs:
html = download_job_page(url)
reqs = parse_requirements(html)
new_resume = generate_resume(resume_text, reqs)
store_job_page(job_id, html, reqs)
out_file = f"resume_{job_id}.txt"
with open(out_file, "w") as f:
f.write(new_resume)
print(f"Generated resume for job {job_id} -> {out_file}")


if __name__ == "__main__":
main()
26 changes: 26 additions & 0 deletions services/resume_pipeline/scraper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import requests
from bs4 import BeautifulSoup
from typing import Tuple

HEADERS = {"User-Agent": "Mozilla/5.0"}

def download_job_page(url: str) -> str:
resp = requests.get(url, headers=HEADERS, timeout=10)
resp.raise_for_status()
return resp.text

def parse_requirements(html: str) -> str:
soup = BeautifulSoup(html, "html.parser")
# Simple heuristic: extract bullet lists under sections containing 'requirement'
sections = soup.find_all(text=lambda t: t and 'require' in t.lower())
if not sections:
return ""
# Gather sibling lists or paragraphs
requirements = []
for s in sections:
parent = s.parent
ul = parent.find_next("ul")
if ul:
items = [li.get_text(" ", strip=True) for li in ul.find_all("li")]
requirements.extend(items)
return "\n".join(requirements)
6 changes: 6 additions & 0 deletions services/telbot/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["python","bot.py"]
Empty file added services/telbot/__init__.py
Empty file.
File renamed without changes.
Empty file.
File renamed without changes.
16 changes: 16 additions & 0 deletions services/telbot/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
python-telegram-bot==13.7
pandas
numpy
plotly
python-dotenv
openai
langchain
langchain_community
langchain_experimental
langchain_openai
sqlalchemy
tenacity
matplotlib
psycopg2-binary
requests
beautifulsoup4
Loading