跳到正文

11 · app

返回:Agent 协议:MCP、A2A、NLWeb · 不可变原始文件

原课程脚本 · 未联网执行

保留原始框架与完整代码。依赖、变量、入口以脚本及其相邻 README 为准;本页为代码导读,未声称所有外部服务均已验证。

配置加载:从本地环境读取端点与部署名;缺少变量时先修复配置,不要把密钥写进代码。

工具定义:类型注解和文档字符串描述输入、用途;模型产生调用请求,框架在应用进程中执行函数。检查是否需要人工批准。

模型连接:project_endpoint 是项目地址,model 是实际部署名称;credential 提供访问身份。客户端创建本身不证明已经部署服务端 Agent。

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()

基于 Microsoft AI Agents for Beginners · 非官方中文学习版

100%