agent-framework-github-copilot 1.0.0rc4__tar.gz → 1.0.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {agent_framework_github_copilot-1.0.0rc4 → agent_framework_github_copilot-1.0.2}/PKG-INFO +33 -6
- {agent_framework_github_copilot-1.0.0rc4 → agent_framework_github_copilot-1.0.2}/README.md +29 -1
- {agent_framework_github_copilot-1.0.0rc4 → agent_framework_github_copilot-1.0.2}/agent_framework_github_copilot/_agent.py +272 -10
- agent_framework_github_copilot-1.0.2/agent_framework_github_copilot/_feature_usage.py +9 -0
- {agent_framework_github_copilot-1.0.0rc4 → agent_framework_github_copilot-1.0.2}/pyproject.toml +4 -5
- {agent_framework_github_copilot-1.0.0rc4 → agent_framework_github_copilot-1.0.2}/LICENSE +0 -0
- {agent_framework_github_copilot-1.0.0rc4 → agent_framework_github_copilot-1.0.2}/agent_framework_github_copilot/__init__.py +0 -0
|
@@ -1,22 +1,21 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agent-framework-github-copilot
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.2
|
|
4
4
|
Summary: GitHub Copilot integration for Microsoft Agent Framework.
|
|
5
5
|
Author-email: Microsoft <af-support@microsoft.com>
|
|
6
|
-
Requires-Python: >=3.
|
|
6
|
+
Requires-Python: >=3.11
|
|
7
7
|
Description-Content-Type: text/markdown
|
|
8
8
|
Classifier: License :: OSI Approved :: MIT License
|
|
9
|
-
Classifier: Development Status ::
|
|
9
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
10
10
|
Classifier: Intended Audience :: Developers
|
|
11
11
|
Classifier: Programming Language :: Python :: 3
|
|
12
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
13
12
|
Classifier: Programming Language :: Python :: 3.11
|
|
14
13
|
Classifier: Programming Language :: Python :: 3.12
|
|
15
14
|
Classifier: Programming Language :: Python :: 3.13
|
|
16
15
|
Classifier: Programming Language :: Python :: 3.14
|
|
17
16
|
Classifier: Typing :: Typed
|
|
18
17
|
License-File: LICENSE
|
|
19
|
-
Requires-Dist: agent-framework-core>=1.
|
|
18
|
+
Requires-Dist: agent-framework-core>=1.13.0,<2
|
|
20
19
|
Requires-Dist: github-copilot-sdk==1.0.2; python_version >= '3.11'
|
|
21
20
|
Project-URL: homepage, https://aka.ms/agent-framework
|
|
22
21
|
Project-URL: issues, https://github.com/microsoft/agent-framework/issues
|
|
@@ -28,7 +27,7 @@ Project-URL: source, https://github.com/microsoft/agent-framework/tree/main/pyth
|
|
|
28
27
|
Please install this package via pip:
|
|
29
28
|
|
|
30
29
|
```bash
|
|
31
|
-
pip install agent-framework-github-copilot
|
|
30
|
+
pip install agent-framework-github-copilot
|
|
32
31
|
```
|
|
33
32
|
|
|
34
33
|
## GitHub Copilot Agent
|
|
@@ -75,6 +74,34 @@ agent = GitHubCopilotAgent(
|
|
|
75
74
|
> Note: with the default (deny-all) permission handler, an `always_require` tool is denied
|
|
76
75
|
> unless you wire an approving `on_permission_request`.
|
|
77
76
|
|
|
77
|
+
### Approving for the rest of the session
|
|
78
|
+
|
|
79
|
+
`PermissionDecisionApproveForSession` scopes its approval with either an `approval` (tool
|
|
80
|
+
prompts) or a `domain` (URL prompts). Both are optional, so a bare
|
|
81
|
+
`PermissionDecisionApproveForSession()` carries no scope at all and the Copilot CLI cannot
|
|
82
|
+
interpret it.
|
|
83
|
+
|
|
84
|
+
`GitHubCopilotAgent` therefore scopes such a decision automatically, using the request that
|
|
85
|
+
triggered it — a shell prompt becomes an approval for that prompt's command identifiers, an
|
|
86
|
+
MCP prompt an approval for that server and tool, a URL prompt an approval for that URL's
|
|
87
|
+
domain, and so on:
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
from copilot.generated.rpc import PermissionDecisionApproveForSession
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def on_permission_request(request, invocation):
|
|
94
|
+
# Scoped to `request` automatically; approves that kind of call for the whole session.
|
|
95
|
+
return PermissionDecisionApproveForSession()
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
The decision is only ever narrowed, never widened. When the prompt reports that it cannot
|
|
99
|
+
offer session-scoped approval (`can_offer_session_approval=False`), or the request kind has
|
|
100
|
+
no session-scoped approval at all (such as a `hook` prompt), the decision is downgraded to a
|
|
101
|
+
single-use approval and a warning is logged. Pass an explicit `approval=` or `domain=` when
|
|
102
|
+
you want to approve something other than the request being handled — decisions that already
|
|
103
|
+
specify a scope are forwarded unchanged.
|
|
104
|
+
|
|
78
105
|
### Deprecated: `on_function_approval`
|
|
79
106
|
|
|
80
107
|
The `on_function_approval` callback is **deprecated**. It still works (and is still enforced
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
Please install this package via pip:
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
|
-
pip install agent-framework-github-copilot
|
|
6
|
+
pip install agent-framework-github-copilot
|
|
7
7
|
```
|
|
8
8
|
|
|
9
9
|
## GitHub Copilot Agent
|
|
@@ -50,6 +50,34 @@ agent = GitHubCopilotAgent(
|
|
|
50
50
|
> Note: with the default (deny-all) permission handler, an `always_require` tool is denied
|
|
51
51
|
> unless you wire an approving `on_permission_request`.
|
|
52
52
|
|
|
53
|
+
### Approving for the rest of the session
|
|
54
|
+
|
|
55
|
+
`PermissionDecisionApproveForSession` scopes its approval with either an `approval` (tool
|
|
56
|
+
prompts) or a `domain` (URL prompts). Both are optional, so a bare
|
|
57
|
+
`PermissionDecisionApproveForSession()` carries no scope at all and the Copilot CLI cannot
|
|
58
|
+
interpret it.
|
|
59
|
+
|
|
60
|
+
`GitHubCopilotAgent` therefore scopes such a decision automatically, using the request that
|
|
61
|
+
triggered it — a shell prompt becomes an approval for that prompt's command identifiers, an
|
|
62
|
+
MCP prompt an approval for that server and tool, a URL prompt an approval for that URL's
|
|
63
|
+
domain, and so on:
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
from copilot.generated.rpc import PermissionDecisionApproveForSession
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def on_permission_request(request, invocation):
|
|
70
|
+
# Scoped to `request` automatically; approves that kind of call for the whole session.
|
|
71
|
+
return PermissionDecisionApproveForSession()
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
The decision is only ever narrowed, never widened. When the prompt reports that it cannot
|
|
75
|
+
offer session-scoped approval (`can_offer_session_approval=False`), or the request kind has
|
|
76
|
+
no session-scoped approval at all (such as a `hook` prompt), the decision is downgraded to a
|
|
77
|
+
single-use approval and a warning is logged. Pass an explicit `approval=` or `domain=` when
|
|
78
|
+
you want to approve something other than the request being handled — decisions that already
|
|
79
|
+
specify a scope are forwarded unchanged.
|
|
80
|
+
|
|
53
81
|
### Deprecated: `on_function_approval`
|
|
54
82
|
|
|
55
83
|
The `on_function_approval` callback is **deprecated**. It still works (and is still enforced
|
|
@@ -10,6 +10,7 @@ import sys
|
|
|
10
10
|
import warnings
|
|
11
11
|
from collections.abc import AsyncIterable, Awaitable, Callable, Mapping, MutableMapping, Sequence
|
|
12
12
|
from typing import Any, ClassVar, Generic, Literal, TypedDict, cast, overload
|
|
13
|
+
from urllib.parse import urlparse
|
|
13
14
|
|
|
14
15
|
from agent_framework import (
|
|
15
16
|
AgentMiddlewareLayer,
|
|
@@ -29,11 +30,18 @@ from agent_framework import (
|
|
|
29
30
|
normalize_messages,
|
|
30
31
|
)
|
|
31
32
|
from agent_framework._settings import load_settings
|
|
33
|
+
from agent_framework._telemetry import mark_feature_used
|
|
32
34
|
from agent_framework._tools import FunctionTool, ToolTypes
|
|
33
|
-
from agent_framework._types import
|
|
34
|
-
|
|
35
|
+
from agent_framework._types import (
|
|
36
|
+
AgentRunInputs,
|
|
37
|
+
_get_data_bytes_as_str, # pyright: ignore[reportPrivateUsage]
|
|
38
|
+
normalize_tools,
|
|
39
|
+
)
|
|
40
|
+
from agent_framework.exceptions import AgentException, ContentError
|
|
35
41
|
from agent_framework.observability import AgentTelemetryLayer
|
|
36
42
|
|
|
43
|
+
from ._feature_usage import FeatureIndex
|
|
44
|
+
|
|
37
45
|
if sys.version_info >= (3, 11):
|
|
38
46
|
from typing import Self # pragma: no cover
|
|
39
47
|
else:
|
|
@@ -45,8 +53,23 @@ else:
|
|
|
45
53
|
|
|
46
54
|
try:
|
|
47
55
|
from copilot import CopilotClient, CopilotSession, RuntimeConnection
|
|
48
|
-
from copilot.generated.rpc import
|
|
56
|
+
from copilot.generated.rpc import (
|
|
57
|
+
PermissionDecisionApproveForSession,
|
|
58
|
+
PermissionDecisionApproveForSessionApproval,
|
|
59
|
+
PermissionDecisionApproveForSessionApprovalCommands,
|
|
60
|
+
PermissionDecisionApproveForSessionApprovalCustomTool,
|
|
61
|
+
PermissionDecisionApproveForSessionApprovalExtensionManagement,
|
|
62
|
+
PermissionDecisionApproveForSessionApprovalExtensionPermissionAccess,
|
|
63
|
+
PermissionDecisionApproveForSessionApprovalMCP,
|
|
64
|
+
PermissionDecisionApproveForSessionApprovalMemory,
|
|
65
|
+
PermissionDecisionApproveForSessionApprovalRead,
|
|
66
|
+
PermissionDecisionApproveForSessionApprovalWrite,
|
|
67
|
+
PermissionDecisionApproveOnce,
|
|
68
|
+
PermissionDecisionUserNotAvailable,
|
|
69
|
+
)
|
|
49
70
|
from copilot.session import (
|
|
71
|
+
Attachment,
|
|
72
|
+
BlobAttachment,
|
|
50
73
|
MCPServerConfig,
|
|
51
74
|
PermissionRequestResult,
|
|
52
75
|
PreToolUseHandler,
|
|
@@ -55,7 +78,21 @@ try:
|
|
|
55
78
|
SessionHooks,
|
|
56
79
|
SystemMessageConfig,
|
|
57
80
|
)
|
|
58
|
-
from copilot.session_events import
|
|
81
|
+
from copilot.session_events import (
|
|
82
|
+
AssistantUsageData,
|
|
83
|
+
PermissionRequest,
|
|
84
|
+
PermissionRequestCustomTool,
|
|
85
|
+
PermissionRequestExtensionManagement,
|
|
86
|
+
PermissionRequestExtensionPermissionAccess,
|
|
87
|
+
PermissionRequestMcp,
|
|
88
|
+
PermissionRequestMemory,
|
|
89
|
+
PermissionRequestRead,
|
|
90
|
+
PermissionRequestShell,
|
|
91
|
+
PermissionRequestUrl,
|
|
92
|
+
PermissionRequestWrite,
|
|
93
|
+
SessionEvent,
|
|
94
|
+
SessionEventType,
|
|
95
|
+
)
|
|
59
96
|
from copilot.tools import Tool as CopilotTool
|
|
60
97
|
from copilot.tools import ToolInvocation, ToolResult
|
|
61
98
|
except ImportError as _copilot_import_error:
|
|
@@ -72,6 +109,9 @@ PermissionHandlerType = Callable[
|
|
|
72
109
|
]
|
|
73
110
|
"""Type for permission request handlers. Supports both sync and async callbacks."""
|
|
74
111
|
|
|
112
|
+
AsyncPermissionHandlerType = Callable[[PermissionRequest, dict[str, str]], "Awaitable[PermissionRequestResult]"]
|
|
113
|
+
"""Type for permission request handlers that are always asynchronous."""
|
|
114
|
+
|
|
75
115
|
|
|
76
116
|
FunctionApprovalCallback = Callable[[Content], "bool | Awaitable[bool]"]
|
|
77
117
|
"""Deprecated approval callback for ``FunctionTool`` instances declared with
|
|
@@ -131,6 +171,173 @@ def _deny_all_permissions(
|
|
|
131
171
|
return PermissionDecisionUserNotAvailable()
|
|
132
172
|
|
|
133
173
|
|
|
174
|
+
def _derive_session_approval(request: PermissionRequest) -> PermissionDecisionApproveForSessionApproval | None:
|
|
175
|
+
"""Build the session-scoped approval implied by ``request``.
|
|
176
|
+
|
|
177
|
+
``PermissionDecisionApproveForSession.approval`` describes *what* is being approved for
|
|
178
|
+
the remainder of the session. Its shape is dictated by the prompt that triggered it, so
|
|
179
|
+
it can be reconstructed from the request itself.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
request: The permission request the decision is responding to.
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
The approval covering ``request``, or ``None`` for request kinds that have no
|
|
186
|
+
session-scoped approval representation (such as ``hook`` prompts).
|
|
187
|
+
"""
|
|
188
|
+
if isinstance(request, PermissionRequestShell):
|
|
189
|
+
return PermissionDecisionApproveForSessionApprovalCommands(
|
|
190
|
+
command_identifiers=[command.identifier for command in request.commands]
|
|
191
|
+
)
|
|
192
|
+
if isinstance(request, PermissionRequestRead):
|
|
193
|
+
return PermissionDecisionApproveForSessionApprovalRead()
|
|
194
|
+
if isinstance(request, PermissionRequestWrite):
|
|
195
|
+
return PermissionDecisionApproveForSessionApprovalWrite()
|
|
196
|
+
if isinstance(request, PermissionRequestMcp):
|
|
197
|
+
return PermissionDecisionApproveForSessionApprovalMCP(
|
|
198
|
+
server_name=request.server_name, tool_name=request.tool_name
|
|
199
|
+
)
|
|
200
|
+
if isinstance(request, PermissionRequestCustomTool):
|
|
201
|
+
return PermissionDecisionApproveForSessionApprovalCustomTool(tool_name=request.tool_name)
|
|
202
|
+
if isinstance(request, PermissionRequestMemory):
|
|
203
|
+
return PermissionDecisionApproveForSessionApprovalMemory()
|
|
204
|
+
if isinstance(request, PermissionRequestExtensionManagement):
|
|
205
|
+
return PermissionDecisionApproveForSessionApprovalExtensionManagement(operation=request.operation)
|
|
206
|
+
if isinstance(request, PermissionRequestExtensionPermissionAccess):
|
|
207
|
+
return PermissionDecisionApproveForSessionApprovalExtensionPermissionAccess(
|
|
208
|
+
extension_name=request.extension_name
|
|
209
|
+
)
|
|
210
|
+
return None
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
# Characters the WHATWG URL parser (used by the Copilot CLI) treats specially for
|
|
214
|
+
# special-scheme URLs in ways that can move the authority boundary: backslashes are
|
|
215
|
+
# normalized to forward slashes, and tabs/newlines/carriage returns are stripped before
|
|
216
|
+
# parsing. Python's ``urlparse`` does none of this, so a URL containing any of them may
|
|
217
|
+
# resolve to a different host than the CLI actually contacts.
|
|
218
|
+
_WHATWG_AMBIGUOUS_URL_CHARS = ("\\", "\t", "\n", "\r")
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _derive_url_session_domain(url: str) -> str | None:
|
|
222
|
+
"""Return the domain to persist for a URL prompt, or ``None`` when it is unsafe to.
|
|
223
|
+
|
|
224
|
+
The persisted domain must match the host the Copilot CLI actually contacts, but the CLI
|
|
225
|
+
parses URLs with WHATWG semantics while this runs on Python's ``urlparse``. The two
|
|
226
|
+
disagree on crafted authorities -- a backslash before the ``@`` in
|
|
227
|
+
``https://example.com<backslash>@evil.com`` resolves to ``example.com`` under the CLI
|
|
228
|
+
but ``evil.com`` under ``urlparse`` -- so trusting ``urlparse`` here could persist a
|
|
229
|
+
session-wide approval for an unrelated, attacker-chosen domain.
|
|
230
|
+
|
|
231
|
+
To keep the "narrow, never widen" guarantee, the domain is only returned when the URL
|
|
232
|
+
contains none of the characters the two parsers handle differently; any ambiguity (or a
|
|
233
|
+
URL with no derivable host) yields ``None`` so the caller can approve the single request
|
|
234
|
+
without persisting a domain.
|
|
235
|
+
|
|
236
|
+
Args:
|
|
237
|
+
url: The URL from the permission request.
|
|
238
|
+
|
|
239
|
+
Returns:
|
|
240
|
+
The lower-cased host to approve for the session, or ``None`` when the URL is
|
|
241
|
+
parser-ambiguous or has no host.
|
|
242
|
+
"""
|
|
243
|
+
if any(char in url for char in _WHATWG_AMBIGUOUS_URL_CHARS):
|
|
244
|
+
return None
|
|
245
|
+
return urlparse(url).hostname or None
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def _normalize_permission_decision(
|
|
249
|
+
decision: PermissionRequestResult,
|
|
250
|
+
request: PermissionRequest,
|
|
251
|
+
) -> PermissionRequestResult:
|
|
252
|
+
"""Fill in the missing scope of an under-specified ``approve-for-session`` decision.
|
|
253
|
+
|
|
254
|
+
``PermissionDecisionApproveForSession`` carries an optional ``approval`` (tool prompts)
|
|
255
|
+
and an optional ``domain`` (URL prompts), so ``PermissionDecisionApproveForSession()``
|
|
256
|
+
is constructible with neither. That serializes to ``{"kind": "approve-for-session"}``,
|
|
257
|
+
which the Copilot CLI cannot interpret -- it crashes with ``Cannot read properties of
|
|
258
|
+
undefined (reading 'commandIdentifiers')``, taking the whole run down with it. This
|
|
259
|
+
reconstructs the intended scope from ``request``.
|
|
260
|
+
|
|
261
|
+
The decision is only ever narrowed, never widened: when the prompt does not offer
|
|
262
|
+
session-scoped approval, or the request kind has no session approval representation,
|
|
263
|
+
the decision is downgraded to a single-use approval.
|
|
264
|
+
|
|
265
|
+
Args:
|
|
266
|
+
decision: The decision returned by the caller's permission handler.
|
|
267
|
+
request: The permission request the decision is responding to.
|
|
268
|
+
|
|
269
|
+
Returns:
|
|
270
|
+
``decision`` unchanged unless it is an ``approve-for-session`` decision missing both
|
|
271
|
+
``approval`` and ``domain``, in which case an equivalent fully-scoped decision (or a
|
|
272
|
+
narrower single-use approval) is returned. The input is never mutated.
|
|
273
|
+
"""
|
|
274
|
+
if not isinstance(decision, PermissionDecisionApproveForSession):
|
|
275
|
+
return decision
|
|
276
|
+
if decision.approval is not None or decision.domain is not None:
|
|
277
|
+
return decision
|
|
278
|
+
|
|
279
|
+
try:
|
|
280
|
+
if isinstance(request, PermissionRequestUrl):
|
|
281
|
+
domain = _derive_url_session_domain(request.url)
|
|
282
|
+
if domain:
|
|
283
|
+
return PermissionDecisionApproveForSession(domain=domain)
|
|
284
|
+
logger.warning(
|
|
285
|
+
"Permission handler returned an unscoped 'approve-for-session' decision for a URL prompt, "
|
|
286
|
+
"but no unambiguous domain could be derived from '%s'. Approving this request only. Return "
|
|
287
|
+
"PermissionDecisionApproveForSession(domain=...) to approve a domain for the session.",
|
|
288
|
+
request.url,
|
|
289
|
+
)
|
|
290
|
+
return PermissionDecisionApproveOnce()
|
|
291
|
+
|
|
292
|
+
# Only shell and write prompts advertise this; other kinds always allow session approval.
|
|
293
|
+
if not getattr(request, "can_offer_session_approval", True):
|
|
294
|
+
logger.warning(
|
|
295
|
+
"Permission handler returned an 'approve-for-session' decision for a '%s' prompt that does not "
|
|
296
|
+
"offer session-scoped approval. Approving this request only.",
|
|
297
|
+
request.kind,
|
|
298
|
+
)
|
|
299
|
+
return PermissionDecisionApproveOnce()
|
|
300
|
+
|
|
301
|
+
approval = _derive_session_approval(request)
|
|
302
|
+
except Exception:
|
|
303
|
+
logger.exception(
|
|
304
|
+
"Failed to derive the session approval for a '%s' permission prompt. Approving this request only.",
|
|
305
|
+
getattr(request, "kind", "unknown"),
|
|
306
|
+
)
|
|
307
|
+
return PermissionDecisionApproveOnce()
|
|
308
|
+
|
|
309
|
+
if approval is None:
|
|
310
|
+
logger.warning(
|
|
311
|
+
"Permission handler returned an unscoped 'approve-for-session' decision for a '%s' prompt, which has "
|
|
312
|
+
"no session-scoped approval. Approving this request only.",
|
|
313
|
+
request.kind,
|
|
314
|
+
)
|
|
315
|
+
return PermissionDecisionApproveOnce()
|
|
316
|
+
return PermissionDecisionApproveForSession(approval=approval)
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def _with_normalized_permission_decisions(handler: PermissionHandlerType) -> AsyncPermissionHandlerType:
|
|
320
|
+
"""Wrap a permission handler so its decisions are normalized before reaching the SDK.
|
|
321
|
+
|
|
322
|
+
Exceptions raised by ``handler`` deliberately propagate: the SDK already catches them
|
|
323
|
+
and denies the request, and preserving that keeps the secure-by-default behavior.
|
|
324
|
+
|
|
325
|
+
Args:
|
|
326
|
+
handler: The caller-supplied permission handler. May be sync or async.
|
|
327
|
+
|
|
328
|
+
Returns:
|
|
329
|
+
An async handler delegating to ``handler`` and normalizing its result.
|
|
330
|
+
"""
|
|
331
|
+
|
|
332
|
+
async def normalized_handler(request: PermissionRequest, invocation: dict[str, str]) -> PermissionRequestResult:
|
|
333
|
+
result = handler(request, invocation)
|
|
334
|
+
if inspect.isawaitable(result):
|
|
335
|
+
result = await result
|
|
336
|
+
return _normalize_permission_decision(result, request)
|
|
337
|
+
|
|
338
|
+
return normalized_handler
|
|
339
|
+
|
|
340
|
+
|
|
134
341
|
class GitHubCopilotSettings(TypedDict, total=False):
|
|
135
342
|
"""GitHub Copilot model settings.
|
|
136
343
|
|
|
@@ -656,10 +863,12 @@ class RawGitHubCopilotAgent(BaseAgent, Generic[OptionsT]):
|
|
|
656
863
|
prompt = "\n".join([message.text for message in context_messages])
|
|
657
864
|
if session_context.instructions:
|
|
658
865
|
prompt = "\n".join(session_context.instructions) + "\n" + prompt
|
|
866
|
+
attachments = self._prepare_attachments_for_copilot(context_messages)
|
|
659
867
|
|
|
660
868
|
unsubscribe = copilot_session.on(usage_event_handler)
|
|
661
869
|
try:
|
|
662
|
-
|
|
870
|
+
mark_feature_used(FeatureIndex.GITHUB_COPILOT)
|
|
871
|
+
response_event = await copilot_session.send_and_wait(prompt, attachments=attachments, timeout=timeout)
|
|
663
872
|
except Exception as ex:
|
|
664
873
|
raise AgentException(f"GitHub Copilot request failed: {ex}") from ex
|
|
665
874
|
finally:
|
|
@@ -762,6 +971,7 @@ class RawGitHubCopilotAgent(BaseAgent, Generic[OptionsT]):
|
|
|
762
971
|
prompt = "\n".join([message.text for message in context_messages])
|
|
763
972
|
if session_context.instructions:
|
|
764
973
|
prompt = "\n".join(session_context.instructions) + "\n" + prompt
|
|
974
|
+
attachments = self._prepare_attachments_for_copilot(context_messages)
|
|
765
975
|
|
|
766
976
|
queue: asyncio.Queue[AgentResponseUpdate | Exception | None] = asyncio.Queue()
|
|
767
977
|
|
|
@@ -844,7 +1054,8 @@ class RawGitHubCopilotAgent(BaseAgent, Generic[OptionsT]):
|
|
|
844
1054
|
unsubscribe = copilot_session.on(event_handler)
|
|
845
1055
|
|
|
846
1056
|
try:
|
|
847
|
-
|
|
1057
|
+
mark_feature_used(FeatureIndex.GITHUB_COPILOT)
|
|
1058
|
+
await copilot_session.send(prompt, attachments=attachments)
|
|
848
1059
|
|
|
849
1060
|
while (item := await queue.get()) is not None:
|
|
850
1061
|
if isinstance(item, Exception):
|
|
@@ -916,6 +1127,56 @@ class RawGitHubCopilotAgent(BaseAgent, Generic[OptionsT]):
|
|
|
916
1127
|
elif opts_system_message is not None:
|
|
917
1128
|
opts["system_message"] = opts_system_message
|
|
918
1129
|
|
|
1130
|
+
@staticmethod
|
|
1131
|
+
def _prepare_attachments_for_copilot(messages: Sequence[Message]) -> list[Attachment] | None:
|
|
1132
|
+
"""Convert inline binary message content into Copilot SDK attachments.
|
|
1133
|
+
|
|
1134
|
+
Scans the outgoing messages for ``data`` content (binary payloads such as
|
|
1135
|
+
images or documents carried as base64 data URIs) and maps each one to an
|
|
1136
|
+
inline ``blob`` attachment understood by the Copilot SDK.
|
|
1137
|
+
|
|
1138
|
+
Only base64 ``data:`` content is forwarded as an attachment. Other content
|
|
1139
|
+
is not turned into an attachment: text content is already carried in the
|
|
1140
|
+
prompt, while remote URIs (for example ``https://`` links) and malformed or
|
|
1141
|
+
non-base64 ``data:`` URIs are skipped -- they are neither attached nor added
|
|
1142
|
+
to the prompt.
|
|
1143
|
+
|
|
1144
|
+
Args:
|
|
1145
|
+
messages: The messages being sent to the Copilot session.
|
|
1146
|
+
|
|
1147
|
+
Returns:
|
|
1148
|
+
A list of Copilot ``Attachment`` objects, or ``None`` when the messages
|
|
1149
|
+
contain no attachable binary content.
|
|
1150
|
+
"""
|
|
1151
|
+
attachments: list[Attachment] = []
|
|
1152
|
+
for message in messages:
|
|
1153
|
+
for content in message.contents:
|
|
1154
|
+
if content.type != "data":
|
|
1155
|
+
continue
|
|
1156
|
+
try:
|
|
1157
|
+
data_str = _get_data_bytes_as_str(content)
|
|
1158
|
+
except ContentError:
|
|
1159
|
+
logger.warning(
|
|
1160
|
+
"Skipping GitHub Copilot attachment with an unsupported data URI; "
|
|
1161
|
+
"only base64-encoded 'data:' URIs can be forwarded as attachments."
|
|
1162
|
+
)
|
|
1163
|
+
continue
|
|
1164
|
+
if not data_str:
|
|
1165
|
+
continue
|
|
1166
|
+
if not content.media_type:
|
|
1167
|
+
logger.warning(
|
|
1168
|
+
"Dropping GitHub Copilot attachment with no media type; the Copilot SDK "
|
|
1169
|
+
"requires a MIME type for inline binary content."
|
|
1170
|
+
)
|
|
1171
|
+
continue
|
|
1172
|
+
blob: BlobAttachment = {
|
|
1173
|
+
"type": "blob",
|
|
1174
|
+
"data": data_str,
|
|
1175
|
+
"mimeType": content.media_type,
|
|
1176
|
+
}
|
|
1177
|
+
attachments.append(blob)
|
|
1178
|
+
return attachments or None
|
|
1179
|
+
|
|
919
1180
|
def _prepare_tools(
|
|
920
1181
|
self,
|
|
921
1182
|
tools: Sequence[ToolTypes | CopilotTool],
|
|
@@ -1138,9 +1399,10 @@ class RawGitHubCopilotAgent(BaseAgent, Generic[OptionsT]):
|
|
|
1138
1399
|
the Copilot SDK, so any ``create_session`` parameter is supported without a
|
|
1139
1400
|
dedicated mapping here (an unknown name surfaces as a ``TypeError`` from the
|
|
1140
1401
|
SDK). A few keys are handled specially because they need a secure default
|
|
1141
|
-
(``on_permission_request`` defaults to denying all requests
|
|
1142
|
-
``
|
|
1143
|
-
|
|
1402
|
+
(``on_permission_request`` defaults to denying all requests, and is wrapped so
|
|
1403
|
+
under-specified ``approve-for-session`` decisions are scoped to the request that
|
|
1404
|
+
triggered them) or transforming: ``tools`` are merged with the agent's tools and
|
|
1405
|
+
converted to SDK tools, and approval callbacks are turned into ``hooks``.
|
|
1144
1406
|
|
|
1145
1407
|
Args:
|
|
1146
1408
|
streaming: Whether to enable streaming for the session.
|
|
@@ -1164,7 +1426,7 @@ class RawGitHubCopilotAgent(BaseAgent, Generic[OptionsT]):
|
|
|
1164
1426
|
# back to the resolved setting (which carries the default_options / env model).
|
|
1165
1427
|
if not kwargs.get("model"):
|
|
1166
1428
|
kwargs["model"] = self._settings.get("model") or None
|
|
1167
|
-
kwargs["on_permission_request"] = (
|
|
1429
|
+
kwargs["on_permission_request"] = _with_normalized_permission_decisions(
|
|
1168
1430
|
opts.get("on_permission_request") or self._permission_handler or _deny_all_permissions
|
|
1169
1431
|
)
|
|
1170
1432
|
kwargs["hooks"] = self._build_session_hooks(all_tools, kwargs)
|
{agent_framework_github_copilot-1.0.0rc4 → agent_framework_github_copilot-1.0.2}/pyproject.toml
RENAMED
|
@@ -3,8 +3,8 @@ name = "agent-framework-github-copilot"
|
|
|
3
3
|
description = "GitHub Copilot integration for Microsoft Agent Framework."
|
|
4
4
|
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
|
|
5
5
|
readme = "README.md"
|
|
6
|
-
requires-python = ">=3.
|
|
7
|
-
version = "1.0.
|
|
6
|
+
requires-python = ">=3.11"
|
|
7
|
+
version = "1.0.2"
|
|
8
8
|
license-files = ["LICENSE"]
|
|
9
9
|
urls.homepage = "https://aka.ms/agent-framework"
|
|
10
10
|
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
|
|
@@ -12,10 +12,9 @@ urls.release_notes = "https://github.com/microsoft/agent-framework/releases?q=ta
|
|
|
12
12
|
urls.issues = "https://github.com/microsoft/agent-framework/issues"
|
|
13
13
|
classifiers = [
|
|
14
14
|
"License :: OSI Approved :: MIT License",
|
|
15
|
-
"Development Status ::
|
|
15
|
+
"Development Status :: 5 - Production/Stable",
|
|
16
16
|
"Intended Audience :: Developers",
|
|
17
17
|
"Programming Language :: Python :: 3",
|
|
18
|
-
"Programming Language :: Python :: 3.10",
|
|
19
18
|
"Programming Language :: Python :: 3.11",
|
|
20
19
|
"Programming Language :: Python :: 3.12",
|
|
21
20
|
"Programming Language :: Python :: 3.13",
|
|
@@ -23,7 +22,7 @@ classifiers = [
|
|
|
23
22
|
"Typing :: Typed",
|
|
24
23
|
]
|
|
25
24
|
dependencies = [
|
|
26
|
-
"agent-framework-core>=1.
|
|
25
|
+
"agent-framework-core>=1.13.0,<2",
|
|
27
26
|
"github-copilot-sdk==1.0.2; python_version >= '3.11'",
|
|
28
27
|
]
|
|
29
28
|
|
|
File without changes
|