Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions packages/python-packages/apiview-copilot/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -513,7 +513,7 @@ async def chat():
print(f"Error: {e}")
else:
# Local mode: use async agent as before
async with get_main_agent() as agent:
with get_main_agent() as (client, agent_id):
while True:
try:
user_input = await async_input(f"{BOLD_GREEN}You:{RESET} ")
Expand All @@ -535,7 +535,11 @@ async def chat():
break
try:
response, thread_id_out, messages = await invoke_agent(
agent=agent, user_input=user_input, thread_id=current_thread_id, messages=messages
client=client,
agent_id=agent_id,
user_input=user_input,
thread_id=current_thread_id,
messages=messages,
)
print(f"{BOLD_BLUE}Agent:{RESET} {response}\n")
current_thread_id = thread_id_out
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ WEBAPP_NAME: apiview-copilot
FOUNDRY_ACCOUNT_NAME: azsdk-engsys-ai
FOUNDRY_PROJECT_NAME: azsdk-engsys-ai
FOUNDRY_KERNEL_MODEL: gpt-5
FOUNDRY_API_VERSION: 2025-05-15-preview
FOUNDRY_API_VERSION: 2025-11-15-preview
AI_RG: azsdk-engsys-ai
OPENAI_NAME: azsdk-engsys-openai
OPENAI_EMBEDDING_MODEL: text-embedding-3-large
Expand Down
199 changes: 96 additions & 103 deletions packages/python-packages/apiview-copilot/src/agent/_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,124 +8,117 @@
Module for managing APIView Copilot agents.
"""

import logging
from contextlib import AsyncExitStack, asynccontextmanager
from datetime import timedelta

from azure.identity.aio import DefaultAzureCredential
from semantic_kernel import Kernel

# pylint: disable=no-name-in-module
from semantic_kernel.agents import (
AzureAIAgent,
AzureAIAgentSettings,
AzureAIAgentThread,
RunPollingOptions,
)
from semantic_kernel.connectors.ai.open_ai import AzureChatCompletion
from src._settings import SettingsManager

from .plugins import (
ApiReviewPlugin,
SearchPlugin,
UtilityPlugin,
get_create_agent,
get_delete_agent,
get_link_agent,
get_retrieve_agent,
)

import asyncio
from contextlib import contextmanager
from typing import Optional

def create_kernel() -> Kernel:
"""Creates a Kernel instance configured for Azure OpenAI."""
settings = SettingsManager()
base_url = settings.get("OPENAI_ENDPOINT")
deployment_name = settings.get("FOUNDRY_KERNEL_MODEL")
api_key = settings.get("OPENAI_API_KEY")
logging.info("Using Azure OpenAI at %s with deployment %s", base_url, deployment_name)
kernel = Kernel(
plugins={}, # Register your plugins here if needed
services={
"AzureChatCompletion": AzureChatCompletion(
base_url=base_url,
deployment_name=deployment_name,
api_key=api_key,
)
},
)
return kernel
from azure.ai.agents import AgentsClient
from azure.ai.agents.models import MessageRole, MessageTextContent
from src._credential import get_credential
from src._settings import SettingsManager


async def invoke_agent(*, agent, user_input, thread_id=None, messages=None):
"""Invoke an agent with the provided user input and thread ID."""
async def invoke_agent(
*,
client: AgentsClient,
agent_id: str,
user_input: str,
thread_id: Optional[str] = None,
messages: Optional[list[str]] = None,
) -> tuple[str, str, list[str]]:
"""
Invoke an agent with the provided user input and thread ID.
Returns: (response_text, thread_id, messages)
"""
messages = messages or []
# Only append user_input if not already the last message
if not messages or messages[-1] != user_input:
messages.append(user_input)
# Only use thread_id if it is a valid Azure thread id (starts with 'thread')
if thread_id and isinstance(thread_id, str) and thread_id.startswith("thread"):
thread = AzureAIAgentThread(client=agent.client, thread_id=thread_id)
else:
thread = AzureAIAgentThread(client=agent.client)
response = await agent.get_response(messages=messages, thread=thread)
thread_id_out = getattr(thread, "id", None) or thread_id
return str(response), thread_id_out, messages


def _get_agent_settings() -> AzureAIAgentSettings:
"""Retrieve the Azure AI Agent settings from the configuration."""
settings = SettingsManager()
return AzureAIAgentSettings(
endpoint=settings.get("FOUNDRY_ENDPOINT"),
model_deployment_name=settings.get("FOUNDRY_KERNEL_MODEL"),
api_version=settings.get("FOUNDRY_API_VERSION"),
)

# 1) Ensure a thread exists
if not thread_id:
thread_obj = await asyncio.to_thread(client.threads.create)
thread_id = thread_obj.id

# 2) Add user message to the thread
await asyncio.to_thread(client.messages.create, thread_id=thread_id, role="user", content=user_input)

# 3) Process a run (polls until terminal state; executes tools if auto-enabled)
await asyncio.to_thread(client.runs.create_and_process, thread_id=thread_id, agent_id=agent_id)

@asynccontextmanager
async def get_main_agent():
# 4) Collect messages and extract the latest assistant text
all_messages = await asyncio.to_thread(client.messages.list, thread_id=thread_id)

def extract_text(obj):
"""Recursively extract all text values from nested lists/dicts and MessageTextContent."""
if isinstance(obj, MessageTextContent):
return obj.text.value
elif isinstance(obj, list):
return " ".join(extract_text(item) for item in obj)
elif isinstance(obj, str):
return obj
else:
return str(obj)

response_text = ""
for m in reversed(list(all_messages)):
role = getattr(m, "role", None)
if role == MessageRole.AGENT.value or role == "assistant":
parts = getattr(m, "content", None)
response_text = extract_text(parts)
break
return response_text, thread_id, messages


@contextmanager
def get_main_agent():
"""Create and yield the main APIView Copilot agent."""
kernel = create_kernel()
ai_agent_settings = _get_agent_settings()
settings = SettingsManager()
endpoint = settings.get("FOUNDRY_ENDPOINT")
model_deployment_name = settings.get("FOUNDRY_KERNEL_MODEL")

ai_instructions = """
Your job is to receive a request from the user, determine their intent, and pass the request to the
appropriate agent or agents for processing. You will then return the response from that agent to the user.
If there's no agent that can handle the request, you will respond with a message indicating that you cannot
process the request. You will also handle any errors that occur during the processing of the request and return
an appropriate error message to the user.
"""
credential = get_credential()
client = AgentsClient(endpoint=endpoint, credential=credential)

# delete_agent = await stack.enter_async_context(get_delete_agent())
# create_agent = await stack.enter_async_context(get_create_agent())
# retrieve_agent = await stack.enter_async_context(get_retrieve_agent())
# link_agent = await stack.enter_async_context(get_link_agent())

tools = []
agent = client.create_agent(
name="ArchAgentMainAgent",
description="An agent that processed requests and passes work to other agents.",
model=model_deployment_name,
instructions=ai_instructions,
tools=tools,
)
# enable all tools by default
client.enable_auto_function_calls(tools=tools)

async with AsyncExitStack() as stack:
credentials = await stack.enter_async_context(DefaultAzureCredential())
client = await stack.enter_async_context(
AzureAIAgent.create_client(
credential=credentials, endpoint=ai_agent_settings.endpoint, api_version=ai_agent_settings.api_version
)
)
delete_agent = await stack.enter_async_context(get_delete_agent())
create_agent = await stack.enter_async_context(get_create_agent())
retrieve_agent = await stack.enter_async_context(get_retrieve_agent())
link_agent = await stack.enter_async_context(get_link_agent())

agent_definition = await client.agents.create_agent(
name="ArchAgentMainAgent",
description="An agent that processed requests and passes work to other agents.",
model=ai_agent_settings.model_deployment_name,
instructions=ai_instructions,
)
agent = AzureAIAgent(
client=client,
definition=agent_definition,
plugins=[
SearchPlugin(),
UtilityPlugin(),
ApiReviewPlugin(),
delete_agent,
create_agent,
retrieve_agent,
link_agent,
],
polling_options=RunPollingOptions(run_polling_interval=timedelta(seconds=1)),
kernel=kernel,
)
yield agent
# agent = AzureAIAgent(
# client=client,
# definition=agent_definition,
# plugins=[
# SearchPlugin(),
# UtilityPlugin(),
# ApiReviewPlugin(),
# delete_agent,
# create_agent,
# retrieve_agent,
# link_agent,
# ],
# polling_options=RunPollingOptions(run_polling_interval=timedelta(seconds=1)),
# kernel=kernel,
# )
try:
yield client, agent.id
finally:
client.delete_agent(agent.id)
Loading
Loading