better-notion 0.9.9__py3-none-any.whl → 1.0.1__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.
- better_notion/plugins/official/__init__.py +3 -1
- better_notion/plugins/official/agents.py +356 -0
- better_notion/utils/agents/__init__.py +65 -0
- better_notion/utils/agents/auth.py +235 -0
- better_notion/utils/agents/dependency_resolver.py +368 -0
- better_notion/utils/agents/project_context.py +232 -0
- better_notion/utils/agents/rbac.py +371 -0
- better_notion/utils/agents/schemas.py +614 -0
- better_notion/utils/agents/state_machine.py +216 -0
- better_notion/utils/agents/workspace.py +371 -0
- {better_notion-0.9.9.dist-info → better_notion-1.0.1.dist-info}/METADATA +2 -2
- {better_notion-0.9.9.dist-info → better_notion-1.0.1.dist-info}/RECORD +15 -6
- {better_notion-0.9.9.dist-info → better_notion-1.0.1.dist-info}/WHEEL +0 -0
- {better_notion-0.9.9.dist-info → better_notion-1.0.1.dist-info}/entry_points.txt +0 -0
- {better_notion-0.9.9.dist-info → better_notion-1.0.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -4,10 +4,12 @@ Official plugins for Better Notion CLI.
|
|
|
4
4
|
This package contains officially maintained plugins that extend
|
|
5
5
|
the CLI with commonly-needed functionality.
|
|
6
6
|
"""
|
|
7
|
+
from better_notion.plugins.official.agents import AgentsPlugin
|
|
7
8
|
from better_notion.plugins.official.productivity import ProductivityPlugin
|
|
8
9
|
|
|
9
|
-
__all__ = ["ProductivityPlugin"]
|
|
10
|
+
__all__ = ["AgentsPlugin", "ProductivityPlugin"]
|
|
10
11
|
|
|
11
12
|
OFFICIAL_PLUGINS = [
|
|
13
|
+
AgentsPlugin,
|
|
12
14
|
ProductivityPlugin,
|
|
13
15
|
]
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
"""Official agents workflow management plugin for Better Notion CLI.
|
|
2
|
+
|
|
3
|
+
This plugin provides comprehensive workflow management capabilities for coordinating
|
|
4
|
+
AI agents working on software development projects through Notion.
|
|
5
|
+
|
|
6
|
+
Features:
|
|
7
|
+
- Workspace initialization (creates all required databases)
|
|
8
|
+
- Project context management (.notion files)
|
|
9
|
+
- Role management (set and check current role)
|
|
10
|
+
- Task discovery and execution (claim, start, complete tasks)
|
|
11
|
+
- Idea capture and management
|
|
12
|
+
- Work issue tracking
|
|
13
|
+
- Dependency resolution
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import asyncio
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import Optional
|
|
21
|
+
|
|
22
|
+
import typer
|
|
23
|
+
|
|
24
|
+
from better_notion._cli.config import Config
|
|
25
|
+
from better_notion._cli.response import format_error, format_success
|
|
26
|
+
from better_notion._sdk.client import NotionClient
|
|
27
|
+
from better_notion.plugins.base import PluginInterface
|
|
28
|
+
from better_notion.utils.agents import (
|
|
29
|
+
DependencyResolver,
|
|
30
|
+
ProjectContext,
|
|
31
|
+
RoleManager,
|
|
32
|
+
get_or_create_agent_id,
|
|
33
|
+
)
|
|
34
|
+
from better_notion.utils.agents.workspace import WorkspaceInitializer, initialize_workspace_command
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def get_client() -> NotionClient:
|
|
38
|
+
"""Get authenticated Notion client."""
|
|
39
|
+
config = Config.load()
|
|
40
|
+
return NotionClient(auth=config.token, timeout=config.timeout)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class AgentsPlugin(PluginInterface):
|
|
44
|
+
"""
|
|
45
|
+
Official agents workflow management plugin.
|
|
46
|
+
|
|
47
|
+
This plugin provides tools for managing software development workflows
|
|
48
|
+
through Notion databases, enabling AI agents to coordinate work on projects.
|
|
49
|
+
|
|
50
|
+
Commands:
|
|
51
|
+
- init: Initialize a new workspace with all databases
|
|
52
|
+
- init-project: Initialize a new project with .notion file
|
|
53
|
+
- role: Manage project role
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
def register_commands(self, app: typer.Typer) -> None:
|
|
57
|
+
"""Register plugin commands with the CLI app."""
|
|
58
|
+
# Create agents command group
|
|
59
|
+
agents_app = typer.Typer(
|
|
60
|
+
name="agents",
|
|
61
|
+
help="Agents workflow management commands",
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
# Register sub-commands
|
|
65
|
+
@agents_app.command("init")
|
|
66
|
+
def init_workspace(
|
|
67
|
+
parent_page_id: str = typer.Option(
|
|
68
|
+
...,
|
|
69
|
+
"--parent-page",
|
|
70
|
+
"-p",
|
|
71
|
+
help="ID of the parent page where databases will be created",
|
|
72
|
+
),
|
|
73
|
+
workspace_name: str = typer.Option(
|
|
74
|
+
"Agents Workspace",
|
|
75
|
+
"--name",
|
|
76
|
+
"-n",
|
|
77
|
+
help="Name for the workspace",
|
|
78
|
+
),
|
|
79
|
+
) -> None:
|
|
80
|
+
"""
|
|
81
|
+
Initialize a new workspace with all required databases.
|
|
82
|
+
|
|
83
|
+
Creates 8 databases in Notion with proper relationships:
|
|
84
|
+
- Organizations
|
|
85
|
+
- Projects
|
|
86
|
+
- Versions
|
|
87
|
+
- Tasks
|
|
88
|
+
- Ideas
|
|
89
|
+
- Work Issues
|
|
90
|
+
- Incidents
|
|
91
|
+
- Tags
|
|
92
|
+
|
|
93
|
+
Example:
|
|
94
|
+
$ notion agents init --parent-page page123 --name "My Workspace"
|
|
95
|
+
"""
|
|
96
|
+
async def _init() -> str:
|
|
97
|
+
try:
|
|
98
|
+
client = get_client()
|
|
99
|
+
initializer = WorkspaceInitializer(client)
|
|
100
|
+
|
|
101
|
+
database_ids = await initializer.initialize_workspace(
|
|
102
|
+
parent_page_id=parent_page_id,
|
|
103
|
+
workspace_name=workspace_name,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
# Save database IDs
|
|
107
|
+
initializer.save_database_ids()
|
|
108
|
+
|
|
109
|
+
return format_success(
|
|
110
|
+
{
|
|
111
|
+
"message": "Workspace initialized successfully",
|
|
112
|
+
"databases_created": len(database_ids),
|
|
113
|
+
"database_ids": database_ids,
|
|
114
|
+
}
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
except Exception as e:
|
|
118
|
+
return format_error("INIT_ERROR", str(e), retry=False)
|
|
119
|
+
|
|
120
|
+
result = asyncio.run(_init())
|
|
121
|
+
typer.echo(result)
|
|
122
|
+
|
|
123
|
+
@agents_app.command("init-project")
|
|
124
|
+
def init_project(
|
|
125
|
+
project_id: str = typer.Option(
|
|
126
|
+
...,
|
|
127
|
+
"--project-id",
|
|
128
|
+
"-i",
|
|
129
|
+
help="Notion page ID for the project",
|
|
130
|
+
),
|
|
131
|
+
project_name: str = typer.Option(
|
|
132
|
+
...,
|
|
133
|
+
"--name",
|
|
134
|
+
"-n",
|
|
135
|
+
help="Project name",
|
|
136
|
+
),
|
|
137
|
+
org_id: str = typer.Option(
|
|
138
|
+
...,
|
|
139
|
+
"--org-id",
|
|
140
|
+
"-o",
|
|
141
|
+
help="Notion page ID for the organization",
|
|
142
|
+
),
|
|
143
|
+
role: str = typer.Option(
|
|
144
|
+
"Developer",
|
|
145
|
+
"--role",
|
|
146
|
+
"-r",
|
|
147
|
+
help="Project role (default: Developer)",
|
|
148
|
+
),
|
|
149
|
+
) -> None:
|
|
150
|
+
"""
|
|
151
|
+
Initialize a new project with a .notion file.
|
|
152
|
+
|
|
153
|
+
Creates a .notion file in the current directory that identifies
|
|
154
|
+
the project context for all CLI commands.
|
|
155
|
+
|
|
156
|
+
Example:
|
|
157
|
+
$ notion agents init-project \\
|
|
158
|
+
--project-id page123 \\
|
|
159
|
+
--name "My Project" \\
|
|
160
|
+
--org-id org456 \\
|
|
161
|
+
--role Developer
|
|
162
|
+
"""
|
|
163
|
+
try:
|
|
164
|
+
# Validate role
|
|
165
|
+
if not RoleManager.is_valid_role(role):
|
|
166
|
+
result = format_error(
|
|
167
|
+
"INVALID_ROLE",
|
|
168
|
+
f"Invalid role: {role}. Valid roles: {', '.join(RoleManager.get_all_roles())}",
|
|
169
|
+
retry=False,
|
|
170
|
+
)
|
|
171
|
+
else:
|
|
172
|
+
# Create .notion file
|
|
173
|
+
context = ProjectContext.create(
|
|
174
|
+
project_id=project_id,
|
|
175
|
+
project_name=project_name,
|
|
176
|
+
org_id=org_id,
|
|
177
|
+
role=role,
|
|
178
|
+
path=Path.cwd(),
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
result = format_success(
|
|
182
|
+
{
|
|
183
|
+
"message": "Project initialized successfully",
|
|
184
|
+
"project_id": context.project_id,
|
|
185
|
+
"project_name": context.project_name,
|
|
186
|
+
"org_id": context.org_id,
|
|
187
|
+
"role": context.role,
|
|
188
|
+
"notion_file": str(Path.cwd() / ".notion"),
|
|
189
|
+
}
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
except Exception as e:
|
|
193
|
+
result = format_error("INIT_PROJECT_ERROR", str(e), retry=False)
|
|
194
|
+
|
|
195
|
+
typer.echo(result)
|
|
196
|
+
|
|
197
|
+
# Role management commands
|
|
198
|
+
role_app = typer.Typer(
|
|
199
|
+
name="role",
|
|
200
|
+
help="Role management commands",
|
|
201
|
+
)
|
|
202
|
+
agents_app.add_typer(role_app)
|
|
203
|
+
|
|
204
|
+
@role_app.command("be")
|
|
205
|
+
def role_be(
|
|
206
|
+
new_role: str = typer.Argument(..., help="Role to set"),
|
|
207
|
+
path: Optional[Path] = typer.Option(
|
|
208
|
+
None, "--path", "-p", help="Path to project directory (default: cwd)"
|
|
209
|
+
),
|
|
210
|
+
) -> None:
|
|
211
|
+
"""
|
|
212
|
+
Set the project role.
|
|
213
|
+
|
|
214
|
+
Updates the role in the .notion file. The role determines what
|
|
215
|
+
actions the agent can perform in this project.
|
|
216
|
+
|
|
217
|
+
Example:
|
|
218
|
+
$ notion agents role be PM
|
|
219
|
+
$ notion agents role be Developer --path /path/to/project
|
|
220
|
+
"""
|
|
221
|
+
try:
|
|
222
|
+
# Validate role
|
|
223
|
+
if not RoleManager.is_valid_role(new_role):
|
|
224
|
+
result = format_error(
|
|
225
|
+
"INVALID_ROLE",
|
|
226
|
+
f"Invalid role: {new_role}. Valid roles: {', '.join(RoleManager.get_all_roles())}",
|
|
227
|
+
retry=False,
|
|
228
|
+
)
|
|
229
|
+
else:
|
|
230
|
+
# Load project context
|
|
231
|
+
if path:
|
|
232
|
+
context = ProjectContext.from_path(path)
|
|
233
|
+
else:
|
|
234
|
+
context = ProjectContext.from_current_directory()
|
|
235
|
+
|
|
236
|
+
if not context:
|
|
237
|
+
result = format_error(
|
|
238
|
+
"NO_PROJECT_CONTEXT",
|
|
239
|
+
"No .notion file found. Are you in a project directory?",
|
|
240
|
+
retry=False,
|
|
241
|
+
)
|
|
242
|
+
else:
|
|
243
|
+
# Update role
|
|
244
|
+
context.update_role(new_role, path=path or None)
|
|
245
|
+
|
|
246
|
+
result = format_success(
|
|
247
|
+
{
|
|
248
|
+
"message": f"Role updated to {new_role}",
|
|
249
|
+
"previous_role": context.role,
|
|
250
|
+
"new_role": new_role,
|
|
251
|
+
}
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
except Exception as e:
|
|
255
|
+
result = format_error("ROLE_UPDATE_ERROR", str(e), retry=False)
|
|
256
|
+
|
|
257
|
+
typer.echo(result)
|
|
258
|
+
|
|
259
|
+
@role_app.command("whoami")
|
|
260
|
+
def role_whoami(
|
|
261
|
+
path: Optional[Path] = typer.Option(
|
|
262
|
+
None, "--path", "-p", help="Path to project directory (default: cwd)"
|
|
263
|
+
),
|
|
264
|
+
) -> None:
|
|
265
|
+
"""
|
|
266
|
+
Show the current project role.
|
|
267
|
+
|
|
268
|
+
Displays the role from the .notion file in the current or
|
|
269
|
+
specified project directory.
|
|
270
|
+
|
|
271
|
+
Example:
|
|
272
|
+
$ notion agents role whoami
|
|
273
|
+
$ notion agents role whoami --path /path/to/project
|
|
274
|
+
"""
|
|
275
|
+
try:
|
|
276
|
+
# Load project context
|
|
277
|
+
if path:
|
|
278
|
+
context = ProjectContext.from_path(path)
|
|
279
|
+
else:
|
|
280
|
+
context = ProjectContext.from_current_directory()
|
|
281
|
+
|
|
282
|
+
if not context:
|
|
283
|
+
result = format_error(
|
|
284
|
+
"NO_PROJECT_CONTEXT",
|
|
285
|
+
"No .notion file found. Are you in a project directory?",
|
|
286
|
+
retry=False,
|
|
287
|
+
)
|
|
288
|
+
else:
|
|
289
|
+
# Get role description
|
|
290
|
+
description = RoleManager.get_role_description(context.role)
|
|
291
|
+
|
|
292
|
+
result = format_success(
|
|
293
|
+
{
|
|
294
|
+
"role": context.role,
|
|
295
|
+
"description": description,
|
|
296
|
+
"project": context.project_name,
|
|
297
|
+
"permissions": RoleManager.get_permissions(context.role),
|
|
298
|
+
}
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
except Exception as e:
|
|
302
|
+
result = format_error("ROLE_ERROR", str(e), retry=False)
|
|
303
|
+
|
|
304
|
+
typer.echo(result)
|
|
305
|
+
|
|
306
|
+
@role_app.command("list")
|
|
307
|
+
def role_list() -> None:
|
|
308
|
+
"""
|
|
309
|
+
List all available roles.
|
|
310
|
+
|
|
311
|
+
Shows all valid roles that can be used in the workflow system.
|
|
312
|
+
|
|
313
|
+
Example:
|
|
314
|
+
$ notion agents role list
|
|
315
|
+
"""
|
|
316
|
+
try:
|
|
317
|
+
roles = RoleManager.get_all_roles()
|
|
318
|
+
|
|
319
|
+
role_info = []
|
|
320
|
+
for role in roles:
|
|
321
|
+
description = RoleManager.get_role_description(role)
|
|
322
|
+
permissions = RoleManager.get_permissions(role)
|
|
323
|
+
role_info.append(
|
|
324
|
+
{
|
|
325
|
+
"role": role,
|
|
326
|
+
"description": description,
|
|
327
|
+
"permission_count": len(permissions),
|
|
328
|
+
}
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
result = format_success(
|
|
332
|
+
{
|
|
333
|
+
"roles": role_info,
|
|
334
|
+
"total": len(roles),
|
|
335
|
+
}
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
except Exception as e:
|
|
339
|
+
result = format_error("ROLE_LIST_ERROR", str(e), retry=False)
|
|
340
|
+
|
|
341
|
+
typer.echo(result)
|
|
342
|
+
|
|
343
|
+
# Register agents app to main CLI
|
|
344
|
+
app.add_typer(agents_app)
|
|
345
|
+
|
|
346
|
+
def get_info(self) -> dict[str, str | bool | list]:
|
|
347
|
+
"""Return plugin metadata."""
|
|
348
|
+
return {
|
|
349
|
+
"name": "agents",
|
|
350
|
+
"version": "1.0.0",
|
|
351
|
+
"description": "Workflow management system for AI agents coordinating on software development projects",
|
|
352
|
+
"author": "Better Notion Team",
|
|
353
|
+
"official": True,
|
|
354
|
+
"category": "workflow",
|
|
355
|
+
"dependencies": [],
|
|
356
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Utility functions and classes for agents workflow management."""
|
|
2
|
+
|
|
3
|
+
from better_notion.utils.agents.auth import (
|
|
4
|
+
AgentContext,
|
|
5
|
+
clear_agent_id,
|
|
6
|
+
get_agent_id_path,
|
|
7
|
+
get_agent_info,
|
|
8
|
+
get_or_create_agent_id,
|
|
9
|
+
is_valid_agent_id,
|
|
10
|
+
set_agent_id,
|
|
11
|
+
)
|
|
12
|
+
from better_notion.utils.agents.dependency_resolver import DependencyResolver
|
|
13
|
+
from better_notion.utils.agents.project_context import ProjectContext
|
|
14
|
+
from better_notion.utils.agents.rbac import RoleManager
|
|
15
|
+
from better_notion.utils.agents.schemas import (
|
|
16
|
+
IncidentSchema,
|
|
17
|
+
IdeaSchema,
|
|
18
|
+
OrganizationSchema,
|
|
19
|
+
ProjectSchema,
|
|
20
|
+
PropertyBuilder,
|
|
21
|
+
SelectOption,
|
|
22
|
+
TagSchema,
|
|
23
|
+
TaskSchema,
|
|
24
|
+
VersionSchema,
|
|
25
|
+
WorkIssueSchema,
|
|
26
|
+
)
|
|
27
|
+
from better_notion.utils.agents.state_machine import TaskStatus, TaskStateMachine
|
|
28
|
+
from better_notion.utils.agents.workspace import (
|
|
29
|
+
WorkspaceInitializer,
|
|
30
|
+
initialize_workspace_command,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
__all__ = [
|
|
34
|
+
# Agent authentication
|
|
35
|
+
"AgentContext",
|
|
36
|
+
"clear_agent_id",
|
|
37
|
+
"get_agent_id_path",
|
|
38
|
+
"get_agent_info",
|
|
39
|
+
"get_or_create_agent_id",
|
|
40
|
+
"is_valid_agent_id",
|
|
41
|
+
"set_agent_id",
|
|
42
|
+
# Dependency resolution
|
|
43
|
+
"DependencyResolver",
|
|
44
|
+
# Project context
|
|
45
|
+
"ProjectContext",
|
|
46
|
+
# Role-based access control
|
|
47
|
+
"RoleManager",
|
|
48
|
+
# Database schemas
|
|
49
|
+
"IncidentSchema",
|
|
50
|
+
"IdeaSchema",
|
|
51
|
+
"OrganizationSchema",
|
|
52
|
+
"ProjectSchema",
|
|
53
|
+
"PropertyBuilder",
|
|
54
|
+
"SelectOption",
|
|
55
|
+
"TagSchema",
|
|
56
|
+
"TaskSchema",
|
|
57
|
+
"VersionSchema",
|
|
58
|
+
"WorkIssueSchema",
|
|
59
|
+
# State machine
|
|
60
|
+
"TaskStatus",
|
|
61
|
+
"TaskStateMachine",
|
|
62
|
+
# Workspace initialization
|
|
63
|
+
"WorkspaceInitializer",
|
|
64
|
+
"initialize_workspace_command",
|
|
65
|
+
]
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
"""Agent authentication and identification for the agents workflow system.
|
|
2
|
+
|
|
3
|
+
This module provides agent identification and tracking functionality. Each agent
|
|
4
|
+
gets a unique ID that is stored locally and used for tracking operations across
|
|
5
|
+
workflow commands.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import uuid
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Optional
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# Default path for agent ID storage
|
|
15
|
+
AGENT_ID_FILE = Path.home() / ".notion" / "agent_id"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def get_agent_id_path() -> Path:
|
|
19
|
+
"""Get the path to the agent ID file.
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
Path to the agent ID file
|
|
23
|
+
|
|
24
|
+
Example:
|
|
25
|
+
>>> path = get_agent_id_path()
|
|
26
|
+
>>> print(path)
|
|
27
|
+
PosixPath('/Users/user/.notion/agent_id')
|
|
28
|
+
"""
|
|
29
|
+
return AGENT_ID_FILE
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def get_or_create_agent_id() -> str:
|
|
33
|
+
"""Get existing agent ID or create a new one.
|
|
34
|
+
|
|
35
|
+
This function checks if an agent ID file exists in ~/.notion/agent_id.
|
|
36
|
+
If it exists, it reads and returns the ID. If not, it generates a new
|
|
37
|
+
UUID-based agent ID and stores it.
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
Agent ID string (format: "agent-{uuid}")
|
|
41
|
+
|
|
42
|
+
Example:
|
|
43
|
+
>>> agent_id = get_or_create_agent_id()
|
|
44
|
+
>>> print(agent_id)
|
|
45
|
+
'agent-1a2b3c4d-5e6f-7g8h-9i0j-1k2l3m4n5o6p'
|
|
46
|
+
"""
|
|
47
|
+
# Ensure directory exists
|
|
48
|
+
AGENT_ID_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
49
|
+
|
|
50
|
+
# Check if agent ID file exists
|
|
51
|
+
if AGENT_ID_FILE.exists() and AGENT_ID_FILE.is_file():
|
|
52
|
+
try:
|
|
53
|
+
with open(AGENT_ID_FILE, encoding="utf-8") as f:
|
|
54
|
+
content = f.read().strip()
|
|
55
|
+
|
|
56
|
+
if content:
|
|
57
|
+
return content
|
|
58
|
+
|
|
59
|
+
except (IOError, OSError):
|
|
60
|
+
# File exists but can't be read
|
|
61
|
+
# Fall through to create new ID
|
|
62
|
+
pass
|
|
63
|
+
|
|
64
|
+
# Generate new agent ID
|
|
65
|
+
agent_id = f"agent-{uuid.uuid4()}"
|
|
66
|
+
|
|
67
|
+
# Save to file
|
|
68
|
+
try:
|
|
69
|
+
with open(AGENT_ID_FILE, "w", encoding="utf-8") as f:
|
|
70
|
+
f.write(agent_id)
|
|
71
|
+
except (IOError, OSError) as e:
|
|
72
|
+
# Can't save agent ID - log warning but continue
|
|
73
|
+
# The agent can still function with a temporary ID
|
|
74
|
+
pass
|
|
75
|
+
|
|
76
|
+
return agent_id
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def set_agent_id(agent_id: str) -> bool:
|
|
80
|
+
"""Set a specific agent ID (useful for testing or manual configuration).
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
agent_id: Agent ID to set
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
True if agent ID was saved successfully, False otherwise
|
|
87
|
+
|
|
88
|
+
Example:
|
|
89
|
+
>>> success = set_agent_id("agent-custom-id-123")
|
|
90
|
+
>>> print(success)
|
|
91
|
+
True
|
|
92
|
+
"""
|
|
93
|
+
# Ensure directory exists
|
|
94
|
+
AGENT_ID_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
95
|
+
|
|
96
|
+
try:
|
|
97
|
+
with open(AGENT_ID_FILE, "w", encoding="utf-8") as f:
|
|
98
|
+
f.write(agent_id.strip())
|
|
99
|
+
|
|
100
|
+
return True
|
|
101
|
+
|
|
102
|
+
except (IOError, OSError):
|
|
103
|
+
return False
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def clear_agent_id() -> bool:
|
|
107
|
+
"""Clear the stored agent ID.
|
|
108
|
+
|
|
109
|
+
This is useful for testing or when you want to generate a fresh agent ID.
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
True if agent ID was cleared successfully, False otherwise
|
|
113
|
+
|
|
114
|
+
Example:
|
|
115
|
+
>>> clear_agent_id()
|
|
116
|
+
True
|
|
117
|
+
>>> new_id = get_or_create_agent_id()
|
|
118
|
+
>>> # Will generate a new ID
|
|
119
|
+
"""
|
|
120
|
+
try:
|
|
121
|
+
if AGENT_ID_FILE.exists():
|
|
122
|
+
AGENT_ID_FILE.unlink()
|
|
123
|
+
|
|
124
|
+
return True
|
|
125
|
+
|
|
126
|
+
except (IOError, OSError):
|
|
127
|
+
return False
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def is_valid_agent_id(agent_id: str) -> bool:
|
|
131
|
+
"""Check if a string is a valid agent ID format.
|
|
132
|
+
|
|
133
|
+
Valid agent IDs must start with "agent-" followed by a UUID.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
agent_id: Agent ID string to validate
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
True if agent ID is valid format, False otherwise
|
|
140
|
+
|
|
141
|
+
Example:
|
|
142
|
+
>>> is_valid_agent_id("agent-1a2b3c4d-5e6f-7g8h-9i0j-1k2l3m4n5o6p")
|
|
143
|
+
True
|
|
144
|
+
|
|
145
|
+
>>> is_valid_agent_id("invalid-id")
|
|
146
|
+
False
|
|
147
|
+
"""
|
|
148
|
+
if not agent_id.startswith("agent-"):
|
|
149
|
+
return False
|
|
150
|
+
|
|
151
|
+
# Extract UUID part
|
|
152
|
+
uuid_part = agent_id[6:] # Remove "agent-" prefix
|
|
153
|
+
|
|
154
|
+
try:
|
|
155
|
+
# Try to parse as UUID
|
|
156
|
+
uuid.UUID(uuid_part)
|
|
157
|
+
return True
|
|
158
|
+
|
|
159
|
+
except ValueError:
|
|
160
|
+
return False
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def get_agent_info() -> dict[str, str]:
|
|
164
|
+
"""Get complete agent information including ID and metadata.
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
Dict with agent information:
|
|
168
|
+
- agent_id: The agent's unique identifier
|
|
169
|
+
- agent_id_exists: Whether the agent ID file exists
|
|
170
|
+
- agent_id_path: Path to the agent ID file
|
|
171
|
+
|
|
172
|
+
Example:
|
|
173
|
+
>>> info = get_agent_info()
|
|
174
|
+
>>> print(info['agent_id'])
|
|
175
|
+
'agent-1a2b3c4d-5e6f-7g8h-9i0j-1k2l3m4n5o6p'
|
|
176
|
+
"""
|
|
177
|
+
return {
|
|
178
|
+
"agent_id": get_or_create_agent_id(),
|
|
179
|
+
"agent_id_exists": AGENT_ID_FILE.exists(),
|
|
180
|
+
"agent_id_path": str(AGENT_ID_FILE),
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class AgentContext:
|
|
185
|
+
"""Context manager for temporary agent ID overrides.
|
|
186
|
+
|
|
187
|
+
This is useful for testing when you want to temporarily use a different
|
|
188
|
+
agent ID without affecting the stored ID.
|
|
189
|
+
|
|
190
|
+
Example:
|
|
191
|
+
>>> with AgentContext("agent-test-123"):
|
|
192
|
+
... agent_id = get_or_create_agent_id()
|
|
193
|
+
... print(agent_id) # "agent-test-123"
|
|
194
|
+
>>> # Back to normal agent ID
|
|
195
|
+
"""
|
|
196
|
+
|
|
197
|
+
def __init__(self, temp_agent_id: str):
|
|
198
|
+
"""Initialize the context manager.
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
temp_agent_id: Temporary agent ID to use during context
|
|
202
|
+
"""
|
|
203
|
+
self.temp_agent_id = temp_agent_id
|
|
204
|
+
self.original_id: Optional[str] = None
|
|
205
|
+
self.file_existed: bool = False
|
|
206
|
+
|
|
207
|
+
def __enter__(self) -> "AgentContext":
|
|
208
|
+
"""Enter the context and save original agent ID."""
|
|
209
|
+
# Save original ID if file exists
|
|
210
|
+
if AGENT_ID_FILE.exists():
|
|
211
|
+
self.file_existed = True
|
|
212
|
+
try:
|
|
213
|
+
with open(AGENT_ID_FILE, encoding="utf-8") as f:
|
|
214
|
+
self.original_id = f.read().strip()
|
|
215
|
+
except (IOError, OSError):
|
|
216
|
+
self.original_id = None
|
|
217
|
+
|
|
218
|
+
# Set temporary ID
|
|
219
|
+
set_agent_id(self.temp_agent_id)
|
|
220
|
+
|
|
221
|
+
return self
|
|
222
|
+
|
|
223
|
+
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
224
|
+
"""Exit the context and restore original agent ID."""
|
|
225
|
+
if self.original_id:
|
|
226
|
+
# Restore original ID
|
|
227
|
+
set_agent_id(self.original_id)
|
|
228
|
+
elif self.file_existed:
|
|
229
|
+
# File existed but couldn't read - clear it
|
|
230
|
+
clear_agent_id()
|
|
231
|
+
else:
|
|
232
|
+
# File didn't exist - remove the temp file
|
|
233
|
+
clear_agent_id()
|
|
234
|
+
|
|
235
|
+
return None
|