From 105695726347a5245e280093044db95e2d75a5ff Mon Sep 17 00:00:00 2001 From: Travis Prescott Date: Thu, 20 Nov 2025 13:58:19 -0800 Subject: [PATCH 1/6] Refactor from SemanticKernel plugins to Azure AI Foundry Tools. --- .../python-packages/apiview-copilot/cli.py | 8 +- .../scripts/infra/variables.yaml | 2 +- .../apiview-copilot/src/agent/_agent.py | 199 ++++++++------- .../src/agent/plugins/_database_plugin.py | 230 ++++++++---------- 4 files changed, 210 insertions(+), 229 deletions(-) diff --git a/packages/python-packages/apiview-copilot/cli.py b/packages/python-packages/apiview-copilot/cli.py index 70d71576f9b..99652555ca6 100644 --- a/packages/python-packages/apiview-copilot/cli.py +++ b/packages/python-packages/apiview-copilot/cli.py @@ -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} ") @@ -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 diff --git a/packages/python-packages/apiview-copilot/scripts/infra/variables.yaml b/packages/python-packages/apiview-copilot/scripts/infra/variables.yaml index 01d03cdcc02..6ade4609ef4 100644 --- a/packages/python-packages/apiview-copilot/scripts/infra/variables.yaml +++ b/packages/python-packages/apiview-copilot/scripts/infra/variables.yaml @@ -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 diff --git a/packages/python-packages/apiview-copilot/src/agent/_agent.py b/packages/python-packages/apiview-copilot/src/agent/_agent.py index a2091aa537e..10ecd160d85 100644 --- a/packages/python-packages/apiview-copilot/src/agent/_agent.py +++ b/packages/python-packages/apiview-copilot/src/agent/_agent.py @@ -8,85 +8,75 @@ 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. @@ -94,38 +84,41 @@ async def get_main_agent(): 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) diff --git a/packages/python-packages/apiview-copilot/src/agent/plugins/_database_plugin.py b/packages/python-packages/apiview-copilot/src/agent/plugins/_database_plugin.py index fb4c7ad3b7f..2d6caadfcd7 100644 --- a/packages/python-packages/apiview-copilot/src/agent/plugins/_database_plugin.py +++ b/packages/python-packages/apiview-copilot/src/agent/plugins/_database_plugin.py @@ -4,29 +4,34 @@ # license information. # -------------------------------------------------------------------------- -"""Plugin for database operations.""" +"""Tool for database operations.""" -from contextlib import AsyncExitStack, asynccontextmanager +from contextlib import asynccontextmanager, contextmanager from datetime import timedelta from typing import Optional +from azure.ai.agents import AgentsClient from azure.cosmos.exceptions import CosmosResourceNotFoundError -from azure.identity import DefaultAzureCredential # pylint: disable=no-name-in-module -from semantic_kernel.agents import AzureAIAgent, RunPollingOptions -from semantic_kernel.functions import kernel_function +from src._credential import get_credential from src._database_manager import ContainerNames, DatabaseManager from src._models import Example, Guideline, Memory +from src._settings import SettingsManager +from src.agent._agent import kernel_function -@asynccontextmanager -async def get_delete_agent(): +@contextmanager +def get_delete_agent(): """Agent for handling database delete requests.""" # pylint: disable=import-outside-toplevel - from src.agent._agent import _get_agent_settings, create_kernel + from src.agent._agent import _get_agent_settings + + credential = get_credential() + settings = SettingsManager() + endpoint = settings.get("FOUNDRY_ENDPOINT") + model_deployment_name = settings.get("FOUNDRY_KERNEL_MODEL") - ai_agent_settings = _get_agent_settings() ai_instructions = f""" You are an agent that processes database delete requests for guidelines, examples, memories or review jobs. @@ -41,40 +46,31 @@ async def get_delete_agent(): - Never delete guidelines. You MUST deny any request to delete a guideline. - Never delete review jobs. You MUST deny any request to delete a review job. Review jobs are deleted programmatically ONLY. """ - async with AsyncExitStack() as stack: - credentials = DefaultAzureCredential() - client = await stack.enter_async_context( - AzureAIAgent.create_client( - credential=credentials, endpoint=ai_agent_settings.endpoint, api_version=ai_agent_settings.api_version - ) - ) - agent_definition = await client.agents.create_agent( - name="DeleteAgent", - description="Handles database delete requests.", - model=ai_agent_settings.model_deployment_name, - instructions=ai_instructions, - ) - agent = AzureAIAgent( - client=client, - definition=agent_definition, - plugins=[DatabaseDeletePlugin()], - polling_options=RunPollingOptions(run_polling_interval=timedelta(seconds=1)), - kernel=create_kernel(), - ) - yield agent - - -@asynccontextmanager + client = AgentsClient(endpoint=endpoint.endpoint, credential=credential) + agent = client.create_agent( + name="DeleteAgent", + description="Handles database delete requests.", + model=model_deployment_name, + instructions=ai_instructions, + tools=[DatabaseDeleteTool()], + ) + try: + yield client, agent.id + finally: + client.delete_agent(agent.id) + + +@contextmanager async def get_create_agent(): """Agent for handling database create requests.""" - # pylint: disable=import-outside-toplevel from src._apiview_reviewer import SUPPORTED_LANGUAGES - from src.agent._agent import ( - _get_agent_settings, - create_kernel, - ) + from src.agent._agent import _get_agent_settings + + credential = get_credential() + settings = SettingsManager() + endpoint = settings.get("FOUNDRY_ENDPOINT") + model_deployment_name = settings.get("FOUNDRY_KERNEL_MODEL") - ai_agent_settings = _get_agent_settings() guideline_schema = Guideline.model_json_schema() example_schema = Example.model_json_schema() memory_schema = Memory.model_json_schema() @@ -147,63 +143,57 @@ async def get_create_agent(): 4. Report all linking actions in your response. - If you do not know what to link, ask the user. """ - credentials = DefaultAzureCredential() - async with AzureAIAgent.create_client( - credential=credentials, endpoint=ai_agent_settings.endpoint, api_version=ai_agent_settings.api_version - ) as client: - agent_definition = await client.agents.create_agent( - name="CreateAgent", - description="Handles database create or insert requests.", - model=ai_agent_settings.model_deployment_name, - instructions=ai_instructions, - ) - agent = AzureAIAgent( - client=client, - definition=agent_definition, - plugins=[DatabaseCreatePlugin()], - polling_options=RunPollingOptions(run_polling_interval=timedelta(seconds=1)), - kernel=create_kernel(), - ) - yield agent - - -@asynccontextmanager + client = AgentsClient(endpoint=endpoint, credential=credential) + agent = client.create_agent( + name="CreateAgent", + description="Handles database create or insert requests.", + model=model_deployment_name, + instructions=ai_instructions, + tools=[DatabaseCreateTool()], + ) + try: + yield client, agent.id + finally: + client.delete_agent(agent.id) + + +@contextmanager async def get_retrieve_agent(): """Agent for handling database retrieval requests.""" - # pylint: disable=import-outside-toplevel - from src.agent._agent import _get_agent_settings, create_kernel + from src.agent._agent import _get_agent_settings + + credential = get_credential() + settings = SettingsManager() + endpoint = settings.get("FOUNDRY_ENDPOINT") + model_deployment_name = settings.get("FOUNDRY_KERNEL_MODEL") - ai_agent_settings = _get_agent_settings() ai_instructions = """ You are an agent that processes database get or retrieval requests for guidelines, examples, memories, or review jobs. """ - credentials = DefaultAzureCredential() - async with AzureAIAgent.create_client( - credential=credentials, endpoint=ai_agent_settings.endpoint, api_version=ai_agent_settings.api_version - ) as client: - agent_definition = await client.agents.create_agent( - name="RetrieveAgent", - description="Handles database retrieval requests.", - model=ai_agent_settings.model_deployment_name, - instructions=ai_instructions, - ) - agent = AzureAIAgent( - client=client, - definition=agent_definition, - plugins=[DatabaseRetrievePlugin()], - polling_options=RunPollingOptions(run_polling_interval=timedelta(seconds=1)), - kernel=create_kernel(), - ) - yield agent - - -@asynccontextmanager + client = AgentsClient(endpoint=endpoint, credential=credential) + agent = client.create_agent( + name="RetrieveAgent", + description="Handles database retrieval requests.", + model=model_deployment_name, + instructions=ai_instructions, + tools=[DatabaseRetrieveTool()], + ) + try: + yield client, agent.id + finally: + client.delete_agent(agent.id) + + +@contextmanager async def get_link_agent(): """Agent for handling database linking and unlinking requests.""" - # pylint: disable=import-outside-toplevel - from src.agent._agent import _get_agent_settings, create_kernel + from src.agent._agent import _get_agent_settings + + credential = get_credential() + settings = SettingsManager() + endpoint = settings.get("FOUNDRY_ENDPOINT") + model_deployment_name = settings.get("FOUNDRY_KERNEL_MODEL") - ai_agent_settings = _get_agent_settings() ai_instructions = f""" You are an agent that processes database requests to link or unlink guidelines, examples, memories or review jobs. @@ -215,39 +205,33 @@ async def get_link_agent(): - target_id can refer to Guidelines, Examples or Memories. - The only valid container names are: {', '.join([c.value for c in ContainerNames])} - Guidelines have the following link fields: - - related_guidelines - - related_examples - - related_memories + - related_guidelines + - related_examples + - related_memories - Memories have the following link fields: - - related_guidelines - - related_examples - - related_memories + - related_guidelines + - related_examples + - related_memories - Examples have the following link fields: - - guideline_ids - - memory_ids + - guideline_ids + - memory_ids """ - credentials = DefaultAzureCredential() - async with AzureAIAgent.create_client( - credential=credentials, endpoint=ai_agent_settings.endpoint, api_version=ai_agent_settings.api_version - ) as client: - agent_definition = await client.agents.create_agent( - name="LinkUnlinkAgent", - description="Handles database linking and unlinking requests.", - model=ai_agent_settings.model_deployment_name, - instructions=ai_instructions, - ) - agent = AzureAIAgent( - client=client, - definition=agent_definition, - plugins=[DatabaseLinkUnlinkPlugin()], - polling_options=RunPollingOptions(run_polling_interval=timedelta(seconds=1)), - kernel=create_kernel(), - ) - yield agent - - -class DatabaseCreatePlugin: - """Plugin for creating database items.""" + client = AgentsClient(endpoint=endpoint, credential=credential) + agent = client.create_agent( + name="LinkUnlinkAgent", + description="Handles database linking and unlinking requests.", + model=model_deployment_name, + instructions=ai_instructions, + tools=[DatabaseLinkUnlinkTool()], + ) + try: + yield client, agent.id + finally: + client.delete_agent(agent.id) + + +class DatabaseCreateTool: + """Tool for creating database items.""" @kernel_function(description="Create a new Guideline in the database.") async def create_guideline( @@ -349,8 +333,8 @@ async def create_example( return db.examples.create(id, data=data) -class DatabaseRetrievePlugin: - """Plugin for retrieving database items.""" +class DatabaseRetrieveTool: + """Tool for retrieving database items.""" @kernel_function(description="Retrieve a memory from the database by its ID.") async def get_memory(self, memory_id: str): @@ -404,8 +388,8 @@ async def get_guideline_schema(self): return Guideline.model_json_schema() -class DatabaseLinkUnlinkPlugin: - """Plugin for linking and unlinking database items.""" +class DatabaseLinkUnlinkTool: + """Tool for linking and unlinking database items.""" @kernel_function( description=""" @@ -554,8 +538,8 @@ async def unlink_items( } -class DatabaseDeletePlugin: - """Plugin for deleting database items.""" +class DatabaseDeleteTool: + """Tool for deleting database items.""" @kernel_function(description="Delete a Guideline from the database by its ID.") async def delete_guideline(self, id: str): From 06b1b3f80c68f7eaa5e6abfbecd1f2f29e83e3b1 Mon Sep 17 00:00:00 2001 From: Travis Prescott Date: Fri, 21 Nov 2025 11:20:59 -0800 Subject: [PATCH 2/6] Rename plugins to tools. Get rid of SK references. --- .../apiview-copilot/src/agent/_agent.py | 7 ++-- .../src/agent/{plugins => tools}/__init__.py | 14 ++++---- .../_api_review_tools.py} | 15 +++----- .../apiview-copilot/src/agent/tools/_base.py | 18 ++++++++++ .../_database_tools.py} | 36 ++++--------------- .../_search_tools.py} | 24 ++++--------- .../_utility_tools.py} | 10 ++---- 7 files changed, 49 insertions(+), 75 deletions(-) rename packages/python-packages/apiview-copilot/src/agent/{plugins => tools}/__init__.py (71%) rename packages/python-packages/apiview-copilot/src/agent/{plugins/_api_review_plugin.py => tools/_api_review_tools.py} (80%) create mode 100644 packages/python-packages/apiview-copilot/src/agent/tools/_base.py rename packages/python-packages/apiview-copilot/src/agent/{plugins/_database_plugin.py => tools/_database_tools.py} (92%) rename packages/python-packages/apiview-copilot/src/agent/{plugins/_search_plugin.py => tools/_search_tools.py} (72%) rename packages/python-packages/apiview-copilot/src/agent/{plugins/_utility_plugin.py => tools/_utility_tools.py} (90%) diff --git a/packages/python-packages/apiview-copilot/src/agent/_agent.py b/packages/python-packages/apiview-copilot/src/agent/_agent.py index 10ecd160d85..2acb87a0433 100644 --- a/packages/python-packages/apiview-copilot/src/agent/_agent.py +++ b/packages/python-packages/apiview-copilot/src/agent/_agent.py @@ -16,6 +16,7 @@ from azure.ai.agents.models import MessageRole, MessageTextContent from src._credential import get_credential from src._settings import SettingsManager +from src.agent.tools._search_tools import SearchTools async def invoke_agent( @@ -92,10 +93,10 @@ def get_main_agent(): # retrieve_agent = await stack.enter_async_context(get_retrieve_agent()) # link_agent = await stack.enter_async_context(get_link_agent()) - tools = [] + tools = SearchTools().all_tools() agent = client.create_agent( - name="ArchAgentMainAgent", - description="An agent that processed requests and passes work to other agents.", + name="APIView Copilot Main Agent", + description="An agent that processes requests and passes work to other agents.", model=model_deployment_name, instructions=ai_instructions, tools=tools, diff --git a/packages/python-packages/apiview-copilot/src/agent/plugins/__init__.py b/packages/python-packages/apiview-copilot/src/agent/tools/__init__.py similarity index 71% rename from packages/python-packages/apiview-copilot/src/agent/plugins/__init__.py rename to packages/python-packages/apiview-copilot/src/agent/tools/__init__.py index ea224a1e6df..3109713db63 100644 --- a/packages/python-packages/apiview-copilot/src/agent/plugins/__init__.py +++ b/packages/python-packages/apiview-copilot/src/agent/tools/__init__.py @@ -8,20 +8,20 @@ This module initializes the plugins for the agent. """ -from ._api_review_plugin import ApiReviewPlugin -from ._database_plugin import ( +from ._api_review_tools import ApiReviewTool +from ._database_tools import ( get_create_agent, get_delete_agent, get_link_agent, get_retrieve_agent, ) -from ._search_plugin import SearchPlugin -from ._utility_plugin import UtilityPlugin +from ._search_tools import SearchTools +from ._utility_tools import UtilityTools __all__ = [ - "ApiReviewPlugin", - "SearchPlugin", - "UtilityPlugin", + "ApiReviewTool", + "SearchTools", + "UtilityTools", "get_create_agent", "get_delete_agent", "get_retrieve_agent", diff --git a/packages/python-packages/apiview-copilot/src/agent/plugins/_api_review_plugin.py b/packages/python-packages/apiview-copilot/src/agent/tools/_api_review_tools.py similarity index 80% rename from packages/python-packages/apiview-copilot/src/agent/plugins/_api_review_plugin.py rename to packages/python-packages/apiview-copilot/src/agent/tools/_api_review_tools.py index 507f6071548..d06953f19d9 100644 --- a/packages/python-packages/apiview-copilot/src/agent/plugins/_api_review_plugin.py +++ b/packages/python-packages/apiview-copilot/src/agent/tools/_api_review_tools.py @@ -5,7 +5,7 @@ # -------------------------------------------------------------------------- """ -Plugin for performing API reviews using the ApiViewReview class. +Tools for performing API reviews using the ApiViewReview class. """ import json @@ -13,12 +13,12 @@ from semantic_kernel.functions import kernel_function from src._apiview import ApiViewClient from src._apiview_reviewer import ApiViewReview +from src.agent.tools._base import Tool -class ApiReviewPlugin: - """Plugin for API review operations.""" +class ApiReviewTool(Tool): + """Tools for API review operations.""" - @kernel_function(description="Perform an API review on a single API.") async def review_api(self, *, language: str, target: str): """ Perform an API review on a single API. @@ -30,7 +30,6 @@ async def review_api(self, *, language: str, target: str): results = reviewer.run() return json.dumps(results.model_dump(), indent=2) - @kernel_function(description="Perform an API review on a diff between two APIs.") async def review_api_diff(self, *, language: str, target: str, base: str): """ Perform an API review on a diff between two APIs. @@ -43,7 +42,6 @@ async def review_api_diff(self, *, language: str, target: str, base: str): results = reviewer.run() return json.dumps(results.model_dump(), indent=2) - @kernel_function(description="Get the text of an API revision.") async def get_apiview_revision(self, *, revision_id: str) -> str: """ Get the text of an API revision. @@ -53,7 +51,6 @@ async def get_apiview_revision(self, *, revision_id: str) -> str: client = ApiViewClient() return await client.get_revision_text(revision_id=revision_id) - @kernel_function(description="Get the text of an API revision by review ID and label.") async def get_apiview_revision_by_review(self, *, review_id: str, label: str = "Latest") -> str: """ Get the text of an API revision by review ID and label. @@ -64,7 +61,6 @@ async def get_apiview_revision_by_review(self, *, review_id: str, label: str = " client = ApiViewClient() return await client.get_revision_text(review_id=review_id, label=label) - @kernel_function(description="Get the outline for a given API revision") async def get_apiview_revision_outline(self, *, revision_id: str) -> str: """ Get the outline for a given API revision. @@ -74,10 +70,9 @@ async def get_apiview_revision_outline(self, *, revision_id: str) -> str: client = ApiViewClient() return await client.get_revision_outline(revision_id=revision_id) - @kernel_function(description="Retrieves any existing comments for a given API revision") async def get_apiview_revision_comments(self, *, revision_id: str) -> str: """ - Get the comments visible for a given API revision. + Retrieves any existing comments for a given API revision Args: revision_id (str): The ID of the API revision to retrieve comments for. """ diff --git a/packages/python-packages/apiview-copilot/src/agent/tools/_base.py b/packages/python-packages/apiview-copilot/src/agent/tools/_base.py new file mode 100644 index 00000000000..c2b2b1f3ce5 --- /dev/null +++ b/packages/python-packages/apiview-copilot/src/agent/tools/_base.py @@ -0,0 +1,18 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- + + +class Tool: + """Base class for agent tools. Provides automatic tool discovery.""" + + def all_tools(self): + """Return all non-internal callable methods for agent registration.""" + tools = [ + getattr(self, name) + for name in dir(self) + if not name.startswith("_") and callable(getattr(self, name)) and name != "all_tools" + ] + return tools diff --git a/packages/python-packages/apiview-copilot/src/agent/plugins/_database_plugin.py b/packages/python-packages/apiview-copilot/src/agent/tools/_database_tools.py similarity index 92% rename from packages/python-packages/apiview-copilot/src/agent/plugins/_database_plugin.py rename to packages/python-packages/apiview-copilot/src/agent/tools/_database_tools.py index 2d6caadfcd7..0e29db65fd0 100644 --- a/packages/python-packages/apiview-copilot/src/agent/plugins/_database_plugin.py +++ b/packages/python-packages/apiview-copilot/src/agent/tools/_database_tools.py @@ -6,8 +6,7 @@ """Tool for database operations.""" -from contextlib import asynccontextmanager, contextmanager -from datetime import timedelta +from contextlib import contextmanager from typing import Optional from azure.ai.agents import AgentsClient @@ -18,7 +17,7 @@ from src._database_manager import ContainerNames, DatabaseManager from src._models import Example, Guideline, Memory from src._settings import SettingsManager -from src.agent._agent import kernel_function +from src.agent.tools._base import Tool @contextmanager @@ -230,10 +229,9 @@ async def get_link_agent(): client.delete_agent(agent.id) -class DatabaseCreateTool: +class DatabaseCreateTool(Tool): """Tool for creating database items.""" - @kernel_function(description="Create a new Guideline in the database.") async def create_guideline( self, id: str, @@ -258,7 +256,6 @@ async def create_guideline( db = DatabaseManager.get_instance() return db.guidelines.create(id, data=data) - @kernel_function(description="Create a new Memory in the database.") async def create_memory( self, id: str, @@ -295,7 +292,6 @@ async def create_memory( db = DatabaseManager.get_instance() return db.memories.create(id, data=data) - @kernel_function(description="Create a new Example in the database.") async def create_example( self, title: str, @@ -333,10 +329,9 @@ async def create_example( return db.examples.create(id, data=data) -class DatabaseRetrieveTool: +class DatabaseRetrieveTool(Tool): """Tool for retrieving database items.""" - @kernel_function(description="Retrieve a memory from the database by its ID.") async def get_memory(self, memory_id: str): """ Retrieve a memory from the database by its ID. @@ -346,14 +341,12 @@ async def get_memory(self, memory_id: str): db = DatabaseManager.get_instance() return db.memories.get(memory_id) - @kernel_function(description="Retrieve the Memory schema.") async def get_memory_schema(self): """ Retrieve the Memory schema. """ return Memory.model_json_schema() - @kernel_function(description="Retrieve an example from the database by its ID.") async def get_example(self, example_id: str): """ Retrieve an example from the database by its ID. @@ -363,14 +356,12 @@ async def get_example(self, example_id: str): db = DatabaseManager.get_instance() return db.examples.get(example_id) - @kernel_function(description="Retrieve the Example schema.") async def get_example_schema(self): """ Retrieve the Example schema. """ return Example.model_json_schema() - @kernel_function(description="Retrieve a guideline from the database by its ID.") async def get_guideline(self, guideline_id: str): """ Retrieve a guideline from the database by its ID. @@ -380,7 +371,6 @@ async def get_guideline(self, guideline_id: str): db = DatabaseManager.get_instance() return db.guidelines.get(guideline_id) - @kernel_function(description="Retrieve the Guideline schema.") async def get_guideline_schema(self): """ Retrieve the Guideline schema. @@ -388,14 +378,9 @@ async def get_guideline_schema(self): return Guideline.model_json_schema() -class DatabaseLinkUnlinkTool: +class DatabaseLinkUnlinkTool(Tool): """Tool for linking and unlinking database items.""" - @kernel_function( - description=""" - Link one or more target items to a source item by adding their IDs to a related field in the source item. - """ - ) async def link_items( self, source_id: str, @@ -463,11 +448,6 @@ async def link_items( target_c.upsert(target_id, data=target_item) return {"status": "done", "source_id": source_id, "source_field": source_field, **results} - @kernel_function( - description=""" - Unlink one or more target items from a source item by removing their IDs from a related field in the source item. - """ - ) async def unlink_items( self, source_id: str, @@ -538,10 +518,9 @@ async def unlink_items( } -class DatabaseDeleteTool: +class DatabaseDeleteTool(Tool): """Tool for deleting database items.""" - @kernel_function(description="Delete a Guideline from the database by its ID.") async def delete_guideline(self, id: str): """Delete a guideline from the database by its ID.""" db = DatabaseManager.get_instance() @@ -551,7 +530,6 @@ async def delete_guideline(self, id: str): except CosmosResourceNotFoundError: return f"Guideline with id '{id}' not found." - @kernel_function(description="Delete a Memory from the database by its ID.") async def delete_memory(self, id: str): """Delete a memory from the database by its ID.""" db = DatabaseManager.get_instance() @@ -561,7 +539,6 @@ async def delete_memory(self, id: str): except CosmosResourceNotFoundError: return f"Memory with id '{id}' not found." - @kernel_function(description="Delete an Example from the database by its ID.") async def delete_example(self, id: str): """Delete an example from the database by its ID.""" db = DatabaseManager.get_instance() @@ -571,7 +548,6 @@ async def delete_example(self, id: str): except CosmosResourceNotFoundError: return f"Example with id '{id}' not found." - @kernel_function(description="Delete a Review Job from the database by its ID.") async def delete_review_job(self, id: str): """Delete a review job from the database by its ID.""" db = DatabaseManager.get_instance() diff --git a/packages/python-packages/apiview-copilot/src/agent/plugins/_search_plugin.py b/packages/python-packages/apiview-copilot/src/agent/tools/_search_tools.py similarity index 72% rename from packages/python-packages/apiview-copilot/src/agent/plugins/_search_plugin.py rename to packages/python-packages/apiview-copilot/src/agent/tools/_search_tools.py index 342e39dee7e..eb91f4ebbdd 100644 --- a/packages/python-packages/apiview-copilot/src/agent/plugins/_search_plugin.py +++ b/packages/python-packages/apiview-copilot/src/agent/tools/_search_tools.py @@ -4,21 +4,16 @@ # license information. # -------------------------------------------------------------------------- -"""Plugin for searching guidelines, examples, and APIView comments in the ArchAgent Knowledge Base.""" +"""Tools for searching guidelines, examples, and APIView comments in the ArchAgent Knowledge Base.""" -from semantic_kernel.functions import kernel_function from src._database_manager import ContainerNames from src._search_manager import SearchManager +from src.agent.tools._base import Tool -class SearchPlugin: - """Plugin for searching guidelines, examples, and APIView comments in the ArchAgent Knowledge Base.""" +class SearchTools(Tool): + """Tools for searching guidelines, examples, and APIView comments in the ArchAgent Knowledge Base.""" - @kernel_function( - description=""" - Search for Guidelines in the ArchAgent Knowledge Base. - """ - ) async def search_guidelines(self, query: str, language: str): """ Search for Guidelines in the ArchAgent Knowledge Base. @@ -31,7 +26,6 @@ async def search_guidelines(self, query: str, language: str): context = search.build_context(results.results) return context.to_markdown() - @kernel_function(description="Search for Examples in the ArchAgent Knowledge Base.") async def search_examples(self, query: str, language: str): """ Search for Examples in the ArchAgent Knowledge Base. @@ -44,10 +38,9 @@ async def search_examples(self, query: str, language: str): context = search.build_context(results.results) return context.to_markdown() - @kernel_function(description="Search for APIView comments in the ArchAgent Knowledge Base.") - async def search_api_view_comments(self, query: str, language: str): + async def search_memories(self, query: str, language: str): """ - Search for APIView comments in the ArchAgent Knowledge Base. + Search for memories in the ArchAgent Knowledge Base. Args: query (str): The search query. language (str): The programming language to filter results. @@ -57,7 +50,6 @@ async def search_api_view_comments(self, query: str, language: str): context = search.build_context(results.results) return context.to_markdown() - @kernel_function(description="Search the ArchAgent Knowledge Base for any content.") async def search_any(self, query: str, language: str): """ Search the ArchAgent Knowledge Base for any content. @@ -70,9 +62,6 @@ async def search_any(self, query: str, language: str): context = search.build_context(results.results) return context.to_markdown() - @kernel_function( - description="Trigger a reindex of a specific Azure Search indexer for the ArchAgent Knowledge Base." - ) async def run_indexer(self, container_name: str): """ Trigger a reindex of the Azure Search index for the ArchAgent Knowledge Base. @@ -83,7 +72,6 @@ async def run_indexer(self, container_name: str): return return SearchManager.run_indexers(container_names=[container_name]) - @kernel_function(description="Trigger a reindex of all Azure Search indexers for the ArchAgent Knowledge Base.") async def run_all_indexers(self): """ Trigger a reindex of all Azure Search indexers for the ArchAgent Knowledge Base. diff --git a/packages/python-packages/apiview-copilot/src/agent/plugins/_utility_plugin.py b/packages/python-packages/apiview-copilot/src/agent/tools/_utility_tools.py similarity index 90% rename from packages/python-packages/apiview-copilot/src/agent/plugins/_utility_plugin.py rename to packages/python-packages/apiview-copilot/src/agent/tools/_utility_tools.py index c9ecd1d9e79..1fa7f2d043f 100644 --- a/packages/python-packages/apiview-copilot/src/agent/plugins/_utility_plugin.py +++ b/packages/python-packages/apiview-copilot/src/agent/tools/_utility_tools.py @@ -14,13 +14,13 @@ import prompty import prompty.azure_beta import requests -from semantic_kernel.functions import kernel_function from src._diff import create_diff_with_line_numbers from src._utils import get_prompt_path +from src.agent.tools._base import Tool -class UtilityPlugin: - """Utility plugin for APIView Copilot.""" +class UtilityTools(Tool): + """Utility tools for APIView Copilot.""" def _download_if_url(self, file_path: str) -> str: """ @@ -37,7 +37,6 @@ def _download_if_url(self, file_path: str) -> str: return tmp.name return file_path - @kernel_function(description="Summarize the provided API.") async def summarize_api(self, api: str, language: str): """ Summarize the provided API. @@ -51,7 +50,6 @@ async def summarize_api(self, api: str, language: str): response = prompty.execute(prompt_path, inputs={"content": api, "language": language}, configuration={}) return response - @kernel_function(description="Summarize the differences between the provided APIs.") async def summarize_api_diff(self, target: str, base: str, language: str): """ Summarize the differences between the provided APIs. @@ -67,7 +65,6 @@ async def summarize_api_diff(self, target: str, base: str, language: str): response = prompty.execute(prompt_path, inputs={"content": api_diff, "language": language}, configuration={}) return response - @kernel_function(description="Load a JSON file from the specified path or URL.") async def load_json_file(self, file_path: str): """ Load a JSON file from the specified path or URL. @@ -86,7 +83,6 @@ async def load_json_file(self, file_path: str): except Exception as e: raise ValueError(f"Error reading JSON file {file_path}.") from e - @kernel_function(description="Load a text file from the specified path or URL.") async def load_text_file(self, file_path: str): """ Load a text file from the specified path or URL. From 234d1ef6cd22c7ddd00ed24d8d660a7794f1b794 Mon Sep 17 00:00:00 2001 From: Travis Prescott Date: Fri, 21 Nov 2025 11:27:00 -0800 Subject: [PATCH 3/6] Convert to toolset. --- .../apiview-copilot/src/agent/_agent.py | 10 ++++++---- .../apiview-copilot/src/agent/tools/_base.py | 4 +++- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/python-packages/apiview-copilot/src/agent/_agent.py b/packages/python-packages/apiview-copilot/src/agent/_agent.py index 2acb87a0433..b699327f76f 100644 --- a/packages/python-packages/apiview-copilot/src/agent/_agent.py +++ b/packages/python-packages/apiview-copilot/src/agent/_agent.py @@ -13,7 +13,7 @@ from typing import Optional from azure.ai.agents import AgentsClient -from azure.ai.agents.models import MessageRole, MessageTextContent +from azure.ai.agents.models import MessageRole, MessageTextContent, ToolSet from src._credential import get_credential from src._settings import SettingsManager from src.agent.tools._search_tools import SearchTools @@ -93,16 +93,18 @@ def get_main_agent(): # retrieve_agent = await stack.enter_async_context(get_retrieve_agent()) # link_agent = await stack.enter_async_context(get_link_agent()) - tools = SearchTools().all_tools() + toolset = ToolSet() + toolset.add(SearchTools().all_tools()) + agent = client.create_agent( name="APIView Copilot Main Agent", description="An agent that processes requests and passes work to other agents.", model=model_deployment_name, instructions=ai_instructions, - tools=tools, + toolset=toolset, ) # enable all tools by default - client.enable_auto_function_calls(tools=tools) + client.enable_auto_function_calls(tools=toolset) # agent = AzureAIAgent( # client=client, diff --git a/packages/python-packages/apiview-copilot/src/agent/tools/_base.py b/packages/python-packages/apiview-copilot/src/agent/tools/_base.py index c2b2b1f3ce5..96e3137a856 100644 --- a/packages/python-packages/apiview-copilot/src/agent/tools/_base.py +++ b/packages/python-packages/apiview-copilot/src/agent/tools/_base.py @@ -4,6 +4,8 @@ # license information. # -------------------------------------------------------------------------- +from azure.ai.agents.models import FunctionTool + class Tool: """Base class for agent tools. Provides automatic tool discovery.""" @@ -15,4 +17,4 @@ def all_tools(self): for name in dir(self) if not name.startswith("_") and callable(getattr(self, name)) and name != "all_tools" ] - return tools + return FunctionTool(tools) From acb79c6c2c8f6b5248431e2f9581c4f005289ef8 Mon Sep 17 00:00:00 2001 From: Travis Prescott Date: Fri, 21 Nov 2025 11:41:46 -0800 Subject: [PATCH 4/6] Expose more "plugins" as tools. --- .../apiview-copilot/src/agent/_agent.py | 28 ++++++++----------- .../src/agent/tools/__init__.py | 4 +-- .../src/agent/tools/_api_review_tools.py | 2 +- .../apiview-copilot/src/agent/tools/_base.py | 4 +-- 4 files changed, 15 insertions(+), 23 deletions(-) diff --git a/packages/python-packages/apiview-copilot/src/agent/_agent.py b/packages/python-packages/apiview-copilot/src/agent/_agent.py index b699327f76f..6cb3da5b5c5 100644 --- a/packages/python-packages/apiview-copilot/src/agent/_agent.py +++ b/packages/python-packages/apiview-copilot/src/agent/_agent.py @@ -13,10 +13,17 @@ from typing import Optional from azure.ai.agents import AgentsClient -from azure.ai.agents.models import MessageRole, MessageTextContent, ToolSet +from azure.ai.agents.models import ( + FunctionTool, + MessageRole, + MessageTextContent, + ToolSet, +) from src._credential import get_credential from src._settings import SettingsManager +from src.agent.tools._api_review_tools import ApiReviewTools from src.agent.tools._search_tools import SearchTools +from src.agent.tools._utility_tools import UtilityTools async def invoke_agent( @@ -88,13 +95,15 @@ def get_main_agent(): credential = get_credential() client = AgentsClient(endpoint=endpoint, credential=credential) + # TODO: These were treated as subagents before and may not be applicable in the new format. # 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()) toolset = ToolSet() - toolset.add(SearchTools().all_tools()) + tools = SearchTools().all_tools() + ApiReviewTools().all_tools() + UtilityTools().all_tools() + toolset.add(FunctionTool(tools)) agent = client.create_agent( name="APIView Copilot Main Agent", @@ -106,21 +115,6 @@ def get_main_agent(): # enable all tools by default client.enable_auto_function_calls(tools=toolset) - # 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: diff --git a/packages/python-packages/apiview-copilot/src/agent/tools/__init__.py b/packages/python-packages/apiview-copilot/src/agent/tools/__init__.py index 3109713db63..c0cf91155eb 100644 --- a/packages/python-packages/apiview-copilot/src/agent/tools/__init__.py +++ b/packages/python-packages/apiview-copilot/src/agent/tools/__init__.py @@ -8,7 +8,7 @@ This module initializes the plugins for the agent. """ -from ._api_review_tools import ApiReviewTool +from ._api_review_tools import ApiReviewTools from ._database_tools import ( get_create_agent, get_delete_agent, @@ -19,7 +19,7 @@ from ._utility_tools import UtilityTools __all__ = [ - "ApiReviewTool", + "ApiReviewTools", "SearchTools", "UtilityTools", "get_create_agent", diff --git a/packages/python-packages/apiview-copilot/src/agent/tools/_api_review_tools.py b/packages/python-packages/apiview-copilot/src/agent/tools/_api_review_tools.py index d06953f19d9..c5ec5c3894f 100644 --- a/packages/python-packages/apiview-copilot/src/agent/tools/_api_review_tools.py +++ b/packages/python-packages/apiview-copilot/src/agent/tools/_api_review_tools.py @@ -16,7 +16,7 @@ from src.agent.tools._base import Tool -class ApiReviewTool(Tool): +class ApiReviewTools(Tool): """Tools for API review operations.""" async def review_api(self, *, language: str, target: str): diff --git a/packages/python-packages/apiview-copilot/src/agent/tools/_base.py b/packages/python-packages/apiview-copilot/src/agent/tools/_base.py index 96e3137a856..c2b2b1f3ce5 100644 --- a/packages/python-packages/apiview-copilot/src/agent/tools/_base.py +++ b/packages/python-packages/apiview-copilot/src/agent/tools/_base.py @@ -4,8 +4,6 @@ # license information. # -------------------------------------------------------------------------- -from azure.ai.agents.models import FunctionTool - class Tool: """Base class for agent tools. Provides automatic tool discovery.""" @@ -17,4 +15,4 @@ def all_tools(self): for name in dir(self) if not name.startswith("_") and callable(getattr(self, name)) and name != "all_tools" ] - return FunctionTool(tools) + return tools From 9f0c52c67158ad3a70bd76a0de271473d1ea5d37 Mon Sep 17 00:00:00 2001 From: Travis Prescott Date: Fri, 21 Nov 2025 12:58:01 -0800 Subject: [PATCH 5/6] Make tool calls synchronous. --- .../src/agent/tools/_api_review_tools.py | 20 +++++------ .../src/agent/tools/_database_tools.py | 36 +++++++++---------- .../src/agent/tools/_search_tools.py | 12 +++---- .../src/agent/tools/_utility_tools.py | 8 ++--- 4 files changed, 38 insertions(+), 38 deletions(-) diff --git a/packages/python-packages/apiview-copilot/src/agent/tools/_api_review_tools.py b/packages/python-packages/apiview-copilot/src/agent/tools/_api_review_tools.py index c5ec5c3894f..9f2fc620e2c 100644 --- a/packages/python-packages/apiview-copilot/src/agent/tools/_api_review_tools.py +++ b/packages/python-packages/apiview-copilot/src/agent/tools/_api_review_tools.py @@ -19,7 +19,7 @@ class ApiReviewTools(Tool): """Tools for API review operations.""" - async def review_api(self, *, language: str, target: str): + def review_api(self, *, language: str, target: str): """ Perform an API review on a single API. Args: @@ -30,7 +30,7 @@ async def review_api(self, *, language: str, target: str): results = reviewer.run() return json.dumps(results.model_dump(), indent=2) - async def review_api_diff(self, *, language: str, target: str, base: str): + def review_api_diff(self, *, language: str, target: str, base: str): """ Perform an API review on a diff between two APIs. Args: @@ -42,16 +42,16 @@ async def review_api_diff(self, *, language: str, target: str, base: str): results = reviewer.run() return json.dumps(results.model_dump(), indent=2) - async def get_apiview_revision(self, *, revision_id: str) -> str: + def get_apiview_revision(self, *, revision_id: str) -> str: """ Get the text of an API revision. Args: revision_id (str): The ID of the API revision to retrieve. """ client = ApiViewClient() - return await client.get_revision_text(revision_id=revision_id) + return client.get_revision_text(revision_id=revision_id) - async def get_apiview_revision_by_review(self, *, review_id: str, label: str = "Latest") -> str: + def get_apiview_revision_by_review(self, *, review_id: str, label: str = "Latest") -> str: """ Get the text of an API revision by review ID and label. Args: @@ -59,22 +59,22 @@ async def get_apiview_revision_by_review(self, *, review_id: str, label: str = " label (str): The label of the API revision to retrieve. """ client = ApiViewClient() - return await client.get_revision_text(review_id=review_id, label=label) + return client.get_revision_text(review_id=review_id, label=label) - async def get_apiview_revision_outline(self, *, revision_id: str) -> str: + def get_apiview_revision_outline(self, *, revision_id: str) -> str: """ Get the outline for a given API revision. Args: revision_id (str): The ID of the API revision to retrieve. """ client = ApiViewClient() - return await client.get_revision_outline(revision_id=revision_id) + return client.get_revision_outline(revision_id=revision_id) - async def get_apiview_revision_comments(self, *, revision_id: str) -> str: + def get_apiview_revision_comments(self, *, revision_id: str) -> str: """ Retrieves any existing comments for a given API revision Args: revision_id (str): The ID of the API revision to retrieve comments for. """ client = ApiViewClient() - return await client.get_review_comments(revision_id=revision_id) + return client.get_review_comments(revision_id=revision_id) diff --git a/packages/python-packages/apiview-copilot/src/agent/tools/_database_tools.py b/packages/python-packages/apiview-copilot/src/agent/tools/_database_tools.py index 0e29db65fd0..6ccc951fbea 100644 --- a/packages/python-packages/apiview-copilot/src/agent/tools/_database_tools.py +++ b/packages/python-packages/apiview-copilot/src/agent/tools/_database_tools.py @@ -60,7 +60,7 @@ def get_delete_agent(): @contextmanager -async def get_create_agent(): +def get_create_agent(): """Agent for handling database create requests.""" from src._apiview_reviewer import SUPPORTED_LANGUAGES from src.agent._agent import _get_agent_settings @@ -157,7 +157,7 @@ async def get_create_agent(): @contextmanager -async def get_retrieve_agent(): +def get_retrieve_agent(): """Agent for handling database retrieval requests.""" from src.agent._agent import _get_agent_settings @@ -184,7 +184,7 @@ async def get_retrieve_agent(): @contextmanager -async def get_link_agent(): +def get_link_agent(): """Agent for handling database linking and unlinking requests.""" from src.agent._agent import _get_agent_settings @@ -232,7 +232,7 @@ async def get_link_agent(): class DatabaseCreateTool(Tool): """Tool for creating database items.""" - async def create_guideline( + def create_guideline( self, id: str, content: str, @@ -256,7 +256,7 @@ async def create_guideline( db = DatabaseManager.get_instance() return db.guidelines.create(id, data=data) - async def create_memory( + def create_memory( self, id: str, title: str, @@ -292,7 +292,7 @@ async def create_memory( db = DatabaseManager.get_instance() return db.memories.create(id, data=data) - async def create_example( + def create_example( self, title: str, content: str, @@ -332,7 +332,7 @@ async def create_example( class DatabaseRetrieveTool(Tool): """Tool for retrieving database items.""" - async def get_memory(self, memory_id: str): + def get_memory(self, memory_id: str): """ Retrieve a memory from the database by its ID. Args: @@ -341,13 +341,13 @@ async def get_memory(self, memory_id: str): db = DatabaseManager.get_instance() return db.memories.get(memory_id) - async def get_memory_schema(self): + def get_memory_schema(self): """ Retrieve the Memory schema. """ return Memory.model_json_schema() - async def get_example(self, example_id: str): + def get_example(self, example_id: str): """ Retrieve an example from the database by its ID. Args: @@ -356,13 +356,13 @@ async def get_example(self, example_id: str): db = DatabaseManager.get_instance() return db.examples.get(example_id) - async def get_example_schema(self): + def get_example_schema(self): """ Retrieve the Example schema. """ return Example.model_json_schema() - async def get_guideline(self, guideline_id: str): + def get_guideline(self, guideline_id: str): """ Retrieve a guideline from the database by its ID. Args: @@ -371,7 +371,7 @@ async def get_guideline(self, guideline_id: str): db = DatabaseManager.get_instance() return db.guidelines.get(guideline_id) - async def get_guideline_schema(self): + def get_guideline_schema(self): """ Retrieve the Guideline schema. """ @@ -381,7 +381,7 @@ async def get_guideline_schema(self): class DatabaseLinkUnlinkTool(Tool): """Tool for linking and unlinking database items.""" - async def link_items( + def link_items( self, source_id: str, source_container: str, @@ -448,7 +448,7 @@ async def link_items( target_c.upsert(target_id, data=target_item) return {"status": "done", "source_id": source_id, "source_field": source_field, **results} - async def unlink_items( + def unlink_items( self, source_id: str, source_container: str, @@ -521,7 +521,7 @@ async def unlink_items( class DatabaseDeleteTool(Tool): """Tool for deleting database items.""" - async def delete_guideline(self, id: str): + def delete_guideline(self, id: str): """Delete a guideline from the database by its ID.""" db = DatabaseManager.get_instance() try: @@ -530,7 +530,7 @@ async def delete_guideline(self, id: str): except CosmosResourceNotFoundError: return f"Guideline with id '{id}' not found." - async def delete_memory(self, id: str): + def delete_memory(self, id: str): """Delete a memory from the database by its ID.""" db = DatabaseManager.get_instance() try: @@ -539,7 +539,7 @@ async def delete_memory(self, id: str): except CosmosResourceNotFoundError: return f"Memory with id '{id}' not found." - async def delete_example(self, id: str): + def delete_example(self, id: str): """Delete an example from the database by its ID.""" db = DatabaseManager.get_instance() try: @@ -548,7 +548,7 @@ async def delete_example(self, id: str): except CosmosResourceNotFoundError: return f"Example with id '{id}' not found." - async def delete_review_job(self, id: str): + def delete_review_job(self, id: str): """Delete a review job from the database by its ID.""" db = DatabaseManager.get_instance() try: diff --git a/packages/python-packages/apiview-copilot/src/agent/tools/_search_tools.py b/packages/python-packages/apiview-copilot/src/agent/tools/_search_tools.py index eb91f4ebbdd..b9909335723 100644 --- a/packages/python-packages/apiview-copilot/src/agent/tools/_search_tools.py +++ b/packages/python-packages/apiview-copilot/src/agent/tools/_search_tools.py @@ -14,7 +14,7 @@ class SearchTools(Tool): """Tools for searching guidelines, examples, and APIView comments in the ArchAgent Knowledge Base.""" - async def search_guidelines(self, query: str, language: str): + def search_guidelines(self, query: str, language: str): """ Search for Guidelines in the ArchAgent Knowledge Base. Args: @@ -26,7 +26,7 @@ async def search_guidelines(self, query: str, language: str): context = search.build_context(results.results) return context.to_markdown() - async def search_examples(self, query: str, language: str): + def search_examples(self, query: str, language: str): """ Search for Examples in the ArchAgent Knowledge Base. Args: @@ -38,7 +38,7 @@ async def search_examples(self, query: str, language: str): context = search.build_context(results.results) return context.to_markdown() - async def search_memories(self, query: str, language: str): + def search_memories(self, query: str, language: str): """ Search for memories in the ArchAgent Knowledge Base. Args: @@ -50,7 +50,7 @@ async def search_memories(self, query: str, language: str): context = search.build_context(results.results) return context.to_markdown() - async def search_any(self, query: str, language: str): + def search_any(self, query: str, language: str): """ Search the ArchAgent Knowledge Base for any content. Args: @@ -62,7 +62,7 @@ async def search_any(self, query: str, language: str): context = search.build_context(results.results) return context.to_markdown() - async def run_indexer(self, container_name: str): + def run_indexer(self, container_name: str): """ Trigger a reindex of the Azure Search index for the ArchAgent Knowledge Base. Args: @@ -72,7 +72,7 @@ async def run_indexer(self, container_name: str): return return SearchManager.run_indexers(container_names=[container_name]) - async def run_all_indexers(self): + def run_all_indexers(self): """ Trigger a reindex of all Azure Search indexers for the ArchAgent Knowledge Base. """ diff --git a/packages/python-packages/apiview-copilot/src/agent/tools/_utility_tools.py b/packages/python-packages/apiview-copilot/src/agent/tools/_utility_tools.py index 1fa7f2d043f..3a3db064c1d 100644 --- a/packages/python-packages/apiview-copilot/src/agent/tools/_utility_tools.py +++ b/packages/python-packages/apiview-copilot/src/agent/tools/_utility_tools.py @@ -37,7 +37,7 @@ def _download_if_url(self, file_path: str) -> str: return tmp.name return file_path - async def summarize_api(self, api: str, language: str): + def summarize_api(self, api: str, language: str): """ Summarize the provided API. Args: @@ -50,7 +50,7 @@ async def summarize_api(self, api: str, language: str): response = prompty.execute(prompt_path, inputs={"content": api, "language": language}, configuration={}) return response - async def summarize_api_diff(self, target: str, base: str, language: str): + def summarize_api_diff(self, target: str, base: str, language: str): """ Summarize the differences between the provided APIs. Args: @@ -65,7 +65,7 @@ async def summarize_api_diff(self, target: str, base: str, language: str): response = prompty.execute(prompt_path, inputs={"content": api_diff, "language": language}, configuration={}) return response - async def load_json_file(self, file_path: str): + def load_json_file(self, file_path: str): """ Load a JSON file from the specified path or URL. Args: @@ -83,7 +83,7 @@ async def load_json_file(self, file_path: str): except Exception as e: raise ValueError(f"Error reading JSON file {file_path}.") from e - async def load_text_file(self, file_path: str): + def load_text_file(self, file_path: str): """ Load a text file from the specified path or URL. Args: From c5f0b1842d8e57b7dff85928e22585a49bf5882d Mon Sep 17 00:00:00 2001 From: Travis Prescott Date: Fri, 21 Nov 2025 15:22:46 -0800 Subject: [PATCH 6/6] Fix output bug. --- .../python-packages/apiview-copilot/src/agent/_agent.py | 2 +- .../apiview-copilot/src/agent/tools/_api_review_tools.py | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/python-packages/apiview-copilot/src/agent/_agent.py b/packages/python-packages/apiview-copilot/src/agent/_agent.py index 6cb3da5b5c5..e8f3e468b05 100644 --- a/packages/python-packages/apiview-copilot/src/agent/_agent.py +++ b/packages/python-packages/apiview-copilot/src/agent/_agent.py @@ -69,7 +69,7 @@ def extract_text(obj): return str(obj) response_text = "" - for m in reversed(list(all_messages)): + for m in list(all_messages): role = getattr(m, "role", None) if role == MessageRole.AGENT.value or role == "assistant": parts = getattr(m, "content", None) diff --git a/packages/python-packages/apiview-copilot/src/agent/tools/_api_review_tools.py b/packages/python-packages/apiview-copilot/src/agent/tools/_api_review_tools.py index 9f2fc620e2c..7bd00dc63b3 100644 --- a/packages/python-packages/apiview-copilot/src/agent/tools/_api_review_tools.py +++ b/packages/python-packages/apiview-copilot/src/agent/tools/_api_review_tools.py @@ -8,6 +8,7 @@ Tools for performing API reviews using the ApiViewReview class. """ +import asyncio import json from semantic_kernel.functions import kernel_function @@ -49,7 +50,7 @@ def get_apiview_revision(self, *, revision_id: str) -> str: revision_id (str): The ID of the API revision to retrieve. """ client = ApiViewClient() - return client.get_revision_text(revision_id=revision_id) + return asyncio.run(client.get_revision_text(revision_id=revision_id)) def get_apiview_revision_by_review(self, *, review_id: str, label: str = "Latest") -> str: """ @@ -59,7 +60,7 @@ def get_apiview_revision_by_review(self, *, review_id: str, label: str = "Latest label (str): The label of the API revision to retrieve. """ client = ApiViewClient() - return client.get_revision_text(review_id=review_id, label=label) + return asyncio.run(client.get_revision_text(review_id=review_id, label=label)) def get_apiview_revision_outline(self, *, revision_id: str) -> str: """ @@ -68,7 +69,7 @@ def get_apiview_revision_outline(self, *, revision_id: str) -> str: revision_id (str): The ID of the API revision to retrieve. """ client = ApiViewClient() - return client.get_revision_outline(revision_id=revision_id) + return asyncio.run(client.get_revision_outline(revision_id=revision_id)) def get_apiview_revision_comments(self, *, revision_id: str) -> str: """ @@ -77,4 +78,4 @@ def get_apiview_revision_comments(self, *, revision_id: str) -> str: revision_id (str): The ID of the API revision to retrieve comments for. """ client = ApiViewClient() - return client.get_review_comments(revision_id=revision_id) + return asyncio.run(client.get_review_comments(revision_id=revision_id))