aops 0.1.0__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.
aops-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Park Kibum
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
aops-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,15 @@
1
+ Metadata-Version: 2.4
2
+ Name: aops
3
+ Version: 0.1.0
4
+ Summary: LangChain integration library for AOps
5
+ Requires-Python: >=3.12
6
+ License-File: LICENSE
7
+ Requires-Dist: httpx>=0.27
8
+ Requires-Dist: pydantic>=2.0
9
+ Requires-Dist: langchain-core>=0.3
10
+ Requires-Dist: python-dotenv>=1.0
11
+ Provides-Extra: dev
12
+ Requires-Dist: pytest>=8.0; extra == "dev"
13
+ Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
14
+ Requires-Dist: respx>=0.21; extra == "dev"
15
+ Dynamic: license-file
aops-0.1.0/README.md ADDED
@@ -0,0 +1,160 @@
1
+ # aops
2
+
3
+ LangChain integration library for [AOps](https://github.com/cow-coding/AgentOps) — load agent prompt configurations from the AOps backend and use them directly in LangChain chains.
4
+
5
+ ## Requirements
6
+
7
+ - Python 3.12+
8
+ - AOps backend running (self-hosted)
9
+ - API key issued from the AOps UI (Agent detail page → New API Key)
10
+
11
+ ## Installation
12
+
13
+ ```bash
14
+ pip install aops
15
+ ```
16
+
17
+ ## Quick Start
18
+
19
+ ```python
20
+ from dotenv import load_dotenv
21
+ load_dotenv()
22
+
23
+ import aops
24
+ aops.init(api_key="aops_...")
25
+
26
+ from aops.langchain import pull
27
+ from langchain_core.prompts import ChatPromptTemplate, HumanMessagePromptTemplate
28
+ from langchain_core.output_parsers import StrOutputParser
29
+ from langchain_openai import ChatOpenAI
30
+
31
+ system_prompt = pull("my-agent/my-chain")
32
+
33
+ chain = (
34
+ ChatPromptTemplate.from_messages([
35
+ system_prompt,
36
+ HumanMessagePromptTemplate.from_template("{user_input}"),
37
+ ])
38
+ | ChatOpenAI(model="gpt-4o-mini")
39
+ | StrOutputParser()
40
+ )
41
+
42
+ result = chain.invoke({"user_input": "Hello!"})
43
+ ```
44
+
45
+ ## Configuration
46
+
47
+ ### API Key
48
+
49
+ AOps API keys embed the server host, so no separate `base_url` is needed.
50
+
51
+ ```
52
+ aops_{base64(host)}_{token}
53
+ ```
54
+
55
+ Issue a key from the AOps UI: **Agent detail page → API Keys → New API Key**
56
+
57
+ ### `aops.init()`
58
+
59
+ Call `init()` once before using any aops functions:
60
+
61
+ ```python
62
+ import aops
63
+
64
+ aops.init(api_key="aops_...")
65
+ ```
66
+
67
+ Or use environment variables — `init()` is optional when env vars are set:
68
+
69
+ ```bash
70
+ # .env
71
+ AGENTOPS_API_KEY=aops_...
72
+ OPENAI_API_KEY=sk-...
73
+ ```
74
+
75
+ | Environment Variable | Default | Description |
76
+ |------------------------|--------------------------|--------------------------------------|
77
+ | `AGENTOPS_API_KEY` | — | API key (host is parsed from it) |
78
+ | `AGENTOPS_BASE_URL` | parsed from key | Override the host embedded in the key |
79
+ | `AGENTOPS_API_PREFIX` | `/api/v1` | API path prefix |
80
+ | `AGENTOPS_CACHE_TTL` | `300` | Prompt cache TTL in seconds (`0` = no cache) |
81
+
82
+ ## API
83
+
84
+ ### `pull(ref, *, version=None)`
85
+
86
+ Fetch a chain from AOps and return a `SystemMessagePromptTemplate`.
87
+
88
+ ```python
89
+ from aops.langchain import pull
90
+
91
+ prompt = pull("my-agent/my-chain") # latest
92
+ prompt = pull("my-agent/my-chain", version=2) # pinned version
93
+ ```
94
+
95
+ The chain's `persona` and `content` are merged into a single system message:
96
+
97
+ ```
98
+ # Persona
99
+ {persona}
100
+
101
+ # Content
102
+ {content}
103
+ ```
104
+
105
+ `content` may contain LangChain template variables (e.g. `{language}`).
106
+ `persona` is treated as a fixed string — its braces are escaped automatically.
107
+
108
+ ---
109
+
110
+ ### `@chain_prompt(agent_name, chain_name, *, version=None)`
111
+
112
+ Decorator that fetches the prompt and injects it as the first argument.
113
+
114
+ **Function decorator**
115
+
116
+ ```python
117
+ from aops.langchain import chain_prompt
118
+ from langchain_core.prompts import SystemMessagePromptTemplate
119
+
120
+ @chain_prompt("my-agent", "my-chain")
121
+ def answer(prompt: SystemMessagePromptTemplate, user_input: str) -> str:
122
+ return (
123
+ ChatPromptTemplate.from_messages([
124
+ prompt,
125
+ HumanMessagePromptTemplate.from_template("{user_input}"),
126
+ ])
127
+ | ChatOpenAI(model="gpt-4o-mini")
128
+ | StrOutputParser()
129
+ ).invoke({"user_input": user_input})
130
+
131
+ result = answer(user_input="What is AOps?")
132
+ ```
133
+
134
+ **Class decorator**
135
+
136
+ The prompt is injected into `__init__` as the first argument after `self`. Build the chain once and reuse it:
137
+
138
+ ```python
139
+ @chain_prompt("my-agent", "my-chain")
140
+ class MyAgent:
141
+ def __init__(self, prompt: SystemMessagePromptTemplate) -> None:
142
+ self.chain = (
143
+ ChatPromptTemplate.from_messages([
144
+ prompt,
145
+ HumanMessagePromptTemplate.from_template("{user_input}"),
146
+ ])
147
+ | ChatOpenAI(model="gpt-4o-mini")
148
+ | StrOutputParser()
149
+ )
150
+
151
+ def run(self, user_input: str) -> str:
152
+ return self.chain.invoke({"user_input": user_input})
153
+
154
+ agent = MyAgent()
155
+ result = agent.run(user_input="Hello!")
156
+ ```
157
+
158
+ ## License
159
+
160
+ MIT
@@ -0,0 +1,32 @@
1
+ """aops — LangChain integration for AgentOps.
2
+
3
+ Quick start::
4
+
5
+ import aops
6
+ aops.init(api_key="aops_aHR0cDovL2xvY2FsaG9zdDo4MDAw_...")
7
+
8
+ from aops.langchain import pull, chain_prompt
9
+ """
10
+
11
+ from aops._client import AopsClient
12
+ from aops._config import init
13
+ from aops._exceptions import (
14
+ AgentNotFoundError,
15
+ AopsConnectionError,
16
+ AopsError,
17
+ ChainNotFoundError,
18
+ VersionNotFoundError,
19
+ )
20
+ from aops._keys import generate_key, parse_key
21
+
22
+ __all__ = [
23
+ "init",
24
+ "AopsClient",
25
+ "generate_key",
26
+ "parse_key",
27
+ "AopsError",
28
+ "AgentNotFoundError",
29
+ "ChainNotFoundError",
30
+ "VersionNotFoundError",
31
+ "AopsConnectionError",
32
+ ]
@@ -0,0 +1,33 @@
1
+ import time
2
+ from typing import Any
3
+
4
+
5
+ class TTLCache:
6
+ """Simple in-memory cache with TTL expiration."""
7
+
8
+ def __init__(self, ttl: int = 300) -> None:
9
+ self._ttl = ttl
10
+ self._store: dict[str, tuple[Any, float]] = {}
11
+
12
+ def get(self, key: str) -> Any | None:
13
+ if self._ttl == 0:
14
+ return None
15
+ entry = self._store.get(key)
16
+ if entry is None:
17
+ return None
18
+ value, ts = entry
19
+ if time.monotonic() - ts > self._ttl:
20
+ del self._store[key]
21
+ return None
22
+ return value
23
+
24
+ def set(self, key: str, value: Any) -> None:
25
+ if self._ttl == 0:
26
+ return
27
+ self._store[key] = (value, time.monotonic())
28
+
29
+ def invalidate(self, key: str) -> None:
30
+ self._store.pop(key, None)
31
+
32
+ def clear(self) -> None:
33
+ self._store.clear()
@@ -0,0 +1,147 @@
1
+ import uuid
2
+
3
+ import httpx
4
+
5
+ from aops._cache import TTLCache
6
+ from aops._config import get_config
7
+ from aops._exceptions import (
8
+ AgentNotFoundError,
9
+ AopsConnectionError,
10
+ ChainNotFoundError,
11
+ VersionNotFoundError,
12
+ )
13
+ from aops._keys import InvalidApiKeyError, parse_key
14
+ from aops._models import AgentModel, ChainModel, ChainVersionModel
15
+
16
+
17
+ class AopsClient:
18
+ """HTTP client for the AgentOps backend API."""
19
+
20
+ def __init__(
21
+ self,
22
+ api_key: str | None = None,
23
+ *,
24
+ base_url: str | None = None,
25
+ api_prefix: str | None = None,
26
+ cache_ttl: int | None = None,
27
+ ) -> None:
28
+ if api_key is not None or base_url is not None:
29
+ # Explicit construction — resolve host from key, then allow override
30
+ resolved_url = base_url or _host_from_key(api_key)
31
+ self._api_base = f"{resolved_url.rstrip('/')}{api_prefix or '/api/v1'}"
32
+ self._api_key = api_key
33
+ ttl = cache_ttl if cache_ttl is not None else 300
34
+ else:
35
+ config = get_config()
36
+ self._api_base = config.api_base
37
+ self._api_key = config.api_key
38
+ ttl = cache_ttl if cache_ttl is not None else config.cache_ttl
39
+
40
+ self._cache = TTLCache(ttl)
41
+
42
+ # ------------------------------------------------------------------
43
+ # Internal helpers
44
+ # ------------------------------------------------------------------
45
+
46
+ def _headers(self) -> dict[str, str]:
47
+ if self._api_key:
48
+ return {"Authorization": f"Bearer {self._api_key}"}
49
+ return {}
50
+
51
+ def _get(self, path: str) -> list | dict:
52
+ url = f"{self._api_base}{path}"
53
+ try:
54
+ with httpx.Client() as client:
55
+ response = client.get(url, headers=self._headers())
56
+ response.raise_for_status()
57
+ return response.json()
58
+ except httpx.ConnectError as exc:
59
+ raise AopsConnectionError(
60
+ f"Cannot reach AgentOps at '{self._api_base}'. "
61
+ "Check that the backend is running and your API key is correct."
62
+ ) from exc
63
+ except httpx.HTTPStatusError as exc:
64
+ status = exc.response.status_code
65
+ if status == 401:
66
+ raise AopsConnectionError(
67
+ "AgentOps rejected the API key (401 Unauthorized). "
68
+ "Check that AGENTOPS_API_KEY is valid."
69
+ ) from exc
70
+ if status == 403:
71
+ raise AopsConnectionError(
72
+ "Access denied (403 Forbidden). "
73
+ "This API key may not have access to the requested agent."
74
+ ) from exc
75
+ raise AopsConnectionError(
76
+ f"AgentOps returned {status} for {url}"
77
+ ) from exc
78
+
79
+ # ------------------------------------------------------------------
80
+ # Public API
81
+ # ------------------------------------------------------------------
82
+
83
+ def get_agent_by_name(self, agent_name: str) -> AgentModel:
84
+ cache_key = f"agent:{agent_name}"
85
+ cached = self._cache.get(cache_key)
86
+ if cached is not None:
87
+ return cached
88
+
89
+ agents = [AgentModel(**a) for a in self._get("/agents/")]
90
+ for agent in agents:
91
+ if agent.name == agent_name:
92
+ self._cache.set(cache_key, agent)
93
+ return agent
94
+
95
+ raise AgentNotFoundError(
96
+ f"Agent '{agent_name}' not found. "
97
+ f"Available agents: {[a.name for a in agents] or '(none)'}"
98
+ )
99
+
100
+ def get_chain_by_name(self, agent_id: uuid.UUID, chain_name: str) -> ChainModel:
101
+ cache_key = f"chain:{agent_id}:{chain_name}"
102
+ cached = self._cache.get(cache_key)
103
+ if cached is not None:
104
+ return cached
105
+
106
+ chains = [ChainModel(**c) for c in self._get(f"/agents/{agent_id}/chains/")]
107
+ for chain in chains:
108
+ if chain.name == chain_name:
109
+ self._cache.set(cache_key, chain)
110
+ return chain
111
+
112
+ raise ChainNotFoundError(
113
+ f"Chain '{chain_name}' not found. "
114
+ f"Available chains: {[c.name for c in chains] or '(none)'}"
115
+ )
116
+
117
+ def get_chain_version(
118
+ self, agent_id: uuid.UUID, chain_id: uuid.UUID, version_number: int
119
+ ) -> ChainVersionModel:
120
+ cache_key = f"version:{chain_id}:{version_number}"
121
+ cached = self._cache.get(cache_key)
122
+ if cached is not None:
123
+ return cached
124
+
125
+ versions = [
126
+ ChainVersionModel(**v)
127
+ for v in self._get(f"/agents/{agent_id}/chains/{chain_id}/versions/")
128
+ ]
129
+ for version in versions:
130
+ if version.version_number == version_number:
131
+ self._cache.set(cache_key, version)
132
+ return version
133
+
134
+ available = sorted(v.version_number for v in versions)
135
+ raise VersionNotFoundError(
136
+ f"Version {version_number} not found. Available versions: {available or '(none)'}"
137
+ )
138
+
139
+
140
+ def _host_from_key(api_key: str | None) -> str:
141
+ if api_key is None:
142
+ return "http://localhost:8000"
143
+ try:
144
+ host, _ = parse_key(api_key)
145
+ return host
146
+ except InvalidApiKeyError:
147
+ return "http://localhost:8000"
@@ -0,0 +1,90 @@
1
+ import os
2
+ from dataclasses import dataclass
3
+
4
+ from aops._keys import InvalidApiKeyError, parse_key
5
+
6
+
7
+ @dataclass
8
+ class Config:
9
+ base_url: str = "http://localhost:8000"
10
+ api_prefix: str = "/api/v1"
11
+ api_key: str | None = None
12
+ cache_ttl: int = 300 # seconds; 0 = no cache
13
+
14
+ @property
15
+ def api_base(self) -> str:
16
+ return f"{self.base_url.rstrip('/')}{self.api_prefix}"
17
+
18
+
19
+ _config: Config | None = None
20
+
21
+
22
+ def init(
23
+ api_key: str | None = None,
24
+ *,
25
+ base_url: str | None = None,
26
+ api_prefix: str | None = None,
27
+ cache_ttl: int | None = None,
28
+ ) -> None:
29
+ """Configure the AgentOps connection.
30
+
31
+ Preferred usage — key only (host is parsed from the key automatically)::
32
+
33
+ aops.init(api_key="aops_aHR0cDovL2xvY2FsaG9zdDo4MDAw_...")
34
+
35
+ Manual URL override (e.g. for testing or reverse-proxy setups)::
36
+
37
+ aops.init(api_key="aops_...", base_url="http://my-proxy:9000")
38
+
39
+ Environment variables (when ``init()`` is not called explicitly):
40
+ AGENTOPS_API_KEY — the API key (host is parsed from it)
41
+ AGENTOPS_BASE_URL — overrides the host embedded in the key
42
+ AGENTOPS_API_PREFIX — default: /api/v1
43
+ AGENTOPS_CACHE_TTL — default: 300 (seconds)
44
+ """
45
+ global _config
46
+
47
+ resolved_key = api_key or os.getenv("AGENTOPS_API_KEY")
48
+ explicit_url = base_url or os.getenv("AGENTOPS_BASE_URL")
49
+
50
+ resolved_url = _resolve_base_url(resolved_key, explicit_url)
51
+
52
+ _config = Config(
53
+ base_url=resolved_url,
54
+ api_prefix=api_prefix or os.getenv("AGENTOPS_API_PREFIX", "/api/v1"),
55
+ api_key=resolved_key,
56
+ cache_ttl=cache_ttl if cache_ttl is not None else int(os.getenv("AGENTOPS_CACHE_TTL", "300")),
57
+ )
58
+
59
+
60
+ def get_config() -> Config:
61
+ global _config
62
+ if _config is None:
63
+ resolved_key = os.getenv("AGENTOPS_API_KEY")
64
+ explicit_url = os.getenv("AGENTOPS_BASE_URL")
65
+ _config = Config(
66
+ base_url=_resolve_base_url(resolved_key, explicit_url),
67
+ api_prefix=os.getenv("AGENTOPS_API_PREFIX", "/api/v1"),
68
+ api_key=resolved_key,
69
+ cache_ttl=int(os.getenv("AGENTOPS_CACHE_TTL", "300")),
70
+ )
71
+ return _config
72
+
73
+
74
+ # ------------------------------------------------------------------
75
+ # Internal helpers
76
+ # ------------------------------------------------------------------
77
+
78
+ def _resolve_base_url(api_key: str | None, explicit_url: str | None) -> str:
79
+ """Determine the base URL from the key and/or an explicit override."""
80
+ if explicit_url:
81
+ return explicit_url
82
+
83
+ if api_key:
84
+ try:
85
+ host, _ = parse_key(api_key)
86
+ return host
87
+ except InvalidApiKeyError:
88
+ pass # fall through to default
89
+
90
+ return "http://localhost:8000"
@@ -0,0 +1,18 @@
1
+ class AopsError(Exception):
2
+ """Base exception for aops."""
3
+
4
+
5
+ class AgentNotFoundError(AopsError):
6
+ """Agent with the given name does not exist."""
7
+
8
+
9
+ class ChainNotFoundError(AopsError):
10
+ """Chain with the given name does not exist."""
11
+
12
+
13
+ class VersionNotFoundError(AopsError):
14
+ """The requested chain version does not exist."""
15
+
16
+
17
+ class AopsConnectionError(AopsError):
18
+ """Failed to connect to the AgentOps backend."""
@@ -0,0 +1,83 @@
1
+ """API key utilities for aops.
2
+
3
+ Key format:
4
+ aops_{base64url(host)}_{secret_token}
5
+
6
+ Example:
7
+ host = "http://localhost:8000"
8
+ key = "aops_aHR0cDovL2xvY2FsaG9zdDo4MDAw_xK9mP2abcXYZ..."
9
+
10
+ The host is base64url-encoded (no padding) inside the key, so the library
11
+ can resolve the AgentOps server without a separate base_url setting.
12
+ """
13
+
14
+ import base64
15
+ import secrets
16
+
17
+ _PREFIX = "aops"
18
+ _SEP = "_"
19
+
20
+
21
+ class InvalidApiKeyError(ValueError):
22
+ """Raised when an API key cannot be parsed."""
23
+
24
+
25
+ def generate_key(host: str) -> str:
26
+ """Generate a new API key that embeds *host*.
27
+
28
+ This helper is intended for the AgentOps backend when issuing keys.
29
+ The returned key must be stored (hashed) and should be shown to the
30
+ user only once.
31
+
32
+ Args:
33
+ host: Full base URL of the AgentOps server,
34
+ e.g. ``"http://localhost:8000"``.
35
+
36
+ Returns:
37
+ A key string of the form ``"aops_{encoded_host}_{token}"``.
38
+ """
39
+ encoded_host = _encode_host(host)
40
+ token = secrets.token_urlsafe(32)
41
+ return f"{_PREFIX}{_SEP}{encoded_host}{_SEP}{token}"
42
+
43
+
44
+ def parse_key(api_key: str) -> tuple[str, str]:
45
+ """Parse an API key and return ``(host, token)``.
46
+
47
+ Args:
48
+ api_key: A key produced by :func:`generate_key`.
49
+
50
+ Returns:
51
+ ``(host, token)`` tuple where *host* is the raw base URL string.
52
+
53
+ Raises:
54
+ :class:`InvalidApiKeyError` if the key format is invalid.
55
+ """
56
+ parts = api_key.split(_SEP, 2)
57
+ if len(parts) != 3 or parts[0] != _PREFIX:
58
+ raise InvalidApiKeyError(
59
+ f"Invalid API key format. Expected 'aops_<host>_<token>', got: '{api_key[:12]}...'"
60
+ )
61
+ _, encoded_host, token = parts
62
+ try:
63
+ host = _decode_host(encoded_host)
64
+ except Exception as exc:
65
+ raise InvalidApiKeyError(
66
+ f"Cannot decode host from API key: {exc}"
67
+ ) from exc
68
+ return host, token
69
+
70
+
71
+ # ------------------------------------------------------------------
72
+ # Internal helpers
73
+ # ------------------------------------------------------------------
74
+
75
+ def _encode_host(host: str) -> str:
76
+ """base64url-encode *host* without padding."""
77
+ return base64.urlsafe_b64encode(host.encode()).decode().rstrip("=")
78
+
79
+
80
+ def _decode_host(encoded: str) -> str:
81
+ """Decode a base64url *encoded* host (handles missing padding)."""
82
+ padded = encoded + "=" * (-len(encoded) % 4)
83
+ return base64.urlsafe_b64decode(padded).decode()
@@ -0,0 +1,33 @@
1
+ import uuid
2
+ from datetime import datetime
3
+
4
+ from pydantic import BaseModel
5
+
6
+
7
+ class AgentModel(BaseModel):
8
+ id: uuid.UUID
9
+ name: str
10
+ description: str | None
11
+ created_at: datetime
12
+ updated_at: datetime
13
+
14
+
15
+ class ChainModel(BaseModel):
16
+ id: uuid.UUID
17
+ agent_id: uuid.UUID
18
+ name: str
19
+ description: str | None
20
+ persona: str
21
+ content: str
22
+ created_at: datetime
23
+ updated_at: datetime
24
+
25
+
26
+ class ChainVersionModel(BaseModel):
27
+ id: uuid.UUID
28
+ chain_id: uuid.UUID
29
+ persona: str
30
+ content: str
31
+ message: str
32
+ version_number: int
33
+ created_at: datetime
@@ -0,0 +1,3 @@
1
+ from aops.langchain._loader import chain_prompt, pull
2
+
3
+ __all__ = ["pull", "chain_prompt"]
@@ -0,0 +1,166 @@
1
+ import functools
2
+ import inspect
3
+ from typing import Callable
4
+
5
+ from langchain_core.prompts import SystemMessagePromptTemplate
6
+
7
+ from aops._client import AopsClient
8
+
9
+
10
+ def _to_system_prompt(persona: str, content: str) -> SystemMessagePromptTemplate:
11
+ """Convert AgentOps chain fields into a SystemMessagePromptTemplate.
12
+
13
+ Both ``persona`` and ``content`` are agent-authored system-level instructions,
14
+ merged into a single SystemMessage:
15
+
16
+ # Persona
17
+ <persona text>
18
+
19
+ # Content
20
+ <content text — may contain {template_variables}>
21
+
22
+ ``persona`` is treated as a fixed string (its braces are escaped).
23
+ ``content`` may contain LangChain template variables in single braces,
24
+ e.g. ``"Answer in {language}"``.
25
+ To include a literal brace in content use double braces: ``{{`` or ``}}``.
26
+ """
27
+ persona_escaped = persona.replace("{", "{{").replace("}", "}}")
28
+ system_text = f"# Persona\n{persona_escaped}\n\n# Content\n{content}"
29
+ return SystemMessagePromptTemplate.from_template(system_text)
30
+
31
+
32
+ def pull(
33
+ ref: str,
34
+ *,
35
+ version: int | None = None,
36
+ client: AopsClient | None = None,
37
+ ) -> SystemMessagePromptTemplate:
38
+ """Fetch a chain from AgentOps and return it as a SystemMessagePromptTemplate.
39
+
40
+ Args:
41
+ ref: ``"agent-name/chain-name"`` reference string.
42
+ version: Specific version number to load. ``None`` loads the current
43
+ (latest saved) chain content.
44
+ client: Optional pre-configured :class:`~aops._client.AopsClient`.
45
+ When omitted the global configuration is used.
46
+
47
+ Returns:
48
+ A :class:`langchain_core.prompts.SystemMessagePromptTemplate` combining
49
+ the chain's ``persona`` and ``content`` into a single system message.
50
+
51
+ Example::
52
+
53
+ from aops.langchain import pull
54
+
55
+ prompt = pull("customer-support/greeter")
56
+ prompt_v2 = pull("customer-support/greeter", version=2)
57
+
58
+ chain = prompt | ChatOpenAI() | StrOutputParser()
59
+ chain.invoke({"topic": "billing"})
60
+ """
61
+ if "/" not in ref:
62
+ raise ValueError(
63
+ f"Invalid ref '{ref}'. Expected format: 'agent-name/chain-name'."
64
+ )
65
+
66
+ agent_name, chain_name = ref.split("/", 1)
67
+ c = client or AopsClient()
68
+
69
+ agent = c.get_agent_by_name(agent_name)
70
+ chain = c.get_chain_by_name(agent.id, chain_name)
71
+
72
+ if version is not None:
73
+ v = c.get_chain_version(agent.id, chain.id, version)
74
+ return _to_system_prompt(v.persona, v.content)
75
+
76
+ return _to_system_prompt(chain.persona, chain.content)
77
+
78
+
79
+ def chain_prompt(
80
+ agent_name: str,
81
+ chain_name: str,
82
+ *,
83
+ version: int | None = None,
84
+ client: AopsClient | None = None,
85
+ ):
86
+ """Decorator that injects a ``SystemMessagePromptTemplate`` from AgentOps.
87
+
88
+ The prompt is fetched once on the first call and then served from the
89
+ client-level cache on subsequent calls.
90
+
91
+ **Function decorator** — the resolved prompt is passed as the first
92
+ positional argument::
93
+
94
+ @chain_prompt("my-agent", "summariser")
95
+ def summarise(prompt: SystemMessagePromptTemplate, text: str) -> str:
96
+ return (prompt | ChatOpenAI()).invoke({"text": text})
97
+
98
+ result = summarise(text="Long article...")
99
+
100
+ **Class decorator** — the resolved prompt is passed as the second
101
+ positional argument to ``__init__`` (after ``self``)::
102
+
103
+ @chain_prompt("my-agent", "summariser")
104
+ class Summariser:
105
+ def __init__(self, prompt: SystemMessagePromptTemplate) -> None:
106
+ self.chain = prompt | ChatOpenAI()
107
+
108
+ def run(self, text: str) -> str:
109
+ return self.chain.invoke({"text": text})
110
+
111
+ summariser = Summariser()
112
+ result = summariser.run(text="Long article...")
113
+
114
+ Args:
115
+ agent_name: Name of the agent registered in AgentOps.
116
+ chain_name: Name of the chain within that agent.
117
+ version: Version number to pin. ``None`` = latest.
118
+ client: Optional custom :class:`~aops._client.AopsClient`.
119
+ """
120
+
121
+ def decorator(target: type | Callable) -> type | Callable:
122
+ if inspect.isclass(target):
123
+ return _wrap_class(target, agent_name, chain_name, version, client)
124
+ return _wrap_function(target, agent_name, chain_name, version, client)
125
+
126
+ return decorator
127
+
128
+
129
+ # ------------------------------------------------------------------
130
+ # Internal wrappers
131
+ # ------------------------------------------------------------------
132
+
133
+ def _wrap_function(
134
+ func: Callable,
135
+ agent_name: str,
136
+ chain_name: str,
137
+ version: int | None,
138
+ client: AopsClient | None,
139
+ ) -> Callable:
140
+ ref = f"{agent_name}/{chain_name}"
141
+
142
+ @functools.wraps(func)
143
+ def wrapper(*args, **kwargs):
144
+ prompt = pull(ref, version=version, client=client)
145
+ return func(prompt, *args, **kwargs)
146
+
147
+ return wrapper
148
+
149
+
150
+ def _wrap_class(
151
+ cls: type,
152
+ agent_name: str,
153
+ chain_name: str,
154
+ version: int | None,
155
+ client: AopsClient | None,
156
+ ) -> type:
157
+ ref = f"{agent_name}/{chain_name}"
158
+ original_init = cls.__init__
159
+
160
+ @functools.wraps(original_init)
161
+ def new_init(self, *args, **kwargs):
162
+ prompt = pull(ref, version=version, client=client)
163
+ original_init(self, prompt, *args, **kwargs)
164
+
165
+ cls.__init__ = new_init
166
+ return cls
@@ -0,0 +1,15 @@
1
+ Metadata-Version: 2.4
2
+ Name: aops
3
+ Version: 0.1.0
4
+ Summary: LangChain integration library for AOps
5
+ Requires-Python: >=3.12
6
+ License-File: LICENSE
7
+ Requires-Dist: httpx>=0.27
8
+ Requires-Dist: pydantic>=2.0
9
+ Requires-Dist: langchain-core>=0.3
10
+ Requires-Dist: python-dotenv>=1.0
11
+ Provides-Extra: dev
12
+ Requires-Dist: pytest>=8.0; extra == "dev"
13
+ Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
14
+ Requires-Dist: respx>=0.21; extra == "dev"
15
+ Dynamic: license-file
@@ -0,0 +1,17 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ aops/__init__.py
5
+ aops/_cache.py
6
+ aops/_client.py
7
+ aops/_config.py
8
+ aops/_exceptions.py
9
+ aops/_keys.py
10
+ aops/_models.py
11
+ aops.egg-info/PKG-INFO
12
+ aops.egg-info/SOURCES.txt
13
+ aops.egg-info/dependency_links.txt
14
+ aops.egg-info/requires.txt
15
+ aops.egg-info/top_level.txt
16
+ aops/langchain/__init__.py
17
+ aops/langchain/_loader.py
@@ -0,0 +1,9 @@
1
+ httpx>=0.27
2
+ pydantic>=2.0
3
+ langchain-core>=0.3
4
+ python-dotenv>=1.0
5
+
6
+ [dev]
7
+ pytest>=8.0
8
+ pytest-asyncio>=0.23
9
+ respx>=0.21
@@ -0,0 +1 @@
1
+ aops
@@ -0,0 +1,26 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "aops"
7
+ version = "0.1.0"
8
+ description = "LangChain integration library for AOps"
9
+ requires-python = ">=3.12"
10
+ dependencies = [
11
+ "httpx>=0.27",
12
+ "pydantic>=2.0",
13
+ "langchain-core>=0.3",
14
+ "python-dotenv>=1.0",
15
+ ]
16
+
17
+ [project.optional-dependencies]
18
+ dev = [
19
+ "pytest>=8.0",
20
+ "pytest-asyncio>=0.23",
21
+ "respx>=0.21",
22
+ ]
23
+
24
+ [tool.setuptools.packages.find]
25
+ where = ["."]
26
+ include = ["aops*"]
aops-0.1.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+