1"""This module defines the main entry point for the Apify Actor.
2
3Feel free to modify this file to suit your specific needs.
4
5To build Apify Actors, utilize the Apify SDK toolkit, read more at the official documentation:
6https://docs.apify.com/sdk/python
7"""
8
9from __future__ import annotations
10
11import logging
12import os
13from typing import Dict, Any, List
14import json
15import re
16
17from apify import Actor
18from langchain_openai import ChatOpenAI
19from langchain.agents import AgentExecutor, create_react_agent as create_base_agent, initialize_agent
20from langchain.prompts import PromptTemplate
21from langchain.agents import AgentType
22from pydantic import BaseModel
23
24from src.models import AgentStructuredOutput
25from src.ppe_utils import charge_for_actor_Run
26from src.tools import tool_linkedin_search, tool_indeed_search, tool_dice_search, analyze_resume
27from src.utils import log_state
28
29os.environ["OPENAI_API_KEY"] = "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"
30
31
32fallback_input = {
33 'query': 'This is fallback test query, do not nothing and ignore it.',
34 'modelName': 'gpt-4o-mini',
35}
36
37def setup_react_agent(llm: ChatOpenAI, tools: list, response_format: Any) -> AgentExecutor:
38 """Create a ReAct agent with the given LLM and tools."""
39
40 prompt = PromptTemplate.from_template("""Answer the following questions as best you can. You have access to the following tools:
41
42{tools}
43
44Use the following format:
45
46Question: the input question you must answer
47Thought: you should always think about what to do
48Action: the action to take, should be one of {tool_names}
49Action Input: the input to the action
50Observation: the result of the action
51... (this Thought/Action/Action Input/Observation can repeat N times)
52Thought: I now know the final answer
53Final Answer: the final answer to the original input question
54
55Begin! Remember to ALWAYS follow the format above - start with Thought, then Action, then Action Input.
56
57Question: {input}
58
59{agent_scratchpad}""")
60
61
62 agent = create_base_agent(llm, tools, prompt)
63
64 return AgentExecutor(
65 agent=agent,
66 tools=tools,
67 verbose=True,
68 handle_parsing_errors=True,
69 max_iterations=6
70 )
71
72def format_job_results(jobs: List[Dict[str, Any]]) -> str:
73 """Format job results into a readable report"""
74 if not jobs:
75 return "No jobs found matching your criteria."
76
77 report = "# Available Job Opportunities\n\n"
78
79 for i, job in enumerate(jobs, 1):
80 report += f"## {i}. {job['title']}\n"
81 report += f"**Company:** {job['company']}\n"
82 report += f"**Location:** {job['location']}\n"
83 report += f"**Type:** {job['employment_type']}\n"
84 report += f"**Salary:** {job['salary']}\n"
85 report += f"**Posted:** {job['posting_date']}\n"
86 report += f"**Description:** {job['description']}\n"
87 report += f"**Apply here:** {job['url']}\n\n"
88 report += "---\n\n"
89
90 return report
91
92
93system_message = """You are a job search assistant. When searching for jobs, you MUST ONLY return a JSON response wrapped in code block markers, with NO OTHER TEXT before or after. Format exactly like this:
94
95```json
96{
97 "summary": {
98 "total_jobs_found": <number>,
99 "skills_matched": ["skill1", "skill2", ...],
100 "experience_years": <number>,
101 "previous_position": "position title"
102 },
103 "jobs": [
104 {
105 "title": "Job Title",
106 "company": "Company Name",
107 "location": "Location",
108 "posting_date": "YYYY-MM-DD",
109 "employment_type": "Full-time/Contract/etc",
110 "salary": "Salary Range",
111 "description": "Brief job description",
112 "url": "Application URL",
113 "is_remote": true/false,
114 "skills_match": ["matched_skill1", "matched_skill2", ...],
115 "match_percentage": 85
116 }
117 ]
118}
119```
120
121CRITICAL RULES:
1221. Return ONLY the JSON code block above - no other text
1232. Always start with ```json and end with ```
1243. Ensure the JSON is valid and properly formatted
1254. Do not include any explanations or thoughts in the output
1265. Fill in all fields, using "Not specified" for missing values
127"""
128
129async def charge_for_actor_start() -> None:
130
131 pass
132
133async def main() -> None:
134 """Main entry point for the Apify Actor."""
135 async with Actor:
136
137 await charge_for_actor_Run()
138
139 actor_input = await Actor.get_input() or fallback_input
140 resume = actor_input.get('resume', '')
141 location = actor_input.get('location', 'Remote')
142 job_type = actor_input.get('jobType', 'full-time')
143 keywords = actor_input.get('keywords', '')
144 model_name=actor_input.get('model_name', '')
145
146
147 llm = ChatOpenAI(
148 model_name="gpt-3.5-turbo",
149 temperature=0.7,
150 max_tokens=2000
151 )
152
153
154 tools = [tool_linkedin_search, tool_indeed_search, tool_dice_search, analyze_resume]
155
156
157 tool_names = [tool.name for tool in tools]
158
159
160 agent = setup_react_agent(llm, tools, None)
161
162
163 result = await agent.ainvoke(
164 {
165 "input": f"""Find relevant job opportunities based on this resume and preferences:
166Resume:
167{resume}
168
169Job Preferences:
170- Location: {location}
171- Job Type: {job_type}
172- Keywords: {keywords}
173
174Analyze the resume and search for matching jobs. Return a JSON response with:
1751. A brief summary of the search results
1762. An array of relevant jobs found (limit to top 5 most relevant)
1773. Recommended next steps for the job seeker
178
179Format the response as a JSON object with these exact fields:
180{{
181 "summary": "Brief overview of search results",
182 "jobs": [
183 {{
184 "title": "Job title",
185 "company": "Company name",
186 "location": "Job location",
187 "salary": "Salary if available",
188 "match_score": "Relevance score 0-1",
189 "url": "Job posting URL"
190 }}
191 ],
192 "recommendations": ["List of recommended next steps"]
193}}""",
194 "tools": tools,
195 "tool_names": tool_names
196 }
197 )
198
199
200 try:
201 if isinstance(result, dict) and 'output' in result:
202 output = result['output']
203
204
205 json_data = None
206
207
208 if isinstance(output, str):
209 try:
210 json_data = json.loads(output)
211 except json.JSONDecodeError:
212
213 json_match = re.search(r'```(?:json)?\s*({\s*".*?})\s*```', output, re.DOTALL)
214 if json_match:
215 try:
216 json_data = json.loads(json_match.group(1).strip())
217 except json.JSONDecodeError:
218 pass
219
220 if json_data:
221
222 cleaned_data = {
223 "summary": json_data.get("summary", "No summary provided"),
224 "jobs": json_data.get("jobs", [])[:5],
225 "recommendations": json_data.get("recommendations", [])
226 }
227 await Actor.push_data(cleaned_data)
228 else:
229 await Actor.push_data({
230 "error": "Could not parse JSON output",
231 "raw_output": output
232 })
233 else:
234 await Actor.push_data({
235 "error": "Unexpected output format",
236 "raw_output": str(result)
237 })
238
239 except Exception as e:
240 Actor.log.error(f"Failed to process results: {str(e)}")
241 await Actor.push_data({
242 "error": f"Failed to process results: {str(e)}",
243 "raw_output": str(result)
244 })
245
246if __name__ == "__main__":
247 Actor.main(main)