Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,10 @@ You receive these inputs:
* **language** — optional language hint (e.g. `python`, `C#`). May be empty.
* **comment_context** — optional structured snapshot of the comment thread
the user was viewing (source, language, comment text, code snippet,
element id). Use it to infer what the user is talking about.
element id). When the thread has more than one comment, an additional
`Thread:` block lists every comment in chronological order as
`@author (timestamp): text` so you can see the full conversation, not
just the anchor comment. Use it to infer what the user is talking about.

You must determine the **category** yourself. Allowed categories:

Expand Down
34 changes: 33 additions & 1 deletion packages/python-packages/apiview-copilot/src/_apiview.py
Original file line number Diff line number Diff line change
Expand Up @@ -1323,7 +1323,10 @@ def get_comment_with_context(comment_id: str, environment: str = "production") -

Returns:
A dict containing:
- comment: The full comment object from the database
- comment: The full comment object from the database (the anchor comment)
- thread_comments: List of comments sharing the same ThreadId, in
chronological order (always includes the anchor comment; falls back
to ``[comment]`` when the thread has no other replies)
Comment thread
helen229 marked this conversation as resolved.
Outdated
- language: The pretty language name (e.g., "Python")
- package_name: The package name from the review
- code: The API code from the revision (if available)
Expand Down Expand Up @@ -1358,6 +1361,34 @@ def get_comment_with_context(comment_id: str, environment: str = "production") -
comment = results[0]
review_id = comment.get("ReviewId")
revision_id = comment.get("APIRevisionId")
thread_id = comment.get("ThreadId")

# Fetch sibling comments in the same thread (chronological order) so
# callers can render the full conversation, not just the anchor comment.
thread_comments: list[dict] = [comment]
if thread_id:
thread_query = """
SELECT c.id, c.CommentText, c.CommentSource, c.CreatedBy, c.CreatedOn,
c.IsResolved, c.IsDeleted, c.ThreadId
FROM c
WHERE c.ThreadId = @thread_id
ORDER BY c.CreatedOn ASC
"""
try:
thread_results = list(
comments_container.query_items(
query=thread_query,
parameters=[{"name": "@thread_id", "value": thread_id}],
enable_cross_partition_query=True,
)
)
# Drop tombstoned comments; keep order from the query.
thread_comments = [c for c in thread_results if not c.get("IsDeleted")]
Comment thread
helen229 marked this conversation as resolved.
Outdated
if not thread_comments:
thread_comments = [comment]
except Exception as e:
print(f"Warning: Could not fetch thread siblings for {thread_id}: {e}")
thread_comments = [comment]

# Get language and package name from Reviews container
language = None
Expand Down Expand Up @@ -1425,6 +1456,7 @@ def get_comment_with_context(comment_id: str, environment: str = "production") -

return {
"comment": comment,
"thread_comments": thread_comments,
"language": language,
"package_name": package_name,
"code": code,
Expand Down
29 changes: 29 additions & 0 deletions packages/python-packages/apiview-copilot/src/_report_issue.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,23 @@ def _format_comment_context_for_prompt(ctx: Optional[dict]) -> str:
value = ctx.get(key)
if value:
parts.append(f"{label}: {value}")
thread = ctx.get("thread_comments") or []
# Only render a transcript when there is more than the anchor comment;
# the single-comment case is already covered by the "Comment" line above.
if len(thread) > 1:
transcript_lines: list[str] = []
for entry in thread:
author = entry.get("created_by") or "unknown"
created_on = entry.get("created_on") or ""
text = (entry.get("comment_text") or "").strip()
if not text:
continue
header = f"@{author}"
if created_on:
header = f"{header} ({created_on})"
transcript_lines.append(f"{header}: {text}")
if transcript_lines:
parts.append("Thread:\n" + "\n".join(transcript_lines))
Comment thread
helen229 marked this conversation as resolved.
return "\n".join(parts)


Expand Down Expand Up @@ -127,6 +144,17 @@ def _lookup_comment_context(comment_id: str) -> Optional[dict]:
if not ctx:
return None
comment_obj = ctx.get("comment") or {}
thread_raw = ctx.get("thread_comments") or []
thread_comments = [
{
"id": entry.get("id"),
"comment_text": entry.get("CommentText"),
"comment_source": entry.get("CommentSource"),
"created_by": entry.get("CreatedBy"),
"created_on": entry.get("CreatedOn"),
}
for entry in thread_raw
]
return {
"comment_text": comment_obj.get("CommentText"),
"comment_source": comment_obj.get("CommentSource"),
Expand All @@ -135,6 +163,7 @@ def _lookup_comment_context(comment_id: str) -> Optional[dict]:
"element_id": comment_obj.get("ElementId"),
"review_id": comment_obj.get("ReviewId"),
"revision_id": comment_obj.get("APIRevisionId"),
"thread_comments": thread_comments,
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,28 @@ def test_empty_dict(self):
def test_none(self):
assert _format_comment_context_for_prompt(None) == ""

def test_single_comment_thread_does_not_emit_transcript(self):
text = _format_comment_context_for_prompt({
"comment_text": "lone comment",
"thread_comments": [
{"comment_text": "lone comment", "created_by": "alice", "created_on": "2026-05-01T00:00:00Z"},
],
})
assert "Thread:" not in text
assert "Comment: lone comment" in text

def test_multi_comment_thread_emits_transcript(self):
text = _format_comment_context_for_prompt({
"comment_text": "first",
"thread_comments": [
{"comment_text": "first", "created_by": "alice", "created_on": "2026-05-01T00:00:00Z"},
{"comment_text": "second", "created_by": "bob", "created_on": "2026-05-02T00:00:00Z"},
],
})
assert "Thread:" in text
assert "@alice (2026-05-01T00:00:00Z): first" in text
assert "@bob (2026-05-02T00:00:00Z): second" in text


class TestBuildFallbackTitleSnippet:
def test_short(self):
Expand Down Expand Up @@ -280,6 +302,22 @@ def test_maps_db_payload_to_comment_context(self, mock_get):
"ReviewId": "r-1",
"APIRevisionId": "rev-2",
},
"thread_comments": [
{
"id": "c1",
"CommentText": "remove async",
"CommentSource": "copilot",
"CreatedBy": "azure-sdk",
"CreatedOn": "2026-05-01T00:00:00Z",
},
{
"id": "c2",
"CommentText": "actually it should stay async",
"CommentSource": "UserGenerated",
"CreatedBy": "alice",
"CreatedOn": "2026-05-02T00:00:00Z",
},
],
"code": "async def upload_blob(self, name: str, data: bytes) -> None: ...",
"language": "Python",
"package_name": "azure-storage-blob",
Expand All @@ -293,6 +331,22 @@ def test_maps_db_payload_to_comment_context(self, mock_get):
"element_id": "AsyncBlobClient.upload_blob",
"review_id": "r-1",
"revision_id": "rev-2",
"thread_comments": [
{
"id": "c1",
"comment_text": "remove async",
"comment_source": "copilot",
"created_by": "azure-sdk",
"created_on": "2026-05-01T00:00:00Z",
},
{
"id": "c2",
"comment_text": "actually it should stay async",
"comment_source": "UserGenerated",
"created_by": "alice",
"created_on": "2026-05-02T00:00:00Z",
},
],
}

@patch("src._report_issue.os.getenv")
Expand Down