|
|
|
import json |
|
import logging |
|
from datetime import datetime |
|
from typing import Dict, List, Optional, Any |
|
import gradio as gr |
|
from openai import AsyncOpenAI |
|
|
|
|
|
logging.basicConfig(level=logging.INFO) |
|
logger = logging.getLogger(__name__) |
|
|
|
|
|
|
|
CONVERSATION_PROMPT = """You are LOSS DOG, a professional profile builder. Your goal is to have natural conversations |
|
with users to gather information about their professional background across 9 categories: |
|
|
|
1. Work History & Experience |
|
2. Salary & Compensation |
|
3. Skills & Certifications |
|
4. Education & Learning |
|
5. Personal Branding & Online Presence |
|
6. Achievements & Awards |
|
7. Social Proof & Networking |
|
8. Project Contributions & Leadership |
|
9. Work Performance & Impact Metrics |
|
|
|
Be friendly and conversational. Ask follow-up questions naturally. When appropriate, guide users to share more details |
|
but respect their boundaries. Once you believe you have gathered sufficient information (or if the user indicates they |
|
have nothing more to share), let them know they can click 'Generate Profile' to proceed. |
|
""" |
|
EXTRACTION_PROMPT = """You are a data extraction specialist. Your task is to: |
|
1. Read through the provided conversation |
|
2. Identify relevant information across 9 categories: |
|
- Work History & Experience (jobs, roles, companies) |
|
- Salary & Compensation (if shared) |
|
- Skills & Certifications |
|
- Education & Learning |
|
- Personal Branding & Online Presence |
|
- Achievements & Awards |
|
- Social Proof & Networking |
|
- Project Contributions & Leadership |
|
- Work Performance & Impact Metrics |
|
|
|
3. Clean and structure the information: |
|
- Deduplicate repeated information |
|
- Resolve any inconsistencies |
|
- Make reasonable inferences when dates or details are partial |
|
- Standardize formatting (dates, company names, titles) |
|
|
|
4. Output a VALID JSON object with this exact structure: |
|
{ |
|
"work_history_experience": { |
|
"positions": [ |
|
{ |
|
"title": "cleaned job title", |
|
"company": "cleaned company name", |
|
"duration": "standardized duration", |
|
"description": "cleaned description", |
|
"confidence": 0.95, |
|
"inferred": false |
|
} |
|
] |
|
}, |
|
"skills_certifications": { |
|
"technical_skills": ["skill1", "skill2"], |
|
"certifications": [ |
|
{ |
|
"name": "certification name", |
|
"issuer": "issuing organization", |
|
"date": "YYYY-MM", |
|
"confidence": 0.9 |
|
} |
|
] |
|
} |
|
// ... other categories following similar structure |
|
} |
|
|
|
IMPORTANT: |
|
- Return ONLY valid JSON |
|
- Always include confidence scores (0.0-1.0) |
|
- Mark any inferred information |
|
- Use consistent date formats (YYYY-MM-DD) |
|
- Clean and standardize all text fields |
|
- Return empty arrays [] for missing sections rather than null |
|
|
|
Example conversation snippet: |
|
User: "I worked at Google for a few years" |
|
Assistant: "That's interesting! What was your role there?" |
|
User: "I was a senior engineer, mostly doing ML stuff" |
|
|
|
Should extract to: |
|
{ |
|
"work_history_experience": { |
|
"positions": [ |
|
{ |
|
"title": "Senior ML Engineer", |
|
"company": "Google", |
|
"duration": { |
|
"start": null, |
|
"end": null, |
|
"description": "multiple years", |
|
"inferred": true |
|
}, |
|
"description": "Machine learning engineering", |
|
"confidence": 0.85 |
|
} |
|
] |
|
} |
|
}""" |
|
class ProfileBuilder: |
|
def __init__(self): |
|
self.conversation_history = [] |
|
self.client = None |
|
|
|
def _initialize_client(self, api_key: str) -> None: |
|
"""Initialize AsyncOpenAI client with API key.""" |
|
if not api_key.startswith("sk-"): |
|
raise ValueError("Invalid API key format") |
|
self.client = AsyncOpenAI(api_key=api_key) |
|
|
|
async def process_message(self, message: str, api_key: str) -> Dict[str, Any]: |
|
"""Process a user message through conversation phase.""" |
|
try: |
|
if not self.client: |
|
self._initialize_client(api_key) |
|
|
|
|
|
self.conversation_history.append({"role": "user", "content": message}) |
|
|
|
|
|
completion = await self.client.chat.completions.create( |
|
model="gpt-4o-mini", |
|
messages=[ |
|
{"role": "system", "content": CONVERSATION_PROMPT}, |
|
*self.conversation_history |
|
], |
|
temperature=0.7 |
|
) |
|
|
|
ai_message = completion.choices[0].message.content |
|
self.conversation_history.append({"role": "assistant", "content": ai_message}) |
|
|
|
return {"response": ai_message} |
|
|
|
except Exception as e: |
|
logger.error(f"Error processing message: {str(e)}") |
|
return {"error": str(e)} |
|
|
|
async def generate_profile(self) -> Dict[str, Any]: |
|
"""Process conversation history into structured profile.""" |
|
try: |
|
if not self.client: |
|
raise ValueError("OpenAI client not initialized") |
|
|
|
|
|
conversation_text = "\n".join( |
|
f"{msg['role']}: {msg['content']}" |
|
for msg in self.conversation_history |
|
) |
|
|
|
|
|
completion = await self.client.chat.completions.create( |
|
model="gpt-4o-mini", |
|
messages=[ |
|
{"role": "system", "content": EXTRACTION_PROMPT}, |
|
{"role": "user", "content": f"Extract profile information from this conversation:\n\n{conversation_text}"} |
|
], |
|
temperature=0.3 |
|
) |
|
|
|
|
|
profile_data = json.loads(completion.choices[0].message.content) |
|
|
|
|
|
profile = { |
|
"profile_data": profile_data, |
|
"metadata": { |
|
"generated_at": datetime.now().isoformat(), |
|
"conversation_length": len(self.conversation_history) |
|
} |
|
} |
|
|
|
|
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") |
|
filename = f"profile_{timestamp}.json" |
|
with open(filename, 'w', encoding='utf-8') as f: |
|
json.dump(profile, f, indent=2) |
|
|
|
return { |
|
"profile": profile, |
|
"filename": filename |
|
} |
|
|
|
except Exception as e: |
|
logger.error(f"Error generating profile: {str(e)}") |
|
return {"error": str(e)} |
|
|
|
def create_gradio_interface(): |
|
"""Create the Gradio interface.""" |
|
builder = ProfileBuilder() |
|
|
|
with gr.Blocks(theme=gr.themes.Soft()) as demo: |
|
gr.Markdown("# 🐕 LOSS DOG - Professional Profile Builder") |
|
|
|
with gr.Row(): |
|
with gr.Column(scale=2): |
|
api_key = gr.Textbox( |
|
label="OpenAI API Key", |
|
type="password", |
|
placeholder="Enter your OpenAI API key" |
|
) |
|
|
|
chatbot = gr.Chatbot(label="Conversation") |
|
|
|
with gr.Row(): |
|
msg = gr.Textbox( |
|
label="Message", |
|
placeholder="Chat with LOSS DOG..." |
|
) |
|
send = gr.Button("Send") |
|
|
|
with gr.Column(scale=1): |
|
generate_btn = gr.Button("Generate Profile") |
|
profile_output = gr.JSON(label="Generated Profile") |
|
download_btn = gr.File(label="Download Profile") |
|
|
|
|
|
async def on_message(message: str, history: List[List[str]], key: str): |
|
if not message.strip(): |
|
return history, None |
|
|
|
result = await builder.process_message(message, key) |
|
|
|
if "error" in result: |
|
return history, {"error": result["error"]} |
|
|
|
history = history + [[message, result["response"]]] |
|
return history, None |
|
|
|
async def on_generate(): |
|
result = await builder.generate_profile() |
|
if "error" in result: |
|
return {"error": result["error"]}, None |
|
return result["profile"], result["filename"] |
|
|
|
|
|
msg.submit( |
|
on_message, |
|
inputs=[msg, chatbot, api_key], |
|
outputs=[chatbot, profile_output] |
|
).then(lambda: "", None, msg) |
|
|
|
send.click( |
|
on_message, |
|
inputs=[msg, chatbot, api_key], |
|
outputs=[chatbot, profile_output] |
|
).then(lambda: "", None, msg) |
|
|
|
generate_btn.click( |
|
on_generate, |
|
outputs=[profile_output, download_btn] |
|
) |
|
|
|
return demo |
|
|
|
if __name__ == "__main__": |
|
demo = create_gradio_interface() |
|
demo.queue() |
|
demo.launch( |
|
server_name="0.0.0.0", |
|
server_port=7860 |
|
) |