launchway 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. launchway-0.1.0/Agents/__init__.py +1 -0
  2. launchway-0.1.0/Agents/agent_profile_service.py +207 -0
  3. launchway-0.1.0/Agents/bullet_generation/__init__.py +7 -0
  4. launchway-0.1.0/Agents/bullet_generation/project_bullet_generator.py +369 -0
  5. launchway-0.1.0/Agents/components/__init__.py +1 -0
  6. launchway-0.1.0/Agents/components/action_recorder.py +746 -0
  7. launchway-0.1.0/Agents/components/brains/__init__.py +1 -0
  8. launchway-0.1.0/Agents/components/brains/gemini_button_brain.py +260 -0
  9. launchway-0.1.0/Agents/components/brains/gemini_field_mapper.py +1568 -0
  10. launchway-0.1.0/Agents/components/brains/gemini_page_analyzer.py +174 -0
  11. launchway-0.1.0/Agents/components/custom_exceptions.py +5 -0
  12. launchway-0.1.0/Agents/components/detectors/__init__.py +1 -0
  13. launchway-0.1.0/Agents/components/detectors/account_creation_detector.py +308 -0
  14. launchway-0.1.0/Agents/components/detectors/application_form_detector.py +146 -0
  15. launchway-0.1.0/Agents/components/detectors/apply_detector.py +387 -0
  16. launchway-0.1.0/Agents/components/detectors/auth_page_detector.py +154 -0
  17. launchway-0.1.0/Agents/components/detectors/next_button_detector.py +101 -0
  18. launchway-0.1.0/Agents/components/detectors/popup_detector.py +106 -0
  19. launchway-0.1.0/Agents/components/detectors/required_field_detector.py +356 -0
  20. launchway-0.1.0/Agents/components/detectors/section_detector.py +115 -0
  21. launchway-0.1.0/Agents/components/detectors/sensitive_field_detector.py +195 -0
  22. launchway-0.1.0/Agents/components/detectors/submit_detector.py +100 -0
  23. launchway-0.1.0/Agents/components/exceptions/__init__.py +26 -0
  24. launchway-0.1.0/Agents/components/exceptions/field_exceptions.py +154 -0
  25. launchway-0.1.0/Agents/components/executors/__init__.py +1 -0
  26. launchway-0.1.0/Agents/components/executors/account_creation_handler.py +540 -0
  27. launchway-0.1.0/Agents/components/executors/ats_dropdown_handlers_v2.py +1117 -0
  28. launchway-0.1.0/Agents/components/executors/click_executor.py +107 -0
  29. launchway-0.1.0/Agents/components/executors/cmp_consent.py +187 -0
  30. launchway-0.1.0/Agents/components/executors/deterministic_field_mapper.py +742 -0
  31. launchway-0.1.0/Agents/components/executors/field_interactor_v2.py +2294 -0
  32. launchway-0.1.0/Agents/components/executors/generic_form_filler_v2_enhanced.py +1952 -0
  33. launchway-0.1.0/Agents/components/executors/iframe_helper.py +65 -0
  34. launchway-0.1.0/Agents/components/executors/intelligent_dropdown_selector.py +177 -0
  35. launchway-0.1.0/Agents/components/executors/learned_patterns_mapper.py +371 -0
  36. launchway-0.1.0/Agents/components/executors/popup_executor.py +73 -0
  37. launchway-0.1.0/Agents/components/executors/question_extractor.py +658 -0
  38. launchway-0.1.0/Agents/components/executors/section_filler.py +436 -0
  39. launchway-0.1.0/Agents/components/pattern_recorder.py +447 -0
  40. launchway-0.1.0/Agents/components/router/__init__.py +1 -0
  41. launchway-0.1.0/Agents/components/router/state_machine.py +292 -0
  42. launchway-0.1.0/Agents/components/services/__init__.py +6 -0
  43. launchway-0.1.0/Agents/components/services/company_credentials_service.py +263 -0
  44. launchway-0.1.0/Agents/components/session/__init__.py +1 -0
  45. launchway-0.1.0/Agents/components/session/session_manager.py +711 -0
  46. launchway-0.1.0/Agents/components/state/__init__.py +1 -0
  47. launchway-0.1.0/Agents/components/state/field_completion_tracker.py +176 -0
  48. launchway-0.1.0/Agents/components/utils/__init__.py +1 -0
  49. launchway-0.1.0/Agents/components/utils/google_docs_converter.py +149 -0
  50. launchway-0.1.0/Agents/components/validators/__init__.py +5 -0
  51. launchway-0.1.0/Agents/components/validators/field_value_validator.py +135 -0
  52. launchway-0.1.0/Agents/components/validators/nav_validator.py +102 -0
  53. launchway-0.1.0/Agents/cover_letter_tailoring.py +125 -0
  54. launchway-0.1.0/Agents/gemini_key_manager.py +226 -0
  55. launchway-0.1.0/Agents/gemini_query_optimizer.py +574 -0
  56. launchway-0.1.0/Agents/gemini_rate_limiter.py +237 -0
  57. launchway-0.1.0/Agents/hidden_browser_manager.py +205 -0
  58. launchway-0.1.0/Agents/improved_char_calc.py +208 -0
  59. launchway-0.1.0/Agents/job_api_adapters.py +1141 -0
  60. launchway-0.1.0/Agents/job_application_agent.py +3661 -0
  61. launchway-0.1.0/Agents/job_relevance_scorer.py +387 -0
  62. launchway-0.1.0/Agents/jobspy_adapter.py +208 -0
  63. launchway-0.1.0/Agents/latex_tailoring_agent.py +1220 -0
  64. launchway-0.1.0/Agents/mimikree_cache.py +101 -0
  65. launchway-0.1.0/Agents/mimikree_integration.py +401 -0
  66. launchway-0.1.0/Agents/multi_source_job_discovery_agent.py +774 -0
  67. launchway-0.1.0/Agents/persistent_browser_manager.py +434 -0
  68. launchway-0.1.0/Agents/project_selection/__init__.py +8 -0
  69. launchway-0.1.0/Agents/project_selection/mimikree_project_discovery.py +369 -0
  70. launchway-0.1.0/Agents/project_selection/relevance_engine.py +466 -0
  71. launchway-0.1.0/Agents/proxy_manager.py +290 -0
  72. launchway-0.1.0/Agents/resume_keyword_extractor.py +244 -0
  73. launchway-0.1.0/Agents/resume_tailoring_agent.py +2272 -0
  74. launchway-0.1.0/Agents/space_borrowing.py +209 -0
  75. launchway-0.1.0/Agents/systematic_tailoring_complete.py +1773 -0
  76. launchway-0.1.0/Agents/token.json +1 -0
  77. launchway-0.1.0/Agents/validation/__init__.py +8 -0
  78. launchway-0.1.0/Agents/validation/hallucination_detector.py +292 -0
  79. launchway-0.1.0/Agents/validation/semantic_validator.py +428 -0
  80. launchway-0.1.0/LICENSE +21 -0
  81. launchway-0.1.0/MANIFEST.in +36 -0
  82. launchway-0.1.0/PKG-INFO +148 -0
  83. launchway-0.1.0/README.md +80 -0
  84. launchway-0.1.0/launchway/__init__.py +7 -0
  85. launchway-0.1.0/launchway/__main__.py +6 -0
  86. launchway-0.1.0/launchway/api_client.py +270 -0
  87. launchway-0.1.0/launchway/cli/__init__.py +1 -0
  88. launchway-0.1.0/launchway/cli/agent.py +169 -0
  89. launchway-0.1.0/launchway/cli/mixins/__init__.py +1 -0
  90. launchway-0.1.0/launchway/cli/mixins/apply.py +350 -0
  91. launchway-0.1.0/launchway/cli/mixins/auth.py +362 -0
  92. launchway-0.1.0/launchway/cli/mixins/browser_setup.py +99 -0
  93. launchway-0.1.0/launchway/cli/mixins/continuous.py +912 -0
  94. launchway-0.1.0/launchway/cli/mixins/history.py +41 -0
  95. launchway-0.1.0/launchway/cli/mixins/job_search.py +201 -0
  96. launchway-0.1.0/launchway/cli/mixins/profile.py +934 -0
  97. launchway-0.1.0/launchway/cli/mixins/settings.py +115 -0
  98. launchway-0.1.0/launchway/cli/mixins/tailoring.py +230 -0
  99. launchway-0.1.0/launchway/cli/utils.py +49 -0
  100. launchway-0.1.0/launchway/config.py +157 -0
  101. launchway-0.1.0/launchway/postinstall.py +55 -0
  102. launchway-0.1.0/launchway/session.py +58 -0
  103. launchway-0.1.0/launchway.egg-info/PKG-INFO +148 -0
  104. launchway-0.1.0/launchway.egg-info/SOURCES.txt +108 -0
  105. launchway-0.1.0/launchway.egg-info/dependency_links.txt +1 -0
  106. launchway-0.1.0/launchway.egg-info/entry_points.txt +2 -0
  107. launchway-0.1.0/launchway.egg-info/requires.txt +46 -0
  108. launchway-0.1.0/launchway.egg-info/top_level.txt +2 -0
  109. launchway-0.1.0/pyproject.toml +147 -0
  110. launchway-0.1.0/setup.cfg +4 -0
@@ -0,0 +1 @@
1
+ """Launchway agent modules — browser automation, AI, resume tailoring."""
@@ -0,0 +1,207 @@
1
+ import sys
2
+ import os
3
+
4
+ # Add parent directory to path to access database_config
5
+ sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
6
+
7
+ from database_config import SessionLocal, User, UserProfile
8
+ from typing import Optional, Dict, Any, Union
9
+ import logging
10
+ from uuid import UUID
11
+
12
+ class AgentProfileService:
13
+ """Service for agents to access user profile data from PostgreSQL"""
14
+
15
+ @staticmethod
16
+ def get_profile_by_user_id(user_id: Union[str, UUID]) -> Optional[Dict[str, Any]]:
17
+ """
18
+ Get complete profile data for a specific user
19
+
20
+ Args:
21
+ user_id: User UUID (can be string or UUID object)
22
+ """
23
+ db = SessionLocal()
24
+ try:
25
+ # Convert string to UUID if needed
26
+ if isinstance(user_id, str):
27
+ user_id = UUID(user_id)
28
+
29
+ # Get user data
30
+ user = db.query(User).filter(User.id == user_id).first()
31
+ if not user:
32
+ logging.error(f"User with ID {user_id} not found")
33
+ return None
34
+
35
+ # Get profile data
36
+ profile = db.query(UserProfile).filter(UserProfile.user_id == user_id).first()
37
+
38
+ # Build complete profile data in the format agents expect
39
+ profile_data = {
40
+ "resume_url": profile.resume_url if profile and profile.resume_url else "",
41
+ "resume_source_type": profile.resume_source_type if profile and profile.resume_source_type else "google_doc",
42
+ "latex_main_tex_path": profile.latex_main_tex_path if profile and profile.latex_main_tex_path else "",
43
+ "latex_file_manifest": profile.latex_file_manifest if profile and profile.latex_file_manifest else [],
44
+ "first name": user.first_name,
45
+ "last name": user.last_name,
46
+ "email": user.email,
47
+ "date of birth": profile.date_of_birth if profile and profile.date_of_birth else "",
48
+ "gender": profile.gender if profile and profile.gender else "",
49
+ "nationality": profile.nationality if profile and profile.nationality else "",
50
+ "preferred language": profile.preferred_language if profile and profile.preferred_language else "",
51
+ "phone": profile.phone if profile and profile.phone else "",
52
+ "address": profile.address if profile and profile.address else "",
53
+ "city": profile.city if profile and profile.city else "",
54
+ "state": profile.state if profile and profile.state else "",
55
+ "zip": profile.zip_code if profile and profile.zip_code else "",
56
+ "country": profile.country if profile and profile.country else "",
57
+ "country_code": profile.country_code if profile and profile.country_code else "",
58
+ "state_code": profile.state_code if profile and profile.state_code else "",
59
+ "linkedin": profile.linkedin if profile and profile.linkedin else "",
60
+ "github": profile.github if profile and profile.github else "",
61
+ "other links": profile.other_links if profile and profile.other_links else [""],
62
+ "education": profile.education if profile and profile.education else [
63
+ {
64
+ "degree": "",
65
+ "institution": "",
66
+ "graduation_year": "",
67
+ "gpa": "",
68
+ "relevant_courses": [""]
69
+ }
70
+ ],
71
+ "work experience": profile.work_experience if profile and profile.work_experience else [
72
+ {
73
+ "title": "",
74
+ "company": "",
75
+ "start_date": "",
76
+ "end_date": "",
77
+ "description": "",
78
+ "achievements": [""]
79
+ }
80
+ ],
81
+ "projects": profile.projects if profile and profile.projects else [
82
+ {
83
+ "name": "",
84
+ "description": "",
85
+ "technologies": [""],
86
+ "github_url": "",
87
+ "live_url": "",
88
+ "features": [""]
89
+ }
90
+ ],
91
+ "skills": profile.skills if profile and profile.skills else {
92
+ "technical": [""],
93
+ "programming_languages": [""],
94
+ "frameworks": [""],
95
+ "tools": [""],
96
+ "soft_skills": [""],
97
+ "languages": [""]
98
+ },
99
+ "summary": profile.summary if profile and profile.summary else "",
100
+ "disabilities": profile.disabilities if profile and profile.disabilities else [],
101
+ "veteran status": profile.veteran_status if profile and profile.veteran_status else "",
102
+ "visa status": profile.visa_status if profile and profile.visa_status else "",
103
+ "visa sponsorship": profile.visa_sponsorship if profile and profile.visa_sponsorship else "",
104
+ "preferred location": profile.preferred_location if profile and profile.preferred_location else [""],
105
+ "willing to relocate": profile.willing_to_relocate if profile and profile.willing_to_relocate is not None else "",
106
+ # AI Engine — included so agents can build a GeminiKeyManager
107
+ "api_primary_mode": profile.api_primary_mode if profile and profile.api_primary_mode else "launchway",
108
+ "api_secondary_mode": profile.api_secondary_mode if profile and profile.api_secondary_mode else None,
109
+ # Decrypted key (server-side only; never sent to CLI clients)
110
+ "custom_gemini_api_key_decrypted": AgentProfileService._decrypt_api_key(
111
+ profile.custom_gemini_api_key if profile else None
112
+ ),
113
+ }
114
+
115
+ return profile_data
116
+
117
+ except Exception as e:
118
+ logging.error(f"Error getting profile for user {user_id}: {e}")
119
+ return None
120
+ finally:
121
+ db.close()
122
+
123
+ @staticmethod
124
+ def _decrypt_api_key(encrypted: Optional[str]) -> Optional[str]:
125
+ """Decrypt the stored Gemini API key using the server's security manager."""
126
+ if not encrypted:
127
+ return None
128
+ try:
129
+ import sys, os
130
+ sys.path.append(os.path.join(os.path.dirname(__file__), '..', 'server'))
131
+ from security_manager import security_manager
132
+ return security_manager.decrypt_sensitive_data(encrypted)
133
+ except Exception as e:
134
+ logging.warning(f"Could not decrypt custom API key: {e}")
135
+ return None
136
+
137
+ @staticmethod
138
+ def get_gemini_key_manager(user_id: Union[str, UUID]) -> Optional[Any]:
139
+ """
140
+ Return a fully configured GeminiKeyManager for the given user.
141
+ Falls back to Launchway shared key if nothing is configured.
142
+ """
143
+ try:
144
+ profile = AgentProfileService.get_profile_by_user_id(user_id)
145
+ if not profile:
146
+ return None
147
+ from gemini_key_manager import GeminiKeyManager
148
+ return GeminiKeyManager.from_profile(profile)
149
+ except Exception as e:
150
+ logging.warning(f"Could not build GeminiKeyManager for {user_id}: {e}")
151
+ return None
152
+
153
+ @staticmethod
154
+ def get_profile_by_email(email: str) -> Optional[Dict[str, Any]]:
155
+ """Get complete profile data for a user by email"""
156
+ db = SessionLocal()
157
+ try:
158
+ user = db.query(User).filter(User.email == email).first()
159
+ if not user:
160
+ logging.error(f"User with email {email} not found")
161
+ return None
162
+
163
+ return AgentProfileService.get_profile_by_user_id(user.id)
164
+
165
+ except Exception as e:
166
+ logging.error(f"Error getting profile for email {email}: {e}")
167
+ return None
168
+ finally:
169
+ db.close()
170
+
171
+ @staticmethod
172
+ def get_latest_user_profile() -> Optional[Dict[str, Any]]:
173
+ """Get the most recently created user's profile (for backward compatibility)"""
174
+ db = SessionLocal()
175
+ try:
176
+ # Get the most recently created user
177
+ latest_user = db.query(User).order_by(User.created_at.desc()).first()
178
+ if not latest_user:
179
+ logging.error("No users found in database")
180
+ return None
181
+
182
+ return AgentProfileService.get_profile_by_user_id(latest_user.id)
183
+
184
+ except Exception as e:
185
+ logging.error(f"Error getting latest user profile: {e}")
186
+ return None
187
+ finally:
188
+ db.close()
189
+
190
+ @staticmethod
191
+ def validate_profile_completeness(profile_data: Dict[str, Any]) -> Dict[str, Any]:
192
+ """Validate if profile has sufficient data for job applications"""
193
+ required_fields = {
194
+ "first name": profile_data.get("first name", ""),
195
+ "last name": profile_data.get("last name", ""),
196
+ "email": profile_data.get("email", ""),
197
+ "phone": profile_data.get("phone", ""),
198
+ "resume_url": profile_data.get("resume_url", "")
199
+ }
200
+
201
+ missing_fields = [field for field, value in required_fields.items() if not value]
202
+
203
+ return {
204
+ "is_complete": len(missing_fields) == 0,
205
+ "missing_fields": missing_fields,
206
+ "completion_percentage": ((len(required_fields) - len(missing_fields)) / len(required_fields)) * 100
207
+ }
@@ -0,0 +1,7 @@
1
+ """
2
+ Bullet generation modules for creating tailored project descriptions.
3
+ """
4
+
5
+ from .project_bullet_generator import ProjectBulletGenerator
6
+
7
+ __all__ = ['ProjectBulletGenerator']
@@ -0,0 +1,369 @@
1
+ """
2
+ Project Bullet Generator
3
+
4
+ Generates tailored resume bullets for projects based on job requirements.
5
+ """
6
+
7
+ import re
8
+ import logging
9
+ from typing import Dict, List, Optional
10
+ import google.generativeai as genai
11
+
12
+ # Set up logging
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ class ProjectBulletGenerator:
17
+ """Generates tailored project bullets for resumes"""
18
+
19
+ def __init__(self, gemini_api_key: str, model_name: str = "gemini-2.5-flash"):
20
+ """
21
+ Initialize the bullet generator.
22
+
23
+ Args:
24
+ gemini_api_key: Gemini API key
25
+ model_name: Gemini model to use
26
+ """
27
+ genai.configure(api_key=gemini_api_key)
28
+ self.model = genai.GenerativeModel(model_name)
29
+ self.logger = logger
30
+
31
+ def generate_bullets(
32
+ self,
33
+ project: Dict,
34
+ job_keywords: List[str],
35
+ job_description: str,
36
+ target_bullet_count: int = 3,
37
+ char_limit_per_line: int = 90,
38
+ mimikree_context: Optional[str] = None
39
+ ) -> List[str]:
40
+ """
41
+ Generate tailored resume bullets for a project.
42
+
43
+ Args:
44
+ project: Project dict with name, description, technologies, features
45
+ job_keywords: Keywords from job description to emphasize
46
+ job_description: Full job description for context
47
+ target_bullet_count: Number of bullets to generate
48
+ char_limit_per_line: Character limit per bullet line
49
+ mimikree_context: Optional additional context from Mimikree
50
+
51
+ Returns:
52
+ List of bullet strings
53
+ """
54
+ try:
55
+ # Build project context
56
+ project_context = f"""
57
+ PROJECT NAME: {project.get('name', 'Unknown Project')}
58
+
59
+ DESCRIPTION: {project.get('description', 'No description available')}
60
+
61
+ TECHNOLOGIES: {', '.join(project.get('technologies', []))}
62
+
63
+ FEATURES:
64
+ {chr(10).join([f"- {f}" for f in project.get('features', [])])}
65
+ """
66
+
67
+ if project.get('detailed_bullets'):
68
+ project_context += f"\n\nEXISTING BULLETS (for reference):\n"
69
+ project_context += "\n".join([f"- {b}" for b in project.get('detailed_bullets', [])])
70
+
71
+ # Prepare additional context section (avoid backslash in f-string)
72
+ additional_context = ""
73
+ if mimikree_context:
74
+ additional_context = f"ADDITIONAL CONTEXT:\n{mimikree_context}\n"
75
+
76
+ prompt = f"""Generate {target_bullet_count} tailored resume bullet points for this project.
77
+
78
+ {project_context}
79
+
80
+ JOB DESCRIPTION EXCERPT:
81
+ {job_description[:800]}
82
+
83
+ KEY JOB REQUIREMENTS TO EMPHASIZE:
84
+ {', '.join(job_keywords[:10])}
85
+
86
+ {additional_context}
87
+
88
+ REQUIREMENTS FOR BULLETS:
89
+ 1. Each bullet MUST be 2-3 lines long (approximately {char_limit_per_line * 2}-{char_limit_per_line * 3} characters)
90
+ 2. Start with strong action verbs (Built, Developed, Implemented, Designed, Created)
91
+ 3. Include quantified metrics where possible (users, performance, scale, etc.)
92
+ 4. Naturally incorporate relevant job keywords: {', '.join(job_keywords[:5])}
93
+ 5. Highlight technical skills and impact
94
+ 6. Be specific about technologies and methodologies used
95
+ 7. Follow the format: [Action] [What] using [Technologies] to achieve [Impact/Result]
96
+
97
+ EXAMPLE FORMAT (adapt to this project):
98
+ Built scalable REST API using Node.js and Express, handling 10K+ requests/day and reducing response time by 40% through optimized database queries and caching strategies
99
+
100
+ Implemented real-time chat feature with WebSocket protocol and Redis pub/sub, enabling instant message delivery for 5K+ concurrent users with 99.9% uptime
101
+
102
+ CRITICAL OUTPUT RULES:
103
+ - Return EXACTLY {target_bullet_count} bullets
104
+ - Each bullet on a new line starting with a dash (-)
105
+ - NO numbering, NO extra formatting
106
+ - NO explanations or metadata
107
+ - Just the clean bullet text
108
+
109
+ Generate {target_bullet_count} bullets now:"""
110
+
111
+ response = self.model.generate_content(prompt)
112
+ bullets_text = response.text.strip()
113
+
114
+ # Parse bullets
115
+ bullets = []
116
+ for line in bullets_text.split('\n'):
117
+ line = line.strip()
118
+ # Remove leading dash, number, or asterisk
119
+ line = re.sub(r'^[-•*\d.)\]]+\s*', '', line)
120
+
121
+ if line and len(line) > 20: # Minimum meaningful length
122
+ bullets.append(line)
123
+
124
+ # Validate bullet count
125
+ if len(bullets) < target_bullet_count:
126
+ self.logger.warning(f"Generated only {len(bullets)} bullets, expected {target_bullet_count}")
127
+ elif len(bullets) > target_bullet_count:
128
+ bullets = bullets[:target_bullet_count]
129
+
130
+ # Validate bullet length (should be 2-3 lines)
131
+ validated_bullets = []
132
+ for bullet in bullets:
133
+ lines_count = len(bullet) // char_limit_per_line + (1 if len(bullet) % char_limit_per_line > 0 else 0)
134
+
135
+ if lines_count < 2:
136
+ self.logger.warning(f"Bullet too short ({lines_count} lines): {bullet[:50]}...")
137
+ elif lines_count > 4:
138
+ self.logger.warning(f"Bullet too long ({lines_count} lines): {bullet[:50]}...")
139
+
140
+ validated_bullets.append(bullet)
141
+
142
+ return validated_bullets
143
+
144
+ except Exception as e:
145
+ self.logger.error(f"Failed to generate bullets: {e}")
146
+ # Fallback: create basic bullets from project info
147
+ fallback_bullets = []
148
+
149
+ if project.get('description'):
150
+ fallback_bullets.append(project['description'])
151
+
152
+ for feature in project.get('features', [])[:2]:
153
+ if len(fallback_bullets) < target_bullet_count:
154
+ fallback_bullets.append(f"Implemented {feature} using {', '.join(project.get('technologies', [])[:2])}")
155
+
156
+ return fallback_bullets[:target_bullet_count]
157
+
158
+ def regenerate_bullet(
159
+ self,
160
+ original_bullet: str,
161
+ feedback: str,
162
+ job_keywords: List[str],
163
+ char_limit_per_line: int = 90
164
+ ) -> str:
165
+ """
166
+ Regenerate a single bullet based on user feedback.
167
+
168
+ Args:
169
+ original_bullet: The bullet to improve
170
+ feedback: User feedback (e.g., "add more metrics", "too generic")
171
+ job_keywords: Keywords to emphasize
172
+ char_limit_per_line: Character limit per line
173
+
174
+ Returns:
175
+ Improved bullet string
176
+ """
177
+ try:
178
+ prompt = f"""Improve this resume bullet based on user feedback.
179
+
180
+ ORIGINAL BULLET:
181
+ {original_bullet}
182
+
183
+ USER FEEDBACK:
184
+ {feedback}
185
+
186
+ JOB KEYWORDS TO EMPHASIZE: {', '.join(job_keywords[:5])}
187
+
188
+ REQUIREMENTS:
189
+ - Keep it 2-3 lines long (~{char_limit_per_line * 2}-{char_limit_per_line * 3} characters)
190
+ - Address the user's feedback
191
+ - Maintain or add quantified metrics
192
+ - Incorporate relevant keywords naturally
193
+ - Keep strong action verb at start
194
+
195
+ Return ONLY the improved bullet text, no explanations."""
196
+
197
+ response = self.model.generate_content(prompt)
198
+ improved = response.text.strip()
199
+
200
+ # Clean up formatting
201
+ improved = re.sub(r'^[-•*\d.)\]]+\s*', '', improved)
202
+
203
+ return improved
204
+
205
+ except Exception as e:
206
+ self.logger.error(f"Failed to regenerate bullet: {e}")
207
+ return original_bullet
208
+
209
+ def generate_bullets_batch(
210
+ self,
211
+ projects: List[Dict],
212
+ job_keywords: List[str],
213
+ job_description: str,
214
+ bullets_per_project: int = 3
215
+ ) -> Dict[str, List[str]]:
216
+ """
217
+ Generate bullets for multiple projects in batch.
218
+
219
+ Args:
220
+ projects: List of project dicts
221
+ job_keywords: Keywords from job description
222
+ job_description: Full job description
223
+ bullets_per_project: Number of bullets per project
224
+
225
+ Returns:
226
+ Dict mapping project IDs/names to bullet lists
227
+ """
228
+ results = {}
229
+
230
+ for project in projects:
231
+ project_id = project.get('id') or project.get('name', 'unknown')
232
+
233
+ bullets = self.generate_bullets(
234
+ project,
235
+ job_keywords,
236
+ job_description,
237
+ target_bullet_count=bullets_per_project
238
+ )
239
+
240
+ results[str(project_id)] = bullets
241
+
242
+ self.logger.info(f"Generated {len(bullets)} bullets for project: {project.get('name', 'Unknown')}")
243
+
244
+ return results
245
+
246
+ def enhance_existing_bullets(
247
+ self,
248
+ project: Dict,
249
+ existing_bullets: List[str],
250
+ job_keywords: List[str],
251
+ job_description: str
252
+ ) -> List[str]:
253
+ """
254
+ Enhance existing project bullets to better match job requirements.
255
+
256
+ Args:
257
+ project: Project dict
258
+ existing_bullets: Current bullets
259
+ job_keywords: Keywords from job description
260
+ job_description: Full job description
261
+
262
+ Returns:
263
+ Enhanced bullet list
264
+ """
265
+ try:
266
+ bullets_text = "\n".join([f"- {b}" for b in existing_bullets])
267
+
268
+ prompt = f"""Enhance these project bullets to better match job requirements.
269
+
270
+ PROJECT: {project.get('name', 'Unknown')}
271
+ TECHNOLOGIES: {', '.join(project.get('technologies', []))}
272
+
273
+ CURRENT BULLETS:
274
+ {bullets_text}
275
+
276
+ JOB DESCRIPTION EXCERPT:
277
+ {job_description[:600]}
278
+
279
+ KEY JOB KEYWORDS: {', '.join(job_keywords[:10])}
280
+
281
+ TASK: Enhance each bullet to:
282
+ 1. Better emphasize relevant keywords: {', '.join(job_keywords[:5])}
283
+ 2. Add more quantified metrics where possible
284
+ 3. Highlight technologies and methodologies relevant to the job
285
+ 4. Maintain 2-3 lines per bullet
286
+ 5. Keep all factual claims (don't fabricate metrics)
287
+
288
+ Return the enhanced bullets in the same order, one per line, starting with dash (-)."""
289
+
290
+ response = self.model.generate_content(prompt)
291
+ enhanced_text = response.text.strip()
292
+
293
+ # Parse enhanced bullets
294
+ enhanced = []
295
+ for line in enhanced_text.split('\n'):
296
+ line = line.strip()
297
+ line = re.sub(r'^[-•*\d.)\]]+\s*', '', line)
298
+ if line and len(line) > 20:
299
+ enhanced.append(line)
300
+
301
+ # Ensure same count as original
302
+ if len(enhanced) != len(existing_bullets):
303
+ self.logger.warning(f"Enhanced bullet count mismatch: {len(enhanced)} vs {len(existing_bullets)}")
304
+ return existing_bullets
305
+
306
+ return enhanced
307
+
308
+ except Exception as e:
309
+ self.logger.error(f"Failed to enhance bullets: {e}")
310
+ return existing_bullets
311
+
312
+ def validate_bullet_quality(self, bullet: str) -> Dict[str, any]:
313
+ """
314
+ Validate the quality of a generated bullet.
315
+
316
+ Args:
317
+ bullet: Bullet text to validate
318
+
319
+ Returns:
320
+ Dict with validation results:
321
+ - is_valid: Boolean
322
+ - issues: List of quality issues
323
+ - score: Quality score 0-100
324
+ """
325
+ issues = []
326
+ score = 100.0
327
+
328
+ # Check 1: Length (should be 2-3 lines, ~160-270 chars)
329
+ if len(bullet) < 100:
330
+ issues.append("Too short - add more detail")
331
+ score -= 20
332
+ elif len(bullet) > 350:
333
+ issues.append("Too long - condense")
334
+ score -= 15
335
+
336
+ # Check 2: Starts with action verb
337
+ action_verbs = [
338
+ 'built', 'developed', 'implemented', 'designed', 'created', 'engineered',
339
+ 'architected', 'deployed', 'optimized', 'improved', 'led', 'managed'
340
+ ]
341
+ if not any(bullet.lower().startswith(verb) for verb in action_verbs):
342
+ issues.append("Should start with strong action verb")
343
+ score -= 15
344
+
345
+ # Check 3: Contains technical terms
346
+ tech_pattern = r'\b[A-Z][a-z]+(?:\.[A-Z][a-z]+)*\b|[A-Z]{2,}'
347
+ if not re.search(tech_pattern, bullet):
348
+ issues.append("Should include specific technologies")
349
+ score -= 10
350
+
351
+ # Check 4: Contains metrics (numbers)
352
+ if not re.search(r'\d+[%+kKmM]?', bullet):
353
+ issues.append("Consider adding quantified metrics")
354
+ score -= 10
355
+
356
+ # Check 5: Not too generic
357
+ generic_phrases = ['various', 'multiple', 'several', 'different', 'many']
358
+ if any(phrase in bullet.lower() for phrase in generic_phrases):
359
+ issues.append("Avoid generic phrases - be specific")
360
+ score -= 10
361
+
362
+ is_valid = score >= 70
363
+
364
+ return {
365
+ 'is_valid': is_valid,
366
+ 'issues': issues,
367
+ 'score': score,
368
+ 'length': len(bullet)
369
+ }
@@ -0,0 +1 @@
1
+ """Agent component sub-packages."""