402 lines
15 KiB
Python
402 lines
15 KiB
Python
import os
|
|
import json
|
|
import logging
|
|
logging.getLogger("agent_framework.foundry").setLevel(logging.ERROR)
|
|
from typing import Annotated
|
|
from dotenv import load_dotenv
|
|
import requests
|
|
import re
|
|
|
|
import chainlit as cl
|
|
from mcp import ClientSession
|
|
|
|
from agent_framework import tool, AgentResponseUpdate, WorkflowBuilder
|
|
from agent_framework.foundry import FoundryChatClient
|
|
from azure.identity import AzureCliCredential
|
|
from azure.core.credentials import AzureKeyCredential
|
|
|
|
from azure.search.documents import SearchClient
|
|
from azure.search.documents.indexes import SearchIndexClient
|
|
from azure.search.documents.indexes.models import SearchIndex, SimpleField, SearchFieldDataType, SearchableField
|
|
|
|
|
|
# Load environment variables
|
|
load_dotenv()
|
|
|
|
# Configure logging
|
|
logging.basicConfig(
|
|
level=logging.INFO,
|
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
|
)
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
# Initialize Azure AI Search with persistent storage
|
|
search_service_endpoint = os.getenv("AZURE_SEARCH_SERVICE_ENDPOINT")
|
|
search_api_key = os.getenv("AZURE_SEARCH_API_KEY")
|
|
index_name = "event-descriptions"
|
|
|
|
search_client = SearchClient(
|
|
endpoint=search_service_endpoint,
|
|
index_name=index_name,
|
|
credential=AzureKeyCredential(search_api_key)
|
|
)
|
|
|
|
index_client = SearchIndexClient(
|
|
endpoint=search_service_endpoint,
|
|
credential=AzureKeyCredential(search_api_key)
|
|
)
|
|
|
|
# Define the index schema
|
|
fields = [
|
|
SimpleField(name="id", type=SearchFieldDataType.String, key=True),
|
|
SearchableField(name="content", type=SearchFieldDataType.String)
|
|
]
|
|
|
|
index = SearchIndex(name=index_name, fields=fields)
|
|
|
|
# Check if index already exists if not, create it
|
|
try:
|
|
existing_index = index_client.get_index(index_name)
|
|
print(f"Index '{index_name}' already exists, using the existing index.")
|
|
except Exception as e:
|
|
# Create the index if it doesn't exist
|
|
print(f"Creating new index '{index_name}'...")
|
|
index_client.create_index(index)
|
|
|
|
# Always read event descriptions from markdown file
|
|
current_dir = os.path.dirname(os.path.abspath(__file__))
|
|
event_descriptions_path = os.path.join(current_dir, "event-descriptions.md")
|
|
|
|
try:
|
|
with open(event_descriptions_path, "r", encoding='utf-8') as f:
|
|
markdown_content = f.read()
|
|
except FileNotFoundError:
|
|
logger.warning(f"Could not find {event_descriptions_path}")
|
|
markdown_content = ""
|
|
|
|
# Split the markdown content into individual event descriptions
|
|
event_descriptions = markdown_content.split("---") # You can change the delimiter
|
|
|
|
# Create documents for Azure Search
|
|
documents = []
|
|
for i, description in enumerate(event_descriptions):
|
|
description = description.strip() # Remove leading/trailing whitespace
|
|
if description: # Avoid empty descriptions
|
|
documents.append({"id": str(i + 1), "content": description})
|
|
|
|
# Add documents to the index (only if we have documents)
|
|
if documents:
|
|
# Delete existing documents first to avoid duplicates
|
|
try:
|
|
search_client.delete_documents(documents=[{"id": doc["id"]} for doc in documents])
|
|
print("Cleared existing documents")
|
|
except Exception as e:
|
|
print(f"Warning: Failed to clear existing documents: {str(e)}")
|
|
|
|
# Upload new documents
|
|
search_client.upload_documents(documents)
|
|
print(f"Uploaded {len(documents)} documents to index")
|
|
|
|
|
|
# RAG tool for event search
|
|
@tool
|
|
def search_events(
|
|
query: Annotated[str, "The search query to find relevant events"]
|
|
) -> str:
|
|
"""Searches for relevant events based on a query using Azure Search and a live API."""
|
|
context_strings = []
|
|
try:
|
|
results = search_client.search(query, top=5)
|
|
for result in results:
|
|
if 'content' in result:
|
|
context_strings.append(f"Event: {result['content']}")
|
|
except Exception as e:
|
|
context_strings.append(f"Error searching Azure Search: {str(e)}")
|
|
# Live API (example: Devpost hackathons)
|
|
try:
|
|
api_resp = requests.get(f"https://devpost.com/api/hackathons?search={query}", timeout=5)
|
|
if api_resp.ok:
|
|
data = api_resp.json()
|
|
for event in data.get('hackathons', [])[:5]:
|
|
context_strings.append(f"Live Event: {event.get('title')} - {event.get('url')}")
|
|
except Exception as e:
|
|
context_strings.append(f"Error fetching live events: {str(e)}")
|
|
if context_strings:
|
|
return "\n\n".join(context_strings)
|
|
else:
|
|
return "No relevant events found."
|
|
|
|
|
|
def flatten(xss):
|
|
return [x for xs in xss for x in xs]
|
|
|
|
|
|
GITHUB_INSTRUCTIONS = """
|
|
You are an expert on GitHub repositories. When answering questions, you **must** use the provided GitHub username to find specific information about that user's repositories, including:
|
|
|
|
* Who created the repositories
|
|
* The programming languages used
|
|
* Information found in files and README.md files within those repositories
|
|
* Provide links to each repository referenfced in your answers
|
|
|
|
**Important:** Never perform general searches for repositories. Always use the given GitHub username to find the relevant information. If a GitHub username is not provided, state that you need a username to proceed.
|
|
"""
|
|
|
|
HACKATHON_AGENT = """
|
|
You are an AI Agent Hackathon Strategist specializing in recommending winning project ideas.
|
|
|
|
Your task:
|
|
1. Analyze the GitHub activity of users to understand their technical skills
|
|
2. Suggest creative AI Agent projects tailored to their expertise.
|
|
3. Focus on projects that align with Microsoft's AI Agent Hackathon prize categories
|
|
|
|
When making recommendations:
|
|
- Base your ideas strictly on the user's GitHub repositories, languages, and tools
|
|
- Give suggestions on tools, languages and frameworks to use to build it.
|
|
- Provide detailed project descriptions including architecture and implementation approach
|
|
- Explain why the project has potential to win in specific prize categories
|
|
- Highlight technical feasibility given the user's demonstrated skills by referencing the specific repositories or languages used.
|
|
|
|
Formatting your response:
|
|
- Provide a clear and structured response that includes:
|
|
- Suggested Project Name
|
|
- Project Description
|
|
- Potential languages and tools to use
|
|
- Link to each relevant GitHub repository you based your recommendation on
|
|
|
|
Hackathon prize categories:
|
|
- Best Overall Agent ($20,000)
|
|
- Best Agent in Python ($5,000)
|
|
- Best Agent in C# ($5,000)
|
|
- Best Agent in Java ($5,000)
|
|
- Best Agent in JavaScript/TypeScript ($5,000)
|
|
- Best Copilot Agent using Microsoft Copilot Studio or Microsoft 365 Agents SDK ($5,000)
|
|
- Best Microsoft Foundry Agent Service Usage ($5,000)
|
|
|
|
"""
|
|
|
|
EVENTS_AGENT = """
|
|
You are an Event Recommendation Agent specializing in suggesting relevant tech events.
|
|
|
|
Your task:
|
|
1. Review the project idea recommended by the Hackathon Agent
|
|
2. Use the search_events function to find relevant events based on the technologies mentioned.
|
|
3. NEVER suggest and event that the where there is not a relevant technology that the user has used.
|
|
3. ONLY recommend events that were returned by the search_events functionf
|
|
|
|
When making recommendations:
|
|
- IMPORTANT: You must first call the search_events function with appropriate technology keywords from the project
|
|
- Only recommend events that were explicitly returned by the search_events function
|
|
- Do not make up or suggest events that weren't in the search results
|
|
- Construct search queries using specific technologies mentioned (e.g., "Python AI workshop" or "JavaScript hackathon")
|
|
- Try multiple search queries if needed to find the most relevant events
|
|
|
|
|
|
For each recommended event:
|
|
- Only include events found in the search_events results
|
|
- Explain the direct connection between the event and the specific project requirements
|
|
- Highlight relevant workshops, sessions, or networking opportunities
|
|
|
|
Formatting your response:
|
|
- Start with "Based on the hackathon project idea, here are relevant events that I found:"
|
|
- Only list events that were returned by the search_events function
|
|
- For each event, include the exact event details as returned by search_events
|
|
- Explain specifically how each event relates to the project technologies
|
|
|
|
If no relevant events are found, acknowledge this and suggest trying different search terms instead of making up events.
|
|
"""
|
|
|
|
|
|
@cl.on_mcp_connect
|
|
async def on_mcp(connection, session: ClientSession):
|
|
logger.info(f"MCP Connection established: {connection.name}")
|
|
result = await session.list_tools()
|
|
tools = [{
|
|
"name": t.name,
|
|
"description": t.description,
|
|
"input_schema": t.inputSchema,
|
|
} for t in result.tools]
|
|
|
|
mcp_tools = cl.user_session.get("mcp_tools", {})
|
|
mcp_tools[connection.name] = tools
|
|
cl.user_session.set("mcp_tools", mcp_tools)
|
|
|
|
# Log available tools
|
|
print(f"Available MCP tools for {connection.name}:")
|
|
for t in tools:
|
|
print(f" - {t['name']}: {t['description']}")
|
|
|
|
@cl.step(type="tool")
|
|
async def call_tool(tool_use):
|
|
tool_name = tool_use.name
|
|
tool_input = tool_use.input
|
|
|
|
current_step = cl.context.current_step
|
|
current_step.name = tool_name
|
|
|
|
# Identify which mcp is used
|
|
mcp_tools = cl.user_session.get("mcp_tools", {})
|
|
mcp_name = None
|
|
|
|
for connection_name, tools in mcp_tools.items():
|
|
if any(t.get("name") == tool_name for t in tools):
|
|
mcp_name = connection_name
|
|
break
|
|
|
|
if not mcp_name:
|
|
current_step.output = json.dumps(
|
|
{"error": f"Tool {tool_name} not found in any MCP connection"})
|
|
return current_step.output
|
|
|
|
mcp_session, _ = cl.context.session.mcp_sessions.get(mcp_name)
|
|
|
|
if not mcp_session:
|
|
current_step.output = json.dumps(
|
|
{"error": f"MCP {mcp_name} not found in any MCP connection"})
|
|
return current_step.output
|
|
|
|
try:
|
|
current_step.output = await mcp_session.call_tool(tool_name, tool_input)
|
|
except Exception as e:
|
|
current_step.output = json.dumps({"error": str(e)})
|
|
|
|
return current_step.output
|
|
|
|
|
|
@cl.on_chat_start
|
|
async def on_chat_start():
|
|
|
|
# Create the Microsoft Foundry Agent Service provider
|
|
provider = FoundryChatClient(
|
|
project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
|
|
model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
|
|
credential=AzureCliCredential(),
|
|
)
|
|
|
|
# Create agents using MAF
|
|
github_agent = provider.as_agent(
|
|
name="GithubAgent",
|
|
instructions=GITHUB_INSTRUCTIONS,
|
|
)
|
|
|
|
hackathon_agent = provider.as_agent(
|
|
name="HackathonAgent",
|
|
instructions=HACKATHON_AGENT,
|
|
)
|
|
|
|
events_agent = provider.as_agent(
|
|
name="EventsAgent",
|
|
instructions=EVENTS_AGENT,
|
|
)
|
|
|
|
# Build a sequential workflow: GitHub → Hackathon → Events
|
|
workflow = WorkflowBuilder(start_executor=github_agent) \
|
|
.add_edge(github_agent, hackathon_agent) \
|
|
.add_edge(hackathon_agent, events_agent) \
|
|
.build()
|
|
|
|
# Store in user session
|
|
cl.user_session.set("provider", provider)
|
|
cl.user_session.set("github_agent", github_agent)
|
|
cl.user_session.set("hackathon_agent", hackathon_agent)
|
|
cl.user_session.set("events_agent", events_agent)
|
|
cl.user_session.set("workflow", workflow)
|
|
cl.user_session.set("mcp_tools", {})
|
|
cl.user_session.set("conversation_history", [])
|
|
|
|
|
|
# Add a cleanup handler for when the session ends
|
|
@cl.on_chat_end
|
|
async def on_chat_end():
|
|
pass
|
|
|
|
|
|
def route_user_input(user_input: str):
|
|
"""
|
|
Analyze user input and return a list of agent names to invoke.
|
|
Returns: list of agent names (e.g., ["GitHubAgent", "HackathonAgent", "EventsAgent"])
|
|
"""
|
|
user_input_lower = user_input.lower()
|
|
agents = []
|
|
# Example patterns (expand as needed)
|
|
if re.search(r"github|repo|repository|commit|pull request", user_input_lower):
|
|
agents.append("GitHubAgent")
|
|
if re.search(r"hackathon|project idea|competition|challenge|win", user_input_lower):
|
|
agents.append("HackathonAgent")
|
|
if re.search(r"event|conference|meetup|workshop|webinar", user_input_lower):
|
|
agents.append("EventsAgent")
|
|
if not agents:
|
|
agents = ["GitHubAgent", "HackathonAgent", "EventsAgent"]
|
|
return agents
|
|
|
|
|
|
@cl.on_message
|
|
async def on_message(message: cl.Message):
|
|
workflow = cl.user_session.get("workflow")
|
|
github_agent = cl.user_session.get("github_agent")
|
|
hackathon_agent = cl.user_session.get("hackathon_agent")
|
|
events_agent = cl.user_session.get("events_agent")
|
|
conversation_history = cl.user_session.get("conversation_history", [])
|
|
|
|
user_input = message.content
|
|
agent_names = route_user_input(user_input)
|
|
conversation_history.append({"role": "user", "content": user_input})
|
|
|
|
# If more than one agent is selected, use the workflow
|
|
if len(agent_names) > 1:
|
|
answer = cl.Message(content="Processing your request using: {}...\n\n".format(", ".join(agent_names)))
|
|
await answer.send()
|
|
agent_responses = []
|
|
try:
|
|
events = workflow.run(user_input, stream=True, tools=[search_events])
|
|
last_author = None
|
|
async for event in events:
|
|
if event.type == "output" and isinstance(event.data, AgentResponseUpdate):
|
|
update = event.data
|
|
author = update.author_name or "Agent"
|
|
if author != last_author:
|
|
if last_author is not None:
|
|
await answer.stream_token("\n\n")
|
|
await answer.stream_token(f"**{author}**: ")
|
|
last_author = author
|
|
if update.text:
|
|
await answer.stream_token(update.text)
|
|
agent_responses.append(f"**{author}**: {update.text}")
|
|
full_response = "".join(agent_responses) if agent_responses else answer.content
|
|
conversation_history.append({"role": "assistant", "content": full_response})
|
|
cl.user_session.set("conversation_history", conversation_history)
|
|
answer.content = full_response
|
|
await answer.update()
|
|
except Exception as e:
|
|
await answer.stream_token(f"\n\n❌ Error: {str(e)}\n\n")
|
|
conversation_history.append({"role": "assistant", "content": f"Error: {str(e)}"})
|
|
cl.user_session.set("conversation_history", conversation_history)
|
|
answer.content += f"\n\n❌ Error: {str(e)}"
|
|
await answer.update()
|
|
else:
|
|
# Single agent: route to the appropriate agent
|
|
agent_name = agent_names[0]
|
|
agent_map = {
|
|
"GitHubAgent": github_agent,
|
|
"HackathonAgent": hackathon_agent,
|
|
"EventsAgent": events_agent,
|
|
}
|
|
agent = agent_map.get(agent_name, github_agent)
|
|
|
|
answer = cl.Message(content=f"Processing your request using {agent_name}...\n\n")
|
|
await answer.send()
|
|
try:
|
|
tools_for_agent = [search_events] if agent_name == "EventsAgent" else []
|
|
response = await agent.run(user_input, tools=tools_for_agent)
|
|
answer.content = str(response)
|
|
conversation_history.append({"role": "assistant", "content": answer.content})
|
|
cl.user_session.set("conversation_history", conversation_history)
|
|
await answer.update()
|
|
except Exception as e:
|
|
await answer.stream_token(f"\n\n❌ Error: {str(e)}\n\n")
|
|
conversation_history.append({"role": "assistant", "content": f"Error: {str(e)}"})
|
|
cl.user_session.set("conversation_history", conversation_history)
|
|
answer.content += f"\n\n❌ Error: {str(e)}"
|
|
await answer.update()
|