aiagentnotes 0.1.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.
@@ -0,0 +1,19 @@
1
+ from aiagentnotes.client import AgentNotes
2
+ from aiagentnotes.env import get_client, is_configured, load_env_file, require_client
3
+ from aiagentnotes.paths import (
4
+ DEFAULT_COMPLETE_PATH,
5
+ DEFAULT_HEALTH_PATH,
6
+ DEFAULT_LOG_PATH,
7
+ )
8
+
9
+ __all__ = [
10
+ "AgentNotes",
11
+ "get_client",
12
+ "require_client",
13
+ "is_configured",
14
+ "load_env_file",
15
+ "DEFAULT_LOG_PATH",
16
+ "DEFAULT_COMPLETE_PATH",
17
+ "DEFAULT_HEALTH_PATH",
18
+ ]
19
+ __version__ = "0.1.0"
aiagentnotes/client.py ADDED
@@ -0,0 +1,240 @@
1
+ """AgentNotes Python SDK — SparkNotes for your AI agents."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import atexit
6
+ import functools
7
+ import os
8
+ import threading
9
+ import traceback
10
+ import uuid
11
+ from typing import Any, Callable, Optional, TypeVar
12
+
13
+ import requests
14
+
15
+ from aiagentnotes.env import load_env_file
16
+ from aiagentnotes.paths import DEFAULT_COMPLETE_PATH, DEFAULT_LOG_PATH
17
+
18
+ F = TypeVar("F", bound=Callable[..., Any])
19
+
20
+ DEFAULT_BASE_URL = "https://agentnotes.app"
21
+
22
+
23
+ def _api_url(base_url: str, path: str) -> str:
24
+ return f"{base_url.rstrip('/')}/{path.lstrip('/')}"
25
+
26
+
27
+ class AgentNotes:
28
+ """Track agent runs and send batched logs to AgentNotes."""
29
+
30
+ def __init__(
31
+ self,
32
+ api_key: str,
33
+ agent_id: str,
34
+ *,
35
+ base_url: str = DEFAULT_BASE_URL,
36
+ log_path: str = DEFAULT_LOG_PATH,
37
+ complete_path: str = DEFAULT_COMPLETE_PATH,
38
+ summary_frequency: str = "daily",
39
+ batch_size: int = 20,
40
+ flush_interval: float = 5.0,
41
+ ) -> None:
42
+ self.api_key = api_key
43
+ self.agent_id = agent_id
44
+ self.base_url = base_url.rstrip("/")
45
+ self.log_path = log_path
46
+ self.complete_path = complete_path
47
+ self.log_url = _api_url(self.base_url, log_path)
48
+ self.complete_url = _api_url(self.base_url, complete_path)
49
+ self.summary_frequency = summary_frequency
50
+ self.batch_size = batch_size
51
+ self.flush_interval = flush_interval
52
+
53
+ self._run_id: Optional[str] = None
54
+ self._buffer: list[dict[str, Any]] = []
55
+ self._lock = threading.Lock()
56
+ self._session = requests.Session()
57
+ self._session.headers.update(
58
+ {
59
+ "Authorization": f"Bearer {api_key}",
60
+ "Content-Type": "application/json",
61
+ }
62
+ )
63
+
64
+ atexit.register(self.flush)
65
+
66
+ @classmethod
67
+ def from_env(cls) -> AgentNotes:
68
+ """Build client from AGENTNOTES_* environment variables (Railway, Docker, etc.)."""
69
+ load_env_file()
70
+ missing = [
71
+ name
72
+ for name, val in (
73
+ ("AGENTNOTES_API_KEY", os.environ.get("AGENTNOTES_API_KEY")),
74
+ ("AGENTNOTES_AGENT_ID", os.environ.get("AGENTNOTES_AGENT_ID")),
75
+ )
76
+ if not val
77
+ ]
78
+ if missing:
79
+ raise ValueError(f"Missing required env: {', '.join(missing)}")
80
+
81
+ return cls(
82
+ api_key=os.environ["AGENTNOTES_API_KEY"],
83
+ agent_id=os.environ["AGENTNOTES_AGENT_ID"],
84
+ base_url=os.environ.get("AGENTNOTES_BASE_URL", DEFAULT_BASE_URL),
85
+ log_path=os.environ.get("AGENTNOTES_LOG_PATH", DEFAULT_LOG_PATH),
86
+ complete_path=os.environ.get(
87
+ "AGENTNOTES_COMPLETE_PATH", DEFAULT_COMPLETE_PATH
88
+ ),
89
+ )
90
+
91
+ def _ensure_run(self) -> str:
92
+ if not self._run_id:
93
+ self._run_id = str(uuid.uuid4())
94
+ return self._run_id
95
+
96
+ def start_run(self, external_run_id: Optional[str] = None) -> str:
97
+ """Start a new run. Returns the run ID."""
98
+ self.flush()
99
+ self._run_id = str(uuid.uuid4())
100
+ if external_run_id:
101
+ self.log("run_started", data={"external_run_id": external_run_id})
102
+ return self._run_id
103
+
104
+ def log(
105
+ self,
106
+ message: str,
107
+ *,
108
+ data: Optional[dict[str, Any]] = None,
109
+ step_name: Optional[str] = None,
110
+ ) -> None:
111
+ """Queue a log event."""
112
+ self._enqueue(
113
+ {
114
+ "step_name": step_name,
115
+ "event_type": "log",
116
+ "message": message,
117
+ "data": data,
118
+ }
119
+ )
120
+
121
+ def log_error(
122
+ self,
123
+ error: BaseException,
124
+ *,
125
+ step_name: Optional[str] = None,
126
+ ) -> None:
127
+ """Queue an error event."""
128
+ self._enqueue(
129
+ {
130
+ "step_name": step_name,
131
+ "event_type": "error",
132
+ "message": str(error),
133
+ "data": {
134
+ "type": type(error).__name__,
135
+ "traceback": traceback.format_exc(),
136
+ },
137
+ }
138
+ )
139
+
140
+ def _enqueue(self, entry: dict[str, Any]) -> None:
141
+ with self._lock:
142
+ self._buffer.append(entry)
143
+ if len(self._buffer) >= self.batch_size:
144
+ self._flush_unlocked()
145
+
146
+ def flush(self) -> None:
147
+ """Send buffered logs to the API."""
148
+ with self._lock:
149
+ self._flush_unlocked()
150
+
151
+ def _flush_unlocked(self) -> None:
152
+ if not self._buffer:
153
+ return
154
+
155
+ batch = self._buffer[:]
156
+ self._buffer.clear()
157
+ run_id = self._ensure_run()
158
+
159
+ payload = {
160
+ "agent_id": self.agent_id,
161
+ "run_id": run_id,
162
+ "logs": batch,
163
+ }
164
+
165
+ try:
166
+ resp = self._session.post(
167
+ self.log_url,
168
+ json=payload,
169
+ timeout=30,
170
+ )
171
+ resp.raise_for_status()
172
+ data = resp.json()
173
+ if data.get("run_id"):
174
+ self._run_id = data["run_id"]
175
+ except requests.RequestException:
176
+ # Re-queue on failure for a best-effort retry
177
+ self._buffer = batch + self._buffer
178
+
179
+ def complete_run(
180
+ self,
181
+ result: Optional[Any] = None,
182
+ *,
183
+ status: str = "completed",
184
+ error_message: Optional[str] = None,
185
+ ) -> None:
186
+ """Mark the current run as complete and flush remaining logs."""
187
+ self.flush()
188
+ if not self._run_id:
189
+ return
190
+
191
+ payload = {
192
+ "agent_id": self.agent_id,
193
+ "run_id": self._run_id,
194
+ "result": result,
195
+ "status": status,
196
+ "error_message": error_message,
197
+ }
198
+
199
+ try:
200
+ resp = self._session.post(
201
+ self.complete_url,
202
+ json=payload,
203
+ timeout=30,
204
+ )
205
+ resp.raise_for_status()
206
+ except requests.RequestException:
207
+ pass
208
+ finally:
209
+ self._run_id = None
210
+
211
+ def track(self, step_name: str) -> Callable[[F], F]:
212
+ """Decorator to track a function as an agent step."""
213
+
214
+ def decorator(fn: F) -> F:
215
+ @functools.wraps(fn)
216
+ def wrapper(*args: Any, **kwargs: Any) -> Any:
217
+ self._enqueue(
218
+ {
219
+ "step_name": step_name,
220
+ "event_type": "step_start",
221
+ "message": f"Starting {step_name}",
222
+ }
223
+ )
224
+ try:
225
+ result = fn(*args, **kwargs)
226
+ self._enqueue(
227
+ {
228
+ "step_name": step_name,
229
+ "event_type": "step_end",
230
+ "message": f"Completed {step_name}",
231
+ }
232
+ )
233
+ return result
234
+ except Exception as e:
235
+ self.log_error(e, step_name=step_name)
236
+ raise
237
+
238
+ return wrapper # type: ignore
239
+
240
+ return decorator
aiagentnotes/env.py ADDED
@@ -0,0 +1,61 @@
1
+ """Environment configuration for AgentNotes clients."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from typing import TYPE_CHECKING, Optional
7
+
8
+ from aiagentnotes.paths import DEFAULT_COMPLETE_PATH, DEFAULT_LOG_PATH
9
+
10
+ if TYPE_CHECKING:
11
+ from aiagentnotes.client import AgentNotes
12
+
13
+ REQUIRED_ENV = ("AGENTNOTES_API_KEY", "AGENTNOTES_AGENT_ID")
14
+ OPTIONAL_ENV = (
15
+ "AGENTNOTES_BASE_URL",
16
+ "AGENTNOTES_LOG_PATH",
17
+ "AGENTNOTES_COMPLETE_PATH",
18
+ )
19
+
20
+
21
+ def load_env_file() -> bool:
22
+ """Load `.env` from the current working directory if python-dotenv is installed."""
23
+ try:
24
+ from dotenv import load_dotenv
25
+ except ImportError:
26
+ return False
27
+ return bool(load_dotenv())
28
+
29
+
30
+ def is_configured() -> bool:
31
+ """True when required AGENTNOTES_* variables are set."""
32
+ load_env_file()
33
+ return all(os.environ.get(name) for name in REQUIRED_ENV)
34
+
35
+
36
+ def get_client() -> Optional["AgentNotes"]:
37
+ """
38
+ Return an AgentNotes client when env is configured, else None.
39
+ Safe to call in bots that should run without logging when not set up.
40
+ """
41
+ if not is_configured():
42
+ return None
43
+ from aiagentnotes.client import AgentNotes
44
+
45
+ return AgentNotes.from_env()
46
+
47
+
48
+ def require_client() -> "AgentNotes":
49
+ """Like get_client() but raises with a helpful message if env is missing."""
50
+ client = get_client()
51
+ if client is not None:
52
+ return client
53
+ missing = [name for name in REQUIRED_ENV if not os.environ.get(name)]
54
+ raise ValueError(
55
+ "AgentNotes is not configured. Set environment variables:\n"
56
+ f" Required: {', '.join(REQUIRED_ENV)}\n"
57
+ f" Optional: {', '.join(OPTIONAL_ENV)} (defaults: "
58
+ f"LOG={DEFAULT_LOG_PATH}, COMPLETE={DEFAULT_COMPLETE_PATH})\n"
59
+ f" Missing now: {', '.join(missing)}\n"
60
+ "Install: pip install aiagentnotes"
61
+ )
@@ -0,0 +1 @@
1
+ """Optional integrations for popular agent frameworks."""
@@ -0,0 +1,26 @@
1
+ """CrewAI step hooks via AgentNotes."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Callable
6
+
7
+ from aiagentnotes.client import AgentNotes
8
+
9
+
10
+ def crew_step_logger(notes: AgentNotes, step_name: str) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
11
+ """Decorator for CrewAI task functions — same as notes.track but explicit naming."""
12
+
13
+ return notes.track(step_name)
14
+
15
+
16
+ def log_crew_kickoff(notes: AgentNotes, crew_name: str, **meta: Any) -> str:
17
+ """Call before crew.kickoff(). Returns run id."""
18
+ run_id = notes.start_run(external_run_id=crew_name)
19
+ notes.log("crew_kickoff", data={"crew": crew_name, **meta})
20
+ return run_id
21
+
22
+
23
+ def log_crew_complete(notes: AgentNotes, result: Any) -> None:
24
+ """Call after crew.kickoff() returns."""
25
+ summary = str(result)[:500] if result is not None else "completed"
26
+ notes.complete_run({"summary": summary})
@@ -0,0 +1,126 @@
1
+ """LangChain callback handler for AgentNotes."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Dict, List, Optional, Union
6
+ from uuid import UUID
7
+
8
+ from aiagentnotes.client import AgentNotes
9
+
10
+ try:
11
+ from langchain_core.callbacks import BaseCallbackHandler
12
+ from langchain_core.agents import AgentAction, AgentFinish
13
+ from langchain_core.outputs import LLMResult
14
+ except ImportError as e:
15
+ raise ImportError(
16
+ "LangChain integration requires langchain-core: pip install langchain-core"
17
+ ) from e
18
+
19
+
20
+ class AgentNotesCallbackHandler(BaseCallbackHandler):
21
+ """Log LangChain chains, tools, and LLM calls to AgentNotes."""
22
+
23
+ def __init__(self, notes: AgentNotes) -> None:
24
+ super().__init__()
25
+ self.notes = notes
26
+ if not notes._run_id:
27
+ notes.start_run()
28
+
29
+ def on_chain_start(
30
+ self,
31
+ serialized: Dict[str, Any],
32
+ inputs: Dict[str, Any],
33
+ *,
34
+ run_id: UUID,
35
+ parent_run_id: Optional[UUID] = None,
36
+ **kwargs: Any,
37
+ ) -> None:
38
+ name = serialized.get("name") or serialized.get("id", ["chain"])[-1]
39
+ self.notes.log(
40
+ f"chain_start: {name}",
41
+ step_name=str(name),
42
+ data={"inputs_keys": list(inputs.keys())},
43
+ )
44
+
45
+ def on_chain_end(
46
+ self,
47
+ outputs: Dict[str, Any],
48
+ *,
49
+ run_id: UUID,
50
+ parent_run_id: Optional[UUID] = None,
51
+ **kwargs: Any,
52
+ ) -> None:
53
+ self.notes.log("chain_end", data={"output_keys": list(outputs.keys())})
54
+
55
+ def on_tool_start(
56
+ self,
57
+ serialized: Dict[str, Any],
58
+ input_str: str,
59
+ *,
60
+ run_id: UUID,
61
+ parent_run_id: Optional[UUID] = None,
62
+ **kwargs: Any,
63
+ ) -> None:
64
+ tool = serialized.get("name", "tool")
65
+ self.notes.log(
66
+ f"tool_start: {tool}",
67
+ step_name=tool,
68
+ data={"input_preview": input_str[:500]},
69
+ )
70
+
71
+ def on_tool_end(
72
+ self,
73
+ output: str,
74
+ *,
75
+ run_id: UUID,
76
+ parent_run_id: Optional[UUID] = None,
77
+ **kwargs: Any,
78
+ ) -> None:
79
+ self.notes.log("tool_end", data={"output_preview": str(output)[:500]})
80
+
81
+ def on_tool_error(
82
+ self,
83
+ error: BaseException,
84
+ *,
85
+ run_id: UUID,
86
+ parent_run_id: Optional[UUID] = None,
87
+ **kwargs: Any,
88
+ ) -> None:
89
+ self.notes.log_error(error)
90
+
91
+ def on_llm_end(
92
+ self,
93
+ response: LLMResult,
94
+ *,
95
+ run_id: UUID,
96
+ parent_run_id: Optional[UUID] = None,
97
+ **kwargs: Any,
98
+ ) -> None:
99
+ self.notes.log("llm_end", data={"generations": len(response.generations)})
100
+
101
+ def on_agent_action(
102
+ self,
103
+ action: AgentAction,
104
+ *,
105
+ run_id: UUID,
106
+ parent_run_id: Optional[UUID] = None,
107
+ **kwargs: Any,
108
+ ) -> None:
109
+ self.notes.log(
110
+ f"agent_action: {action.tool}",
111
+ step_name=action.tool,
112
+ data={"tool_input": str(action.tool_input)[:300]},
113
+ )
114
+
115
+ def on_agent_finish(
116
+ self,
117
+ finish: AgentFinish,
118
+ *,
119
+ run_id: UUID,
120
+ parent_run_id: Optional[UUID] = None,
121
+ **kwargs: Any,
122
+ ) -> None:
123
+ self.notes.log(
124
+ "agent_finish",
125
+ data={"output_preview": str(finish.return_values)[:500]},
126
+ )
aiagentnotes/paths.py ADDED
@@ -0,0 +1,5 @@
1
+ """Default HTTP paths for the AgentNotes Next.js API."""
2
+
3
+ DEFAULT_LOG_PATH = "/api/v1/logs"
4
+ DEFAULT_COMPLETE_PATH = "/api/v1/runs/complete"
5
+ DEFAULT_HEALTH_PATH = "/api/v1/health"
@@ -0,0 +1,103 @@
1
+ Metadata-Version: 2.4
2
+ Name: aiagentnotes
3
+ Version: 0.1.0
4
+ Summary: SparkNotes for your AI agents — log runs to AgentNotes with one pip install and env vars.
5
+ Project-URL: Homepage, https://github.com/mattmerrick/agentnotes
6
+ Project-URL: Documentation, https://github.com/mattmerrick/agentnotes/tree/main/sdk
7
+ Project-URL: Repository, https://github.com/mattmerrick/agentnotes
8
+ Author-email: AgentNotes <yomatt41@gmail.com>
9
+ License-Expression: MIT
10
+ Keywords: agents,ai,langchain,logging,observability,railway
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.9
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
21
+ Requires-Python: >=3.9
22
+ Requires-Dist: requests>=2.28.0
23
+ Provides-Extra: dev
24
+ Requires-Dist: build; extra == 'dev'
25
+ Requires-Dist: pytest>=7.0; extra == 'dev'
26
+ Requires-Dist: twine; extra == 'dev'
27
+ Provides-Extra: dotenv
28
+ Requires-Dist: python-dotenv>=1.0.0; extra == 'dotenv'
29
+ Provides-Extra: langchain
30
+ Requires-Dist: langchain-core>=0.2.0; extra == 'langchain'
31
+ Description-Content-Type: text/markdown
32
+
33
+ # AgentNotes Python SDK
34
+
35
+ SparkNotes for your AI agents. Install once, set env vars, call `AgentNotes.from_env()`.
36
+
37
+ ## Install (PyPI)
38
+
39
+ ```bash
40
+ pip install aiagentnotes
41
+ ```
42
+
43
+ Optional local `.env` support:
44
+
45
+ ```bash
46
+ pip install "aiagentnotes[dotenv]"
47
+ ```
48
+
49
+ ## Environment variables
50
+
51
+ See **[ENV.md](./ENV.md)** for the full list.
52
+
53
+ **Minimum:**
54
+
55
+ ```bash
56
+ export AGENTNOTES_API_KEY="an_xxxxxxxx"
57
+ export AGENTNOTES_AGENT_ID="my-bot-slug"
58
+ export AGENTNOTES_BASE_URL="https://your-agentnotes.vercel.app"
59
+ ```
60
+
61
+ ## Quick start
62
+
63
+ ```python
64
+ from aiagentnotes import AgentNotes
65
+
66
+ notes = AgentNotes.from_env()
67
+
68
+ @notes.track("handle_request")
69
+ def handle_request(req):
70
+ notes.log("received", data={"id": getattr(req, "id", None)})
71
+ notes.complete_run({"summary": "Done"})
72
+ ```
73
+
74
+ ## Optional: logging only when configured
75
+
76
+ ```python
77
+ from aiagentnotes import get_client
78
+
79
+ notes = get_client()
80
+ if notes:
81
+ notes.log("bot started")
82
+ ```
83
+
84
+ ## API
85
+
86
+ | Method | Description |
87
+ |--------|-------------|
88
+ | `AgentNotes.from_env()` | Build client from `AGENTNOTES_*` env vars |
89
+ | `get_client()` | Same, or `None` if not configured |
90
+ | `require_client()` | Same, or raise with setup instructions |
91
+ | `notes.log(message, ...)` | Log an event |
92
+ | `notes.complete_run(result)` | Finish run |
93
+ | `notes.track("step")` | Decorator for a step |
94
+
95
+ ## Development (this monorepo)
96
+
97
+ ```bash
98
+ cd sdk && pip install -e ".[dev,dotenv]"
99
+ ```
100
+
101
+ ## Publish to PyPI
102
+
103
+ See [../docs/PUBLISHING.md](../docs/PUBLISHING.md).
@@ -0,0 +1,10 @@
1
+ aiagentnotes/__init__.py,sha256=fvLUoM4cNmiQdzZDaZvQ1bqXb9Yu_S0Lcnf5XmGU9Mg,455
2
+ aiagentnotes/client.py,sha256=clDxD5pc-TagkHYyFuxNCoW3QNFZaqK06eNQg7GL9r0,7175
3
+ aiagentnotes/env.py,sha256=W38kxhyi6X9NLL9-lc0ih_tHIQnEWJSmDMpLvaOlfLA,1843
4
+ aiagentnotes/paths.py,sha256=NNW4TVbmTCSOqfwWKPTmuruaimdnTGXnwtmjyBI8dNk,179
5
+ aiagentnotes/integrations/__init__.py,sha256=yn0cq43EquaRZkTPgnDl5_20js-JT9afmQlwFOyiOqs,58
6
+ aiagentnotes/integrations/crewai.py,sha256=0volZ8jHSkGea3luvavfbBAdEB0E3eQ9Z5YSn3Ux1YA,890
7
+ aiagentnotes/integrations/langchain.py,sha256=c8tiTWAeWGtgRif7-ycgaFHBOeIRQHO-GZ5rPRGG5Pw,3373
8
+ aiagentnotes-0.1.0.dist-info/METADATA,sha256=OS36zSUg0JXvMXrUwS6VNl-AaZ9JWyoopiiQXPqf9dM,2876
9
+ aiagentnotes-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
10
+ aiagentnotes-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any