import os from dotenv import load_dotenv from langchain_openai import ChatOpenAI, OpenAIEmbeddings from langchain_community.vectorstores import FAISS from langchain.agents import AgentExecutor, create_openai_tools_agent from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder from langchain.tools import tool from github import Github from github import Auth load_dotenv() # Constants / Config COMPANY_NAME = "TechFlow Solutions" COMPANY_CONTACT = "support@techflow.com | +1-555-0199" DB_PATH = "vector_db" def get_vector_store(): embeddings = OpenAIEmbeddings() vector_store = FAISS.load_local( DB_PATH, embeddings, allow_dangerous_deserialization=True ) return vector_store @tool def search_knowledge_base(query: str) -> str: """ Search the company knowledge base for answers to user questions. Returns relevance of text and citations (source files and page numbers). """ try: vector_store = get_vector_store() # Use relevance scores (0 to 1, where 1 is best match) results = vector_store.similarity_search_with_relevance_scores(query, k=5) response = "" relevant_count = 0 # Threshold for relevance (0.7 is a reasonable baseline for OpenAI embeddings) THRESHOLD = 0.7 print(f"\n--- Search Query: '{query}' ---") for i, (doc, score) in enumerate(results): print(f"Result {i+1}: Score {score:.4f} | Content: {doc.page_content[:50]}...") if score < THRESHOLD: print(f" -> FILTERED (Below {THRESHOLD})") continue relevant_count += 1 print(f" -> ACCEPTED") if score < THRESHOLD: continue relevant_count += 1 source = doc.metadata.get("source", "Unknown") page = doc.metadata.get("page", "Unknown") # Extract just the filename from the path filename = os.path.basename(source) response += f"--- Result {relevant_count} (Score: {score:.2f}) ---\n" response += f"Content: {doc.page_content}\n" response += f"Source: {filename}, Page: {page}\n\n" return response if response else "No relevant information found in the knowledge base (all results below threshold)." except Exception as e: return f"Error searching knowledge base: {str(e)}" class TicketSystem: def __init__(self, token, repo_name): auth = Auth.Token(token) self.g = Github(auth=auth) self.repo = self.g.get_repo(repo_name) def create_ticket(self, title, body, project_folder): """ project_folder: Project folder name (e.g. 'Capstone2-1.2Antigravity') """ # 1. Check or create label label_name = project_folder.lower().replace("/", "-").replace(" ", "-") try: self.repo.get_label(label_name) except: # Create new label (blue) self.repo.create_label(name=label_name, color="0075ca") # 2. Decorate title full_title = f"[{project_folder}] {title}" # 3. Add details to body full_body = f"**Project:** {project_folder}\n\n**Description:**\n{body}" # 4. Create Issue new_issue = self.repo.create_issue( title=full_title, body=full_body, labels=[label_name, "customer-support"] ) return new_issue def create_github_issue(summary: str, description: str, user_email: str, user_name: str) -> str: token = os.getenv("GITHUB_TOKEN") repo_name = os.getenv("REPO_NAME") project_folder = os.getenv("PROJECT_FOLDER", "Capstone Project") if not token or not repo_name: return "Error: GitHub credentials not configured. Cannot create ticket." try: ticket_system = TicketSystem(token, repo_name) # Combine user details into the body description full_description = f"**User Name:** {user_name}\n**User Email:** {user_email}\n\n{description}" issue = ticket_system.create_ticket( title=summary, body=full_description, project_folder=project_folder ) return f"Ticket created successfully! Ticket ID: #{issue.number}. Link: {issue.html_url}" except Exception as e: return f"Error creating ticket: {str(e)}" @tool def create_support_ticket(summary: str, description: str, user_email: str, user_name: str) -> str: """ Create a support ticket (GitHub Issue) for the user. Use this when the knowledge base doesn't have the answer or the user explicitly asks to raise a ticket. Include all details: user name, email, issue summary and full description. """ return create_github_issue(summary, description, user_email, user_name) def create_agent(): llm = ChatOpenAI(model="gpt-4o", temperature=0) tools = [search_knowledge_base, create_support_ticket] system_prompt = f"""You are a helpful and professional customer support agent for {COMPANY_NAME}. Company Contact Info: {COMPANY_CONTACT} Your goal is to assist users with their questions using the available tools. GUIDELINES: 1. **ALWAYS SEARCH**: You MUST use the `search_knowledge_base` tool for **EVERY** user message, even if it looks like a typo, gibberish, or nonsense. - **Reason**: The search tool has internal logic to handle/reject irrelevant queries. You must let it run. - **Do not** simply reply "It seems like a typo" without calling the tool first. 2. **Intent**: If you can infer a valid term (e.g. "solutiun" -> "Solution"), search for the corrected term. If it is total gibberish, search for the gibberish exactly. 2. **Comprehensive Synthesis**: Use the provided search results to answer the user's question. - **Summarize ALL chunks**: You must synthesize information from ALL relevant chunks provided by the search tool. - **Proactive Answering**: If exact matches aren't found, define related concepts (e.g., Software Architecture for Solution Architecture) found in the text. - **NEVER** refuse to answer if there is ANY retrieved text that is even remotely technical or relevant. 3. **MANDATORY CITATIONS**: You MUST list **ALL** source citations found in the search results at the end of your response. - Even if you summarize multiple chunks, list every unique source/page used. - Format: `**Source 1:** [filename] (Page [number])` 4. **IF ANSWER NOT FOUND**: Only if the search results are completely empty or nonsensical string matches, state: "I could not find the answer in the knowledge base." 5. **Ticket Creation**: If you truly cannot help, or if the user explicitly asks, create a support ticket using `create_support_ticket`. 6. Required details for a ticket: Title (Summary), Description, User Name, User Email. 7. Be polite and concise. """ prompt = ChatPromptTemplate.from_messages([ ("system", system_prompt), MessagesPlaceholder(variable_name="chat_history"), ("human", "{input}"), MessagesPlaceholder(variable_name="agent_scratchpad"), ]) agent = create_openai_tools_agent(llm, tools, prompt) agent_executor = AgentExecutor( agent=agent, tools=tools, verbose=True, handle_parsing_errors=True ) return agent_executor