Rishabh2095 commited on
Commit
c7be661
·
1 Parent(s): 8349858

Modified Dockerfile and docker-compose for HF Deployement

Browse files
Dockerfile CHANGED
@@ -1,32 +1,83 @@
1
  # syntax=docker/dockerfile:1.4
2
  FROM langchain/langgraph-api:3.12
3
 
4
- # HuggingFace Spaces requires port 7860
5
- ENV PORT=7860
6
- ENV LANGGRAPH_PORT=7860
 
 
 
 
 
7
 
8
  ENV LANGSERVE_GRAPHS='{"job_app_graph": "/deps/job_writer/src/job_writing_agent/workflow.py:job_app_graph", "research_workflow": "/deps/job_writer/src/job_writing_agent/nodes/research_workflow.py:research_workflow", "data_loading_workflow": "/deps/job_writer/src/job_writing_agent/nodes/data_loading_workflow.py:data_loading_workflow"}'
9
 
10
- COPY pyproject.toml langgraph.json /deps/job_writer/
11
- COPY src/ /deps/job_writer/src/
 
 
 
 
 
 
12
 
13
- RUN for dep in /deps/*; do \
 
 
 
14
  if [ -d "$dep" ]; then \
15
  echo "Installing $dep"; \
16
- (cd "$dep" && PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir -c /api/constraints.txt -e .); \
17
  fi; \
18
  done
19
 
20
- # Use cache mount for Playwright - browsers persist between builds!
 
 
 
21
  RUN --mount=type=cache,target=/root/.cache/ms-playwright \
22
- playwright install chromium && \
23
- playwright install-deps
24
 
 
25
  RUN mkdir -p /api/langgraph_api /api/langgraph_runtime /api/langgraph_license && \
26
  touch /api/langgraph_api/__init__.py /api/langgraph_runtime/__init__.py /api/langgraph_license/__init__.py && \
27
- PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir --no-deps -e /api
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
 
29
  WORKDIR /deps/job_writer
30
 
31
  # Expose port for HuggingFace Spaces
32
- EXPOSE 7860
 
 
 
 
 
1
  # syntax=docker/dockerfile:1.4
2
  FROM langchain/langgraph-api:3.12
3
 
4
+ # Set Python environment variables (best practice)
5
+ ENV PYTHONUNBUFFERED=1 \
6
+ PYTHONDONTWRITEBYTECODE=1 \
7
+ PORT=7860 \
8
+ LANGGRAPH_PORT=7860
9
+
10
+ # Create user with UID 1000 for HuggingFace Spaces compatibility
11
+ RUN useradd -m -u 1000 hf_user
12
 
13
  ENV LANGSERVE_GRAPHS='{"job_app_graph": "/deps/job_writer/src/job_writing_agent/workflow.py:job_app_graph", "research_workflow": "/deps/job_writer/src/job_writing_agent/nodes/research_workflow.py:research_workflow", "data_loading_workflow": "/deps/job_writer/src/job_writing_agent/nodes/data_loading_workflow.py:data_loading_workflow"}'
14
 
15
+ # Copy package metadata and structure files (needed for editable install)
16
+ COPY --chown=hf_user:hf_user pyproject.toml langgraph.json README.md /deps/job_writer/
17
+
18
+ # Create src directory structure (needed for setuptools to find packages)
19
+ RUN mkdir -p /deps/job_writer/src
20
+
21
+ # Copy source code (required for editable install)
22
+ COPY --chown=hf_user:hf_user src/ /deps/job_writer/src/
23
 
24
+ # Install Python dependencies as ROOT using --system flag
25
+ # Using cache mount for faster rebuilds
26
+ RUN --mount=type=cache,target=/root/.cache/uv \
27
+ for dep in /deps/*; do \
28
  if [ -d "$dep" ]; then \
29
  echo "Installing $dep"; \
30
+ (cd "$dep" && uv pip install --system --no-cache-dir -c /api/constraints.txt -e .); \
31
  fi; \
32
  done
33
 
34
+ # Install Playwright system dependencies (after playwright package is installed)
35
+ RUN playwright install-deps chromium
36
+
37
+ # Install Playwright browser binaries (with cache mount)
38
  RUN --mount=type=cache,target=/root/.cache/ms-playwright \
39
+ playwright install chromium
 
40
 
41
+ # Create API directories and install langgraph-api as ROOT
42
  RUN mkdir -p /api/langgraph_api /api/langgraph_runtime /api/langgraph_license && \
43
  touch /api/langgraph_api/__init__.py /api/langgraph_runtime/__init__.py /api/langgraph_license/__init__.py && \
44
+ uv pip install --system --no-cache-dir --no-deps -e /api
45
+
46
+ # Fix permissions for packages that write to their own directories
47
+ # Make ONLY the specific directories writable (not entire site-packages)
48
+ RUN mkdir -p /usr/local/lib/python3.12/site-packages/litellm/litellm_core_utils/tokenizers && \
49
+ chown -R hf_user:hf_user /usr/local/lib/python3.12/site-packages/litellm/litellm_core_utils/tokenizers && \
50
+ chmod -R u+w /usr/local/lib/python3.12/site-packages/litellm/litellm_core_utils/tokenizers
51
+
52
+ # Create user cache directories with proper permissions (BEFORE switching user)
53
+ # Following XDG Base Directory Specification: https://specifications.freedesktop.org/basedir-spec/
54
+ RUN mkdir -p /home/hf_user/.cache/tiktoken \
55
+ /home/hf_user/.cache/litellm \
56
+ /home/hf_user/.cache/huggingface \
57
+ /home/hf_user/.cache/torch \
58
+ /home/hf_user/.local/share && \
59
+ chown -R hf_user:hf_user /home/hf_user/.cache /home/hf_user/.local
60
+
61
+ # Switch to hf_user for runtime (after all root operations)
62
+ USER hf_user
63
+
64
+ # Set environment variables following XDG Base Directory Specification
65
+ # This ensures all packages respect standard cache locations
66
+ ENV HOME=/home/hf_user \
67
+ PATH="/home/hf_user/.local/bin:$PATH" \
68
+ XDG_CACHE_HOME=/home/hf_user/.cache \
69
+ XDG_DATA_HOME=/home/hf_user/.local/share \
70
+ XDG_CONFIG_HOME=/home/hf_user/.config \
71
+ # Package-specific cache directories (for packages that don't fully respect XDG)
72
+ TIKTOKEN_CACHE_DIR=/home/hf_user/.cache/tiktoken \
73
+ HF_HOME=/home/hf_user/.cache/huggingface \
74
+ TORCH_HOME=/home/hf_user/.cache/torch
75
 
76
  WORKDIR /deps/job_writer
77
 
78
  # Expose port for HuggingFace Spaces
79
+ EXPOSE 7860
80
+
81
+ # Healthcheck (LangGraph API typically has /ok endpoint)
82
+ HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
83
+ CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:7860/ok')" || exit 1
README.md CHANGED
@@ -6,6 +6,7 @@ colorTo: purple
6
  sdk: docker
7
  app_port: 7860
8
  pinned: false
 
9
  ---
10
 
11
  # Job Writer Module
 
6
  sdk: docker
7
  app_port: 7860
8
  pinned: false
9
+ python_version: 3.12.8
10
  ---
11
 
12
  # Job Writer Module
docker-compose.yml CHANGED
@@ -9,6 +9,8 @@ services:
9
  interval: 5s
10
  timeout: 3s
11
  retries: 5
 
 
12
  networks:
13
  - job-app-network
14
 
@@ -26,17 +28,18 @@ services:
26
  interval: 5s
27
  timeout: 5s
28
  retries: 5
 
29
  volumes:
30
  - pg_data_local:/var/lib/postgresql/data
 
31
  networks:
32
  - job-app-network
33
 
34
- # Optional: Uncomment to run your agent container alongside Redis/Postgres
35
  agent:
36
  build:
37
  context: .
38
  dockerfile: Dockerfile
39
- image: job-app-workflow:latest
40
  container_name: job-app-agent
41
  ports:
42
  - "7860:7860"
@@ -47,6 +50,13 @@ services:
47
  - DATABASE_URI=postgresql://postgres:postgres@postgres:5432/postgres
48
  env_file:
49
  - .docker_env
 
 
 
 
 
 
 
50
  depends_on:
51
  redis:
52
  condition: service_healthy
@@ -54,10 +64,19 @@ services:
54
  condition: service_healthy
55
  networks:
56
  - job-app-network
 
 
 
 
 
 
 
 
 
57
 
58
  networks:
59
  job-app-network:
60
  driver: bridge
61
 
62
  volumes:
63
- pg_data_local:
 
9
  interval: 5s
10
  timeout: 3s
11
  retries: 5
12
+ start_period: 10s
13
+ restart: unless-stopped
14
  networks:
15
  - job-app-network
16
 
 
28
  interval: 5s
29
  timeout: 5s
30
  retries: 5
31
+ start_period: 10s
32
  volumes:
33
  - pg_data_local:/var/lib/postgresql/data
34
+ restart: unless-stopped
35
  networks:
36
  - job-app-network
37
 
 
38
  agent:
39
  build:
40
  context: .
41
  dockerfile: Dockerfile
42
+ image: job-app-workflow:latest # Consider versioned tag in production
43
  container_name: job-app-agent
44
  ports:
45
  - "7860:7860"
 
50
  - DATABASE_URI=postgresql://postgres:postgres@postgres:5432/postgres
51
  env_file:
52
  - .docker_env
53
+ healthcheck:
54
+ test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:7860/ok')"]
55
+ interval: 30s
56
+ timeout: 10s
57
+ retries: 3
58
+ start_period: 40s
59
+ restart: unless-stopped
60
  depends_on:
61
  redis:
62
  condition: service_healthy
 
64
  condition: service_healthy
65
  networks:
66
  - job-app-network
67
+ # Optional: Resource limits (uncomment for production)
68
+ # deploy:
69
+ # resources:
70
+ # limits:
71
+ # cpus: '2'
72
+ # memory: 4G
73
+ # reservations:
74
+ # cpus: '1'
75
+ # memory: 2G
76
 
77
  networks:
78
  job-app-network:
79
  driver: bridge
80
 
81
  volumes:
82
+ pg_data_local:
src/job_writing_agent/agents/nodes.py CHANGED
@@ -94,11 +94,13 @@ def create_draft(state: ResearchState) -> ResultState:
94
  logger.info(f"Draft has been created: {response.content}")
95
 
96
  app_state = ResultState(
97
- draft=response.content,
98
  feedback="",
99
  critique_feedback="",
100
  current_node="create_draft",
101
- output_data={},
 
 
102
  )
103
 
104
  return app_state
@@ -116,9 +118,6 @@ def critique_draft(state: ResultState) -> ResultState:
116
  company_research_data = state.get("company_research_data", {})
117
  job_description = str(company_research_data.get("job_description", ""))
118
  draft_content = str(state.get("draft", ""))
119
- feedback = state.get("feedback", "")
120
- output_data = state.get("output_data", "")
121
- current_node = state.get("current_node", "")
122
 
123
  # Debug logging to verify values
124
  logger.debug(f"Job description length: {len(job_description)}")
@@ -126,8 +125,16 @@ def critique_draft(state: ResultState) -> ResultState:
126
 
127
  # Early return if required fields are missing
128
  if not job_description or not draft_content:
129
- logger.warning("Missing job_description or draft in state")
130
- return ResultState(**state, current_node=current_node)
 
 
 
 
 
 
 
 
131
 
132
  # Create LLM inside function (lazy initialization)
133
  llm_provider = LLMFactory()
@@ -198,7 +205,13 @@ def critique_draft(state: ResultState) -> ResultState:
198
 
199
  # Store the critique - using validated variables from top of function
200
  return ResultState(
201
- **state, critique_feedback=critique_content, current_node=current_node
 
 
 
 
 
 
202
  )
203
 
204
  except Exception as e:
@@ -232,7 +245,15 @@ def human_approval(state: ResultState) -> ResultState:
232
 
233
  print(f"Human feedback: {human_feedback}")
234
 
235
- return ResultState(**state, feedback=human_feedback, current_node="human_approval")
 
 
 
 
 
 
 
 
236
 
237
 
238
  def finalize_document(state: ResultState) -> ResultState:
@@ -278,16 +299,20 @@ def finalize_document(state: ResultState) -> ResultState:
278
  )
279
 
280
  # Return final state using validated variables
 
 
281
  return ResultState(
282
  draft=draft_content,
283
  feedback=feedback_content,
284
  critique_feedback=critique_feedback_content,
285
  current_node="finalize",
286
  output_data=(
287
- final_content.content
288
  if hasattr(final_content, "content")
289
- else final_content
290
  ),
 
 
291
  )
292
 
293
 
 
94
  logger.info(f"Draft has been created: {response.content}")
95
 
96
  app_state = ResultState(
97
+ draft=str(response.content),
98
  feedback="",
99
  critique_feedback="",
100
  current_node="create_draft",
101
+ output_data="",
102
+ company_research_data=state.get("company_research_data", {}),
103
+ messages=state.get("messages", []),
104
  )
105
 
106
  return app_state
 
118
  company_research_data = state.get("company_research_data", {})
119
  job_description = str(company_research_data.get("job_description", ""))
120
  draft_content = str(state.get("draft", ""))
 
 
 
121
 
122
  # Debug logging to verify values
123
  logger.debug(f"Job description length: {len(job_description)}")
 
125
 
126
  # Early return if required fields are missing
127
  if not job_description or not draft_content:
128
+ logger.warning("Missing content for critique in state")
129
+ return ResultState(
130
+ draft=state.get("draft", ""),
131
+ feedback=state.get("feedback", ""),
132
+ critique_feedback="",
133
+ current_node="critique",
134
+ output_data="",
135
+ company_research_data=state.get("company_research_data", {}),
136
+ messages=state.get("messages", []),
137
+ )
138
 
139
  # Create LLM inside function (lazy initialization)
140
  llm_provider = LLMFactory()
 
205
 
206
  # Store the critique - using validated variables from top of function
207
  return ResultState(
208
+ draft=state.get("draft", ""),
209
+ feedback=state.get("feedback", ""),
210
+ critique_feedback=str(critique_content),
211
+ current_node="critique",
212
+ output_data="",
213
+ company_research_data=state.get("company_research_data", {}),
214
+ messages=state.get("messages", []),
215
  )
216
 
217
  except Exception as e:
 
245
 
246
  print(f"Human feedback: {human_feedback}")
247
 
248
+ return ResultState(
249
+ draft=state.get("draft", ""),
250
+ feedback=human_feedback,
251
+ critique_feedback=state.get("critique_feedback", ""),
252
+ current_node="human_approval",
253
+ output_data="",
254
+ company_research_data=state.get("company_research_data", {}),
255
+ messages=state.get("messages", []),
256
+ )
257
 
258
 
259
  def finalize_document(state: ResultState) -> ResultState:
 
299
  )
300
 
301
  # Return final state using validated variables
302
+ # Current (INCOMPLETE):
303
+
304
  return ResultState(
305
  draft=draft_content,
306
  feedback=feedback_content,
307
  critique_feedback=critique_feedback_content,
308
  current_node="finalize",
309
  output_data=(
310
+ str(final_content.content)
311
  if hasattr(final_content, "content")
312
+ else str(final_content)
313
  ),
314
+ company_research_data=state.get("company_research_data", {}),
315
+ messages=state.get("messages", []),
316
  )
317
 
318
 
src/job_writing_agent/classes/__init__.py CHANGED
@@ -1,17 +1,3 @@
1
- from .classes import (
2
- AppState,
3
- ResearchState,
4
- DataLoadState,
5
- ResultState,
6
- FormField,
7
- FormFieldsExtraction,
8
- )
9
 
10
- __all__ = [
11
- "AppState",
12
- "ResearchState",
13
- "DataLoadState",
14
- "ResultState",
15
- "FormField",
16
- "FormFieldsExtraction",
17
- ]
 
1
+ from .classes import AppState, ResearchState, DataLoadState, ResultState
 
 
 
 
 
 
 
2
 
3
+ __all__ = ["AppState", "ResearchState", "DataLoadState", "ResultState"]
 
 
 
 
 
 
 
src/job_writing_agent/utils/document_processing.py CHANGED
@@ -416,17 +416,17 @@ async def parse_job_description_from_url(url: str) -> Document:
416
  if not cerebras_api_key:
417
  raise ValueError("CEREBRAS_API_KEY environment variable not set")
418
 
419
- dspy.configure(
 
420
  lm=dspy.LM(
421
  "cerebras/qwen-3-32b",
422
  api_key=cerebras_api_key,
423
  temperature=0.1,
424
  max_tokens=60000, # Note: This max_tokens is unusually high
425
  )
426
- )
427
-
428
- job_extract_fn = dspy.Predict(ExtractJobDescription)
429
- result = job_extract_fn(job_description_html_content=raw_content)
430
  logger.info("Successfully processed job description with LLM.")
431
 
432
  # 4. Create the final Document with structured data
 
416
  if not cerebras_api_key:
417
  raise ValueError("CEREBRAS_API_KEY environment variable not set")
418
 
419
+ # Use dspy.context() for async tasks instead of dspy.configure()
420
+ with dspy.context(
421
  lm=dspy.LM(
422
  "cerebras/qwen-3-32b",
423
  api_key=cerebras_api_key,
424
  temperature=0.1,
425
  max_tokens=60000, # Note: This max_tokens is unusually high
426
  )
427
+ ):
428
+ job_extract_fn = dspy.Predict(ExtractJobDescription)
429
+ result = job_extract_fn(job_description_html_content=raw_content)
 
430
  logger.info("Successfully processed job description with LLM.")
431
 
432
  # 4. Create the final Document with structured data