avenza 1.0.0__py3-none-any.whl
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.
- avenza/__init__.py +30 -0
- avenza/_buffer.py +52 -0
- avenza/_context.py +30 -0
- avenza/_instrument/__init__.py +17 -0
- avenza/_instrument/anthropic_patch.py +77 -0
- avenza/_instrument/gemini_patch.py +76 -0
- avenza/_instrument/openai_patch.py +77 -0
- avenza/agent.py +180 -0
- avenza/cli.py +189 -0
- avenza/client.py +123 -0
- avenza/exceptions.py +17 -0
- avenza/integrations/__init__.py +1 -0
- avenza/integrations/crewai.py +13 -0
- avenza/integrations/langchain.py +100 -0
- avenza/py.typed +0 -0
- avenza/run.py +207 -0
- avenza/testing.py +122 -0
- avenza-1.0.0.dist-info/METADATA +143 -0
- avenza-1.0.0.dist-info/RECORD +22 -0
- avenza-1.0.0.dist-info/WHEEL +5 -0
- avenza-1.0.0.dist-info/entry_points.txt +2 -0
- avenza-1.0.0.dist-info/top_level.txt +1 -0
avenza/__init__.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Avenza Python SDK
|
|
3
|
+
|
|
4
|
+
Instrument AI agents in one line. Cost, value, and SLO tracking — automatically.
|
|
5
|
+
|
|
6
|
+
Quickstart:
|
|
7
|
+
from avenza import Agent
|
|
8
|
+
|
|
9
|
+
agent = Agent(name='Invoice Bot', risk_tier='T2')
|
|
10
|
+
|
|
11
|
+
with agent.run() as run:
|
|
12
|
+
result = process(data)
|
|
13
|
+
run.success = result.ok
|
|
14
|
+
run.log_value('task_completed', quantity=1, unit_value_usd=1.50)
|
|
15
|
+
|
|
16
|
+
Auto-instrumentation is active by default — if you're using the official Anthropic,
|
|
17
|
+
OpenAI, or Gemini client, token usage is captured without any additional code.
|
|
18
|
+
"""
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from .agent import Agent
|
|
22
|
+
from .exceptions import AvenzaConfigError, AvenzaError
|
|
23
|
+
|
|
24
|
+
try:
|
|
25
|
+
from importlib.metadata import version as _version
|
|
26
|
+
__version__: str = _version("avenza")
|
|
27
|
+
except Exception:
|
|
28
|
+
__version__ = "dev"
|
|
29
|
+
|
|
30
|
+
__all__ = ["Agent", "AvenzaError", "AvenzaConfigError", "__version__"]
|
avenza/_buffer.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Offline buffer — persists failed sends to .avenza_buffer.jsonl and retries
|
|
3
|
+
on next SDK init. Designed for intermittent-connectivity environments.
|
|
4
|
+
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import logging
|
|
9
|
+
import os
|
|
10
|
+
from typing import TYPE_CHECKING, Any
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from .client import AvenzaClient
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
_DEFAULT_PATH = ".avenza_buffer.jsonl"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class OfflineBuffer:
|
|
21
|
+
def __init__(self, path: str = _DEFAULT_PATH) -> None:
|
|
22
|
+
self._path = path
|
|
23
|
+
|
|
24
|
+
def save(self, path: str, payload: dict[str, Any]) -> None:
|
|
25
|
+
try:
|
|
26
|
+
with open(self._path, "a", encoding="utf-8") as f:
|
|
27
|
+
f.write(json.dumps({"path": path, "payload": payload}) + "\n")
|
|
28
|
+
except OSError:
|
|
29
|
+
pass # Read-only FS or permission error — drop silently
|
|
30
|
+
|
|
31
|
+
def flush(self, client: "AvenzaClient") -> None:
|
|
32
|
+
if not os.path.exists(self._path):
|
|
33
|
+
return
|
|
34
|
+
try:
|
|
35
|
+
with open(self._path, encoding="utf-8") as f:
|
|
36
|
+
lines = f.readlines()
|
|
37
|
+
os.remove(self._path)
|
|
38
|
+
except OSError:
|
|
39
|
+
return
|
|
40
|
+
|
|
41
|
+
flushed = 0
|
|
42
|
+
for line in lines:
|
|
43
|
+
try:
|
|
44
|
+
entry = json.loads(line.strip())
|
|
45
|
+
if entry.get("path") and entry.get("payload") is not None:
|
|
46
|
+
client.post_async(entry["path"], entry["payload"])
|
|
47
|
+
flushed += 1
|
|
48
|
+
except (json.JSONDecodeError, KeyError):
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
if flushed:
|
|
52
|
+
logger.info("avenza: flushed %d buffered run(s) from offline buffer", flushed)
|
avenza/_context.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""
|
|
2
|
+
contextvars-based active-run tracking.
|
|
3
|
+
|
|
4
|
+
Each OS thread and each asyncio task gets its own isolated view of the current
|
|
5
|
+
run, so token capture is always attributed to the correct run even under
|
|
6
|
+
concurrent async tasks or multi-threaded agents.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import contextvars
|
|
11
|
+
from typing import TYPE_CHECKING, Optional
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from .run import RunContext
|
|
15
|
+
|
|
16
|
+
_current_run: contextvars.ContextVar[Optional["RunContext"]] = contextvars.ContextVar(
|
|
17
|
+
"avenza_current_run", default=None
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def get_current_run() -> Optional["RunContext"]:
|
|
22
|
+
return _current_run.get()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def set_current_run(run: "RunContext") -> contextvars.Token:
|
|
26
|
+
return _current_run.set(run)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def reset_current_run(token: contextvars.Token) -> None:
|
|
30
|
+
_current_run.reset(token)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Auto-instrumentation dispatcher.
|
|
3
|
+
|
|
4
|
+
patch_all() tries each provider's patch. If the provider's SDK is not
|
|
5
|
+
installed, the patch is silently skipped — no error, no warning.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def patch_all() -> None:
|
|
11
|
+
from .anthropic_patch import patch_anthropic
|
|
12
|
+
from .openai_patch import patch_openai
|
|
13
|
+
from .gemini_patch import patch_gemini
|
|
14
|
+
|
|
15
|
+
patch_anthropic()
|
|
16
|
+
patch_openai()
|
|
17
|
+
patch_gemini()
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Patches Anthropic's official Python client to capture token usage automatically.
|
|
3
|
+
Wraps both the sync and async message creation methods.
|
|
4
|
+
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import functools
|
|
8
|
+
import logging
|
|
9
|
+
|
|
10
|
+
from .._context import get_current_run
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
_patched = False
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def patch_anthropic() -> None:
|
|
18
|
+
global _patched
|
|
19
|
+
if _patched:
|
|
20
|
+
return
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
import anthropic
|
|
24
|
+
except ImportError:
|
|
25
|
+
return # SDK not installed — nothing to patch, no error
|
|
26
|
+
|
|
27
|
+
try:
|
|
28
|
+
_patch_sync(anthropic)
|
|
29
|
+
_patch_async(anthropic)
|
|
30
|
+
_patched = True
|
|
31
|
+
logger.debug("avenza: anthropic auto-instrumentation active")
|
|
32
|
+
except Exception as exc:
|
|
33
|
+
logger.debug("avenza: anthropic patch failed (non-fatal) — %s", exc)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _patch_sync(anthropic: object) -> None:
|
|
37
|
+
messages_cls = anthropic.resources.messages.Messages # type: ignore[attr-defined]
|
|
38
|
+
original = messages_cls.create
|
|
39
|
+
|
|
40
|
+
@functools.wraps(original)
|
|
41
|
+
def wrapped(self: object, *args: object, **kwargs: object) -> object:
|
|
42
|
+
response = original(self, *args, **kwargs)
|
|
43
|
+
run = get_current_run()
|
|
44
|
+
if run is not None and hasattr(response, "usage"):
|
|
45
|
+
run._record_auto_tokens(
|
|
46
|
+
provider="anthropic",
|
|
47
|
+
model=str(kwargs.get("model", "unknown")),
|
|
48
|
+
input_tokens=getattr(response.usage, "input_tokens", 0),
|
|
49
|
+
output_tokens=getattr(response.usage, "output_tokens", 0),
|
|
50
|
+
)
|
|
51
|
+
return response
|
|
52
|
+
|
|
53
|
+
messages_cls.create = wrapped
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _patch_async(anthropic: object) -> None:
|
|
57
|
+
try:
|
|
58
|
+
async_cls = anthropic.resources.messages.AsyncMessages # type: ignore[attr-defined]
|
|
59
|
+
except AttributeError:
|
|
60
|
+
return # older version — no async client
|
|
61
|
+
|
|
62
|
+
original_async = async_cls.create
|
|
63
|
+
|
|
64
|
+
@functools.wraps(original_async)
|
|
65
|
+
async def wrapped_async(self: object, *args: object, **kwargs: object) -> object:
|
|
66
|
+
response = await original_async(self, *args, **kwargs)
|
|
67
|
+
run = get_current_run()
|
|
68
|
+
if run is not None and hasattr(response, "usage"):
|
|
69
|
+
run._record_auto_tokens(
|
|
70
|
+
provider="anthropic",
|
|
71
|
+
model=str(kwargs.get("model", "unknown")),
|
|
72
|
+
input_tokens=getattr(response.usage, "input_tokens", 0),
|
|
73
|
+
output_tokens=getattr(response.usage, "output_tokens", 0),
|
|
74
|
+
)
|
|
75
|
+
return response
|
|
76
|
+
|
|
77
|
+
async_cls.create = wrapped_async
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Patches Google's Generative AI client to capture token usage automatically.
|
|
3
|
+
Works with both google-generativeai (genai) and google-cloud-aiplatform clients.
|
|
4
|
+
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import functools
|
|
8
|
+
import logging
|
|
9
|
+
|
|
10
|
+
from .._context import get_current_run
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
_patched = False
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def patch_gemini() -> None:
|
|
18
|
+
global _patched
|
|
19
|
+
if _patched:
|
|
20
|
+
return
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
_patch_genai()
|
|
24
|
+
_patched = True
|
|
25
|
+
except ImportError:
|
|
26
|
+
pass # Neither SDK installed — skip silently
|
|
27
|
+
except Exception as exc:
|
|
28
|
+
logger.debug("avenza: gemini patch failed (non-fatal) — %s", exc)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _patch_genai() -> None:
|
|
32
|
+
import google.generativeai as genai # type: ignore[import]
|
|
33
|
+
model_cls = genai.GenerativeModel
|
|
34
|
+
|
|
35
|
+
original_sync = model_cls.generate_content
|
|
36
|
+
|
|
37
|
+
@functools.wraps(original_sync)
|
|
38
|
+
def wrapped_sync(self: object, *args: object, **kwargs: object) -> object:
|
|
39
|
+
response = original_sync(self, *args, **kwargs)
|
|
40
|
+
run = get_current_run()
|
|
41
|
+
if run is not None:
|
|
42
|
+
_capture_gemini_usage(run, response, getattr(self, "model_name", "gemini"))
|
|
43
|
+
return response
|
|
44
|
+
|
|
45
|
+
model_cls.generate_content = wrapped_sync
|
|
46
|
+
logger.debug("avenza: gemini (google-generativeai) auto-instrumentation active")
|
|
47
|
+
|
|
48
|
+
# Async variant
|
|
49
|
+
if hasattr(model_cls, "generate_content_async"):
|
|
50
|
+
original_async = model_cls.generate_content_async
|
|
51
|
+
|
|
52
|
+
@functools.wraps(original_async)
|
|
53
|
+
async def wrapped_async(self: object, *args: object, **kwargs: object) -> object:
|
|
54
|
+
response = await original_async(self, *args, **kwargs)
|
|
55
|
+
run = get_current_run()
|
|
56
|
+
if run is not None:
|
|
57
|
+
_capture_gemini_usage(run, response, getattr(self, "model_name", "gemini"))
|
|
58
|
+
return response
|
|
59
|
+
|
|
60
|
+
model_cls.generate_content_async = wrapped_async
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _capture_gemini_usage(run: object, response: object, model_name: str) -> None:
|
|
64
|
+
# google-generativeai stores token counts in response.usage_metadata
|
|
65
|
+
usage = getattr(response, "usage_metadata", None)
|
|
66
|
+
if usage is None:
|
|
67
|
+
return
|
|
68
|
+
input_tokens = getattr(usage, "prompt_token_count", 0) or 0
|
|
69
|
+
output_tokens = getattr(usage, "candidates_token_count", 0) or 0
|
|
70
|
+
if input_tokens or output_tokens:
|
|
71
|
+
run._record_auto_tokens( # type: ignore[union-attr]
|
|
72
|
+
provider="google",
|
|
73
|
+
model=model_name,
|
|
74
|
+
input_tokens=input_tokens,
|
|
75
|
+
output_tokens=output_tokens,
|
|
76
|
+
)
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Patches OpenAI's official Python client to capture token usage automatically.
|
|
3
|
+
Handles both sync (Completions.create) and async (AsyncCompletions.create).
|
|
4
|
+
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import functools
|
|
8
|
+
import logging
|
|
9
|
+
|
|
10
|
+
from .._context import get_current_run
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
_patched = False
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def patch_openai() -> None:
|
|
18
|
+
global _patched
|
|
19
|
+
if _patched:
|
|
20
|
+
return
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
import openai
|
|
24
|
+
except ImportError:
|
|
25
|
+
return
|
|
26
|
+
|
|
27
|
+
try:
|
|
28
|
+
_patch_sync(openai)
|
|
29
|
+
_patch_async(openai)
|
|
30
|
+
_patched = True
|
|
31
|
+
logger.debug("avenza: openai auto-instrumentation active")
|
|
32
|
+
except Exception as exc:
|
|
33
|
+
logger.debug("avenza: openai patch failed (non-fatal) — %s", exc)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _patch_sync(openai: object) -> None:
|
|
37
|
+
completions_cls = openai.resources.chat.completions.Completions # type: ignore[attr-defined]
|
|
38
|
+
original = completions_cls.create
|
|
39
|
+
|
|
40
|
+
@functools.wraps(original)
|
|
41
|
+
def wrapped(self: object, *args: object, **kwargs: object) -> object:
|
|
42
|
+
response = original(self, *args, **kwargs)
|
|
43
|
+
run = get_current_run()
|
|
44
|
+
if run is not None and hasattr(response, "usage") and response.usage is not None:
|
|
45
|
+
run._record_auto_tokens(
|
|
46
|
+
provider="openai",
|
|
47
|
+
model=str(kwargs.get("model", getattr(response, "model", "unknown"))),
|
|
48
|
+
input_tokens=getattr(response.usage, "prompt_tokens", 0),
|
|
49
|
+
output_tokens=getattr(response.usage, "completion_tokens", 0),
|
|
50
|
+
)
|
|
51
|
+
return response
|
|
52
|
+
|
|
53
|
+
completions_cls.create = wrapped
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _patch_async(openai: object) -> None:
|
|
57
|
+
try:
|
|
58
|
+
async_cls = openai.resources.chat.completions.AsyncCompletions # type: ignore[attr-defined]
|
|
59
|
+
except AttributeError:
|
|
60
|
+
return
|
|
61
|
+
|
|
62
|
+
original_async = async_cls.create
|
|
63
|
+
|
|
64
|
+
@functools.wraps(original_async)
|
|
65
|
+
async def wrapped_async(self: object, *args: object, **kwargs: object) -> object:
|
|
66
|
+
response = await original_async(self, *args, **kwargs)
|
|
67
|
+
run = get_current_run()
|
|
68
|
+
if run is not None and hasattr(response, "usage") and response.usage is not None:
|
|
69
|
+
run._record_auto_tokens(
|
|
70
|
+
provider="openai",
|
|
71
|
+
model=str(kwargs.get("model", getattr(response, "model", "unknown"))),
|
|
72
|
+
input_tokens=getattr(response.usage, "prompt_tokens", 0),
|
|
73
|
+
output_tokens=getattr(response.usage, "completion_tokens", 0),
|
|
74
|
+
)
|
|
75
|
+
return response
|
|
76
|
+
|
|
77
|
+
async_cls.create = wrapped_async
|
avenza/agent.py
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Agent — the entry point for instrumenting a single AI agent.
|
|
3
|
+
|
|
4
|
+
agent = Agent(name='Invoice Bot', risk_tier='T2')
|
|
5
|
+
|
|
6
|
+
with agent.run() as run:
|
|
7
|
+
result = do_work()
|
|
8
|
+
run.success = result.ok
|
|
9
|
+
run.log_value('task_completed', quantity=1, unit_value_usd=1.50)
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import logging
|
|
14
|
+
import os
|
|
15
|
+
from typing import Any, Optional
|
|
16
|
+
|
|
17
|
+
from .client import AvenzaClient
|
|
18
|
+
from .exceptions import AvenzaConfigError
|
|
19
|
+
from .run import AgentRefGetter, RunContext
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Agent:
|
|
25
|
+
"""
|
|
26
|
+
Represents a single AI agent registered in Avenza.
|
|
27
|
+
|
|
28
|
+
Parameters
|
|
29
|
+
----------
|
|
30
|
+
name : Agent display name — used to find-or-create on the platform.
|
|
31
|
+
risk_tier : 'T1' (autonomous), 'T2' (approve-first), or 'T3' (assist-only).
|
|
32
|
+
api_key : Bearer token with sdk scope. Falls back to AVENZA_API_KEY env var.
|
|
33
|
+
model : LLM model identifier — used for cost table lookup.
|
|
34
|
+
owner : Email of the accountable human. Defaults to API key's owning user.
|
|
35
|
+
auto_instrument : Patch official LLM client libraries automatically (default True).
|
|
36
|
+
offline_buffer : Queue failed sends to local disk and retry on next init (default True).
|
|
37
|
+
base_url : Override for self-hosted deployments.
|
|
38
|
+
language : Runtime language tag sent in registration heartbeat.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
*,
|
|
44
|
+
name: str,
|
|
45
|
+
risk_tier: str = "T1",
|
|
46
|
+
api_key: Optional[str] = None,
|
|
47
|
+
model: Optional[str] = None,
|
|
48
|
+
owner: Optional[str] = None,
|
|
49
|
+
auto_instrument: bool = True,
|
|
50
|
+
offline_buffer: bool = True,
|
|
51
|
+
base_url: Optional[str] = None,
|
|
52
|
+
language: str = "python",
|
|
53
|
+
) -> None:
|
|
54
|
+
resolved_key = api_key or os.environ.get("AVENZA_API_KEY")
|
|
55
|
+
if not resolved_key:
|
|
56
|
+
raise AvenzaConfigError(
|
|
57
|
+
"No API key provided. Set AVENZA_API_KEY in your environment "
|
|
58
|
+
"or pass api_key= to Agent()."
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
self.name = name
|
|
62
|
+
self.risk_tier = risk_tier
|
|
63
|
+
self._model = model
|
|
64
|
+
|
|
65
|
+
import importlib.metadata as _meta
|
|
66
|
+
try:
|
|
67
|
+
sdk_version = _meta.version("avenza")
|
|
68
|
+
except Exception:
|
|
69
|
+
sdk_version = "dev"
|
|
70
|
+
|
|
71
|
+
self._client = AvenzaClient(
|
|
72
|
+
api_key=resolved_key,
|
|
73
|
+
base_url=base_url or os.environ.get("AVENZA_URL"),
|
|
74
|
+
offline_buffer=offline_buffer,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
self._ref_getter = AgentRefGetter()
|
|
78
|
+
|
|
79
|
+
# Kick off agent registration on the background thread
|
|
80
|
+
self._client.post_async(
|
|
81
|
+
"/agents/register",
|
|
82
|
+
{
|
|
83
|
+
"agent_ref": None, # server assigns; name is the match key
|
|
84
|
+
"name": name,
|
|
85
|
+
"risk_tier": risk_tier,
|
|
86
|
+
"sdk_version": sdk_version,
|
|
87
|
+
"language": language,
|
|
88
|
+
**({"model": model} if model else {}),
|
|
89
|
+
**({"owner": owner} if owner else {}),
|
|
90
|
+
},
|
|
91
|
+
)
|
|
92
|
+
# Registration response will never come back through post_async.
|
|
93
|
+
# We do a synchronous registration call to get the agent_ref.
|
|
94
|
+
# This is the one-time network cost per Agent() instantiation.
|
|
95
|
+
self._register_sync(name, risk_tier, model, owner, sdk_version, language)
|
|
96
|
+
|
|
97
|
+
if auto_instrument:
|
|
98
|
+
from ._instrument import patch_all
|
|
99
|
+
patch_all()
|
|
100
|
+
|
|
101
|
+
def run(self, run_ref: Optional[str] = None) -> RunContext:
|
|
102
|
+
"""Return a RunContext to wrap a single agent execution."""
|
|
103
|
+
return RunContext(
|
|
104
|
+
client=self._client,
|
|
105
|
+
agent_ref_getter=self._ref_getter,
|
|
106
|
+
run_ref=run_ref,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
def task(
|
|
110
|
+
self,
|
|
111
|
+
*,
|
|
112
|
+
value_type: Optional[str] = None,
|
|
113
|
+
unit_value_usd: float = 0.0,
|
|
114
|
+
quantity: float = 1.0,
|
|
115
|
+
) -> Any:
|
|
116
|
+
"""Decorator that wraps a function in a run automatically."""
|
|
117
|
+
import functools
|
|
118
|
+
|
|
119
|
+
def decorator(fn: Any) -> Any:
|
|
120
|
+
@functools.wraps(fn)
|
|
121
|
+
def sync_wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
122
|
+
with self.run() as run:
|
|
123
|
+
try:
|
|
124
|
+
result = fn(*args, **kwargs)
|
|
125
|
+
run.success = bool(result)
|
|
126
|
+
if value_type:
|
|
127
|
+
run.log_value(value_type, quantity=quantity, unit_value_usd=unit_value_usd)
|
|
128
|
+
return result
|
|
129
|
+
except Exception:
|
|
130
|
+
run.success = False
|
|
131
|
+
raise
|
|
132
|
+
|
|
133
|
+
@functools.wraps(fn)
|
|
134
|
+
async def async_wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
135
|
+
async with self.run() as run:
|
|
136
|
+
try:
|
|
137
|
+
result = await fn(*args, **kwargs)
|
|
138
|
+
run.success = bool(result)
|
|
139
|
+
if value_type:
|
|
140
|
+
run.log_value(value_type, quantity=quantity, unit_value_usd=unit_value_usd)
|
|
141
|
+
return result
|
|
142
|
+
except Exception:
|
|
143
|
+
run.success = False
|
|
144
|
+
raise
|
|
145
|
+
|
|
146
|
+
import asyncio as _asyncio
|
|
147
|
+
if _asyncio.iscoroutinefunction(fn):
|
|
148
|
+
return async_wrapper
|
|
149
|
+
return sync_wrapper
|
|
150
|
+
|
|
151
|
+
return decorator
|
|
152
|
+
|
|
153
|
+
def _register_sync(
|
|
154
|
+
self,
|
|
155
|
+
name: str,
|
|
156
|
+
risk_tier: str,
|
|
157
|
+
model: Optional[str],
|
|
158
|
+
owner: Optional[str],
|
|
159
|
+
sdk_version: str,
|
|
160
|
+
language: str,
|
|
161
|
+
) -> None:
|
|
162
|
+
try:
|
|
163
|
+
data = self._client.post_sync(
|
|
164
|
+
"/agents/register",
|
|
165
|
+
{
|
|
166
|
+
"agent_ref": name, # used as a hint; server may assign differently
|
|
167
|
+
"name": name,
|
|
168
|
+
"risk_tier": risk_tier,
|
|
169
|
+
"sdk_version": sdk_version,
|
|
170
|
+
"language": language,
|
|
171
|
+
**({"model": model} if model else {}),
|
|
172
|
+
**({"owner": owner} if owner else {}),
|
|
173
|
+
},
|
|
174
|
+
)
|
|
175
|
+
agent_ref: str = data.get("agent_ref", name)
|
|
176
|
+
self._ref_getter.set(agent_ref)
|
|
177
|
+
logger.debug("avenza: registered agent=%s ref=%s", name, agent_ref)
|
|
178
|
+
except Exception as exc:
|
|
179
|
+
logger.debug("avenza: registration failed, will retry — %s", exc)
|
|
180
|
+
self._ref_getter.set(name.upper().replace(" ", "-")[:20])
|