""" Professional chatbot app that combines personal and company information. Resources read: - me/summary.txt - me/linkedin.pdf - me/company_info.docx Environment variables used: - GOOGLE_API_KEY - PUSHOVER_USER (optional) - PUSHOVER_TOKEN (optional) - PROFILE_NAME (optional, defaults to "Your Name") - COMPANY_NAME (optional, defaults to "Your Company") Run: uv run python app1.py Note: Ensure dependencies are installed: python-docx, pypdf, gradio, python-dotenv, openai, requests """ from __future__ import annotations import json import os from typing import Any, Dict, List, Optional import gradio as gr import requests from dotenv import load_dotenv from pypdf import PdfReader from docx import Document from openai import OpenAI # ----------------------------- # Environment and clients # ----------------------------- PROFILE_NAME = "Smith Bhavsar" COMPANY_NAME = "Koderxpert Technologies LLP" # Resource paths SUMMARY_PATH = "me/summary.txt" LINKEDIN_PDF_PATH = "me/resume.pdf" COMPANY_DOCX_PATH = "me/company_info.docx" # Gemini (OpenAI-compatible) client load_dotenv(override=True) gemini = OpenAI( api_key=os.getenv("GOOGLE_API_KEY"), base_url="https://generativelanguage.googleapis.com/v1beta/openai/" ) # ----------------------------- # Optional notifications (Pushover) # ----------------------------- PUSHOVER_USER = os.getenv("pushover_user") PUSHOVER_TOKEN = os.getenv("PUSHOVER_TOKEN") PUSHOVER_URL = "https://api.pushover.net/1/messages.json" def push_notification(message: str) -> None: if not (PUSHOVER_USER and PUSHOVER_TOKEN): # Silent no-op if not configured return try: payload = {"user": PUSHOVER_USER, "token": PUSHOVER_TOKEN, "message": message} requests.post(PUSHOVER_URL, data=payload, timeout=5) except Exception: # Avoid raising in request path pass # ----------------------------- # Utilities to read files # ----------------------------- def read_text_file(path: str) -> str: try: with open(path, "r", encoding="utf-8") as f: return f.read().strip() except FileNotFoundError: return "" except Exception: return "" def read_pdf_text(path: str) -> str: try: reader = PdfReader(path) combined = [] for page in reader.pages: text = page.extract_text() or "" if text: combined.append(text) return "\n\n".join(combined).strip() except Exception: return "" def read_docx_text(path: str) -> str: try: doc = Document(path) paragraphs = [p.text for p in doc.paragraphs if p.text] return "\n".join(paragraphs).strip() except Exception: return "" # ----------------------------- # Tools (function calling) # ----------------------------- def record_user_details(email: str, name: str = "Name not provided", notes: str = "not provided") -> Dict[str, str]: push_notification(f"Recording interest from {name} with email {email} and notes {notes}") return {"recorded": "ok"} def record_unknown_question(question: str) -> Dict[str, str]: push_notification(f"Recording question I couldn't answer: {question}") return {"recorded": "ok"} record_user_details_json: Dict[str, Any] = { "name": "record_user_details", "description": "Record that a user is interested in being in touch and provided an email address.", "parameters": { "type": "object", "properties": { "email": {"type": "string", "description": "The user's email address."}, "name": {"type": "string", "description": "The user's name, if provided."}, "notes": { "type": "string", "description": "Any additional context from the conversation worth recording." }, }, "required": ["email"], "additionalProperties": False, }, } record_unknown_question_json: Dict[str, Any] = { "name": "record_unknown_question", "description": "Record any question that could not be answered.", "parameters": { "type": "object", "properties": { "question": {"type": "string", "description": "The question that could not be answered."} }, "required": ["question"], "additionalProperties": False, }, } TOOLS: List[Dict[str, Any]] = [ {"type": "function", "function": record_user_details_json}, {"type": "function", "function": record_unknown_question_json}, ] def handle_tool_calls(tool_calls: Any) -> List[Dict[str, Any]]: results: List[Dict[str, Any]] = [] for tool_call in tool_calls: tool_name: str = tool_call.function.name arguments: Dict[str, Any] = json.loads(tool_call.function.arguments) tool = globals().get(tool_name) result: Dict[str, Any] = tool(**arguments) if callable(tool) else {} results.append( {"role": "tool", "content": json.dumps(result), "tool_call_id": tool_call.id} ) return results summary_text = read_text_file(SUMMARY_PATH) linkedin_text = read_pdf_text(LINKEDIN_PDF_PATH) company_text = read_docx_text(COMPANY_DOCX_PATH) def build_system_prompt() -> str: sections: List[str] = [] sections.append( ( f"You are a professional AI assistant representing {COMPANY_NAME}. " f"Your role is to answer questions about personal background, career, skills, and experience of {PROFILE_NAME}, " f"as well as offerings, capabilities, case studies, and differentiators of {COMPANY_NAME}. " f"Adopt a concise, warm, and helpful tone suitable for prospective clients, partners, or recruiters. " f"If an answer is unknown or not present in the provided context, state that transparently and use the provided tool to record the unknown question. " f"If the user expresses interest in follow-up, politely ask for their email and record it with the provided tool." ) ) if summary_text: sections.append("\n## Personal Summary\n" + summary_text) if linkedin_text: sections.append("\n## LinkedIn Profile Extract\n" + linkedin_text) if company_text: sections.append("\n## Company Information\n" + company_text) sections.append( ( "\nGuidelines:\n" "- Be accurate and avoid speculation; cite from the provided context when helpful.\n" "- Keep answers 3-5 sentences by default; expand only when the user requests more detail.\n" "- If you cannot answer, apologize briefly and call the record_unknown_question tool with the user's question.\n" "- When the user indicates interest or asks about next steps, request their email and call record_user_details with any context.\n" "- Do not fabricate confidential details or personal data that is not in the context.\n" ) ) sections.append( f"\nStay in character as a professional representative of {COMPANY_NAME} and {PROFILE_NAME}." ) return "\n\n".join(sections) SYSTEM_PROMPT = build_system_prompt() # ----------------------------- # Chat loop used by Gradio # ----------------------------- def chat(message: str, history: List[Dict[str, str]]) -> str: messages: List[Dict[str, Any]] = ( [{"role": "system", "content": SYSTEM_PROMPT}] + history + [{"role": "user", "content": message}] ) while True: response = gemini.chat.completions.create( model="gemini-2.0-flash", messages=messages, tools=TOOLS, ) finish_reason = response.choices[0].finish_reason if finish_reason == "tool_calls": assistant_message = response.choices[0].message tool_calls = assistant_message.tool_calls results = handle_tool_calls(tool_calls) messages.append(assistant_message) messages.extend(results) continue else: return response.choices[0].message.content or "" # ----------------------------- # Gradio App # ----------------------------- def build_interface() -> gr.Blocks: theme = gr.themes.Soft(primary_hue="blue") title = f"{COMPANY_NAME} – Professional Assistant" description = ( "Ask about experience, skills, projects, and services. " "Provide your email if you'd like a follow-up." ) example_categories = { "📌 Company Profile": [ "What services does your company offer?", f"Can you summarize {PROFILE_NAME}'s background?", "Can you give a brief history of Koderxpert Technologies LLP?", "What is your mission and vision statement?", ], "🛠 Odoo ERP Expertise": [ "Do you provide Odoo ERP implementation and customization?", "Can you explain your Odoo Certified Expert status?", "Which Odoo modules do you specialize in?", "Have you done Odoo migrations from older versions or other ERPs?", ], "📂 Case Studies": [ "Tell me about the Manel Fashion case study.", "Can you share an example of a manufacturing industry project?", "What kind of results have your clients seen after implementation?", ], "🌍 Global Presence": [ "Do you work with clients outside India?", "Which industries do you serve the most?", "What countries have you delivered ERP projects in?", ], "👨‍💻 Hiring & Engagement": [ "Can I hire an Odoo developer from your company?", "Do you offer long-term support packs?", ], "📞 Contact": [ "How can I get in touch for a consultation?", "What are your office locations?", "Who is the founder of Koderxpert?", ] } def handle_text_input(message, history): history.append({"role": "user", "content": message}) bot_reply = chat(message, history) history.append({"role": "assistant", "content": bot_reply}) return history,"" with gr.Blocks(theme=theme, css=""" .input-row {display: flex; gap: 6px; align-items: center;} .example-section {margin-bottom: 12px;} """) as demo: gr.Markdown(f"## {title}\n{description}") chatbot = gr.Chatbot(type="messages", height=400) with gr.Row(): text_input = gr.Textbox( placeholder="Ask me something...", show_label=False, container=True, scale=10 ) send_btn = gr.Button("Send", scale=1) # Examples BELOW chat but above input row for cleaner look for category, questions in example_categories.items(): with gr.Accordion(category, open=False): for q in questions: gr.Button(q).click( handle_text_input, inputs=[gr.Textbox(value=q, visible=False), chatbot], outputs=[chatbot, text_input] ) # Bind manual input send text_input.submit(handle_text_input, [text_input, chatbot], [chatbot, text_input]) send_btn.click(handle_text_input, [text_input, chatbot], [chatbot, text_input]) return demo if __name__ == "__main__": app = build_interface() # On Spaces, Gradio sets appropriate server name/port; locally defaults are fine app.launch()