Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
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
2 changes: 2 additions & 0 deletions eng/common/scripts/Prepare-Release.ps1
100644 → 100755
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
#!/usr/bin/env pwsh

#Requires -Version 6.0

<#
Expand Down
26 changes: 26 additions & 0 deletions packages/python-packages/apiview-copilot/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
from src._diff import create_diff_with_line_numbers
from src._mention import handle_mention_request
from src._settings import SettingsManager
from src._thread_resolution import handle_thread_resolution_request
from src._utils import get_language_pretty_name, get_prompt_path
from src.agent._agent import get_main_agent, invoke_agent

Expand Down Expand Up @@ -253,6 +254,31 @@ async def handle_mention(request: MentionRequest):
raise HTTPException(status_code=500, detail="Internal server error") from e


@app.post("/api-review/resolve", response_model=AgentChatResponse)
async def handle_thread_resolution(request: MentionRequest):
"""Handle thread resolution in API reviews."""
logger.info(
"Received /api-review/resolve request: language=%s, package_name=%s, comments_count=%d",
request.language,
request.package_name,
len(request.comments) if request.comments else 0,
)
try:
pretty_language = get_language_pretty_name(request.language)
response = handle_thread_resolution_request(
comments=request.comments,
language=pretty_language,
package_name=request.package_name,
code=request.code,
)
return AgentChatResponse(
response=response, thread_id="", messages=[] # No thread ID for this endpoint # No messages to return
)
except HTTPException as e:
logger.error("Error in /api-review/resolve: %s", e, exc_info=True)
raise HTTPException(status_code=500, detail="Internal server error") from e


# Start the cleanup thread when the app starts
cleanup_thread = threading.Thread(target=cleanup_job_store, daemon=True)
cleanup_thread.start()
62 changes: 55 additions & 7 deletions packages/python-packages/apiview-copilot/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
from src._metrics import get_metrics_report
from src._search_manager import SearchManager
from src._settings import SettingsManager
from src._thread_resolution import handle_thread_resolution_request
from src._utils import get_language_pretty_name
from src.agent._agent import get_main_agent, invoke_agent

Expand Down Expand Up @@ -589,6 +590,53 @@ def handle_agent_mention(comments_path: str, remote: bool = False):
)


def handle_agent_thread_resolution(comments_path: str, remote: bool = False):
"""
Handles requests to update the knowledge base when a conversation is resolved.
"""
# load comments from the comments_path
comments = []
if os.path.exists(comments_path):
with open(comments_path, "r", encoding="utf-8") as f:
data = json.load(f)
else:
print(f"Comments file {comments_path} does not exist.")
return
comments = data.get("comments", [])
language = data.get("language", None)
package_name = data.get("package_name", None)
code = data.get("code", None)
if language not in SUPPORTED_LANGUAGES:
print(f"Unsupported language `{language}`")
return
pretty_language = get_language_pretty_name(language)

if remote:
settings = SettingsManager()
base_url = settings.get("WEBAPP_ENDPOINT")
api_endpoint = f"{base_url}/api-review/resolve"
try:
resp = requests.post(
api_endpoint,
json={"comments": comments, "language": language, "packageName": package_name, "code": code},
timeout=60,
)
data = resp.json()
if resp.status_code == 200:
print(f"{BOLD_BLUE}Agent response:{RESET}\n{data.get('response', '')}\n")
else:
print(f"Error: {resp.status_code} - {data}")
except Exception as e:
print(f"Error: {e}")
else:
return handle_thread_resolution_request(
comments=comments,
language=pretty_language,
package_name=package_name,
code=code,
)


def db_get(container_name: str, id: str):
"""Retrieve an item from the database."""
db = get_database_manager()
Expand Down Expand Up @@ -837,6 +885,7 @@ def load_command_table(self, args):
with CommandGroup(self, "agent", "__main__#{}") as g:
g.command("mention", "handle_agent_mention")
g.command("chat", "handle_agent_chat")
g.command("resolve-thread", "handle_agent_thread_resolution")
with CommandGroup(self, "eval", "__main__#{}") as g:
g.command("run", "run_test_case")
g.command("create", "create_test_case")
Expand Down Expand Up @@ -1064,20 +1113,19 @@ def load_arguments(self, command):
help="The path to the base APIView file for diff summarization.",
options_list=["--base", "-b"],
)
with ArgumentsContext(self, "agent mention") as ac:
ac.argument(
"comments_path",
type=str,
help="Path to the JSON file containing comments for the agent to process.",
options_list=["--comments-path", "-c"],
)
with ArgumentsContext(self, "agent") as ac:
ac.argument(
"thread_id",
type=str,
help="The thread ID to continue the discussion. If not provided, a new thread will be created.",
options_list=["--thread-id", "-t"],
)
ac.argument(
"comments_path",
type=str,
help="Path to the JSON file containing comments for the agent to process.",
options_list=["--comments-path", "-c"],
)
with ArgumentsContext(self, "db") as ac:
ac.argument(
"container_name",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
{
"type": "json_schema",
"json_schema": {
"name": "conversation_parsing_result",
"strict": true,
"schema": {
"type": "object",
"properties": {
"guideline_ids": {
"type": "array",
"description": "List of guideline IDs that are relevant to the conversation, if any.",
"items": {
"type": "string"
}
},
"memory": {
"$ref": "#/definitions/Memory"
}
},
"required": ["guideline_ids", "memory"],
"additionalProperties": false,
"definitions": {
"Example": {
"type": "object",
"properties": {
"content": {
"type": "string",
"description": "Code snippet demonstrating the example."
},
"id": {
"type": "string",
"description": "Unique identifier for the example."
},
"title": {
"type": "string",
"description": "Short descriptive title of the example."
},
"example_type": {
"type": "string",
"description": "Whether this example is 'good' or 'bad'.",
"enum": ["good", "bad"]
}
},
"required": ["id", "content", "title", "example_type"],
"additionalProperties": false
},
"Memory": {
"type": "object",
"properties": {
"content": {
"type": "string",
"description": "Memory content."
},
"id": {
"type": "string",
"description": "Unique identifier for the memory."
},
"title": {
"type": "string",
"description": "Short descriptive title of the memory."
},
"language": {
"type": "string",
"description": "Programming language (e.g. 'python'), if this memory is specific to a language."
},
"service": {
"type": "string",
"description": "Service name (e.g., 'Storage Blob') if this memory is specific to a service, or empty if it is generally applicable."
},
"is_exception": {
"type": "boolean",
"description": "Identifies whether this memory describes an exception to established policies or is a reinforcement of them."
},
"related_examples": {
"type": "array",
"description": "Array of example objects relevant to this memory.",
"items": {
"$ref": "#/definitions/Example"
}
}
},
"required": [
"content",
"id",
"title",
"language",
"service",
"is_exception",
"related_examples"
],
"additionalProperties": false
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
---
name: Parse APIView Conversation Resolution - Action
description: A prompt that parses an APIView conversation resolutions and determines if action is required.
authors:
- tjprescott
version: 1.0.0
model:
api: chat
configuration:
type: azure_openai
api_version: 2025-03-01-preview
azure_endpoint: ${env:OPENAI_ENDPOINT}
azure_deployment: gpt-5
parameters:
frequency_penalty: 0
presence_penalty: 0
max_completion_tokens: 80000
reasoning_effort: "minimal"
sample:
language: python
package_name: azure.widget
code: "class WidgetObject:"
comments: |
[
{
"CreatedBy": "azure-sdk",
"CommentText": "This name is unnecessarily verbose.\n\nSuggestion: `Widget`.\n\n**GUIDELINES**\n\n'https://azure.github.io/azure-sdk/python_design.html#naming-conventions'.",
"CreatedOn": "2025-03-17T17:48:25.920445-04:00"
},
{
"CreatedBy": "noodle",
"CommentText": "We discussed it internally and want to keep it as is because we used that name in the JS SDK and we want to keep them the same.",
"CreatedOn": "2025-03-18T13:15:19.1494832-04:00"
},
{
"CreatedBy": "tjprescott",
"CommentText": "Noodle, sorry, that's not a valid reason. If you wanted the names to be consistent you should have had them reviewed at the same time. The suffix `Object` adds no useful information and just results in a longer name.",
"CreatedOn": "2025-03-19T17:48:25.920445-04:00"
},
{
"CreatedBy": "noodle",
"CommentText": "But it actually is important and we use it elsewhere, for example BloopObject.",
"CreatedOn": "2025-03-19T17:48:26.920445-04:00"
},
{
"CreatedBy": "tjprescott",
"CommentText": "Noodle, ugh, okay. Since it is already used in other places that have released, this is an allowable exception.",
"CreatedOn": "2025-03-19T17:49:25.920445-04:00"
}
]
---
system:
You are an agent that processes resolved conversations from APIView to determine if any action should be taken.

# INSTRUCTIONS

- Your job is to determine what the outcome of a resolved conversation is and whether any action needs to be taken within the Knowledge Base.
- Your response must be in JSON format (DO NOT include the ```json at the start or ``` at the end of your response!):
```json
{
"action": "<record_result|record_exception|no_action|unclear>",
"reasoning": "<rationale>"
}
```

# Examples

## record_result

`record_result` means that the conversation states or clearly implies that the suggestion made by either "azure-sdk" or the architect was implemented. Usually, this means that someone other than "azure-sdk" or
an architect made the final comment acknowledging they would make the change. In this case, we want to update the Knowledge Base tying the original problem to the accepted solution, so it can quickly be
reapplied in the future.

## record_exception

`record_exception` means that the conversation states or clearly implies that the suggestion made by either "azure-sdk" or the architect was not implemented, and the reason why. Usually, this means that the
final comment from "azure-sdk" or the architect acknowledges that this is a permitted exception. In this case, we want to update the Knowledge Base to reflect this exception, for this language and this service
only.

## no_action

`no_action` means that the conversation states or clearly implies that the suggestion made by either "azure-sdk" or the architect was not implemented, and the reason why. Usually, this means that the final
comment from the architect acknowledges that this is fine, either because the architect or "azure-sdk" was mistaken, or because it was an arbitrary choice and the architect is fine with the service team's
decision. In this, no update should be made to the Knowledge Base.

## unclear

`unclear` means that the conversation does not provide enough context to determine whether one of the other cases applies or not. In this case, we do not want to the update the Knowledge Base. We may
want to seek clarity, but we MUST NOT make assumptions about the outcome. Usually, this means that the final comment is from "azure-sdk" or the architect, but it does not clearly state whether that the
disputed code is fine.

user:
This {{language}} code is being discussed for the {{package_name}} package.
```{{language}}
{{code}}
```

Here is the conversation:
{{comments}}

Please determine what action, if any, should be taken based on the conversation resolution.
Loading
Loading