acontext 0.0.5__tar.gz → 0.0.7__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.
- {acontext-0.0.5 → acontext-0.0.7}/PKG-INFO +140 -37
- {acontext-0.0.5 → acontext-0.0.7}/README.md +139 -36
- {acontext-0.0.5 → acontext-0.0.7}/pyproject.toml +1 -1
- acontext-0.0.7/src/acontext/agent/base.py +89 -0
- acontext-0.0.7/src/acontext/agent/disk.py +325 -0
- acontext-0.0.7/src/acontext/py.typed +0 -0
- {acontext-0.0.5 → acontext-0.0.7}/src/acontext/resources/async_spaces.py +34 -32
- {acontext-0.0.5 → acontext-0.0.7}/src/acontext/resources/spaces.py +34 -32
- {acontext-0.0.5 → acontext-0.0.7}/src/acontext/types/__init__.py +4 -0
- {acontext-0.0.5 → acontext-0.0.7}/src/acontext/types/space.py +23 -0
- {acontext-0.0.5 → acontext-0.0.7}/src/acontext/__init__.py +0 -0
- {acontext-0.0.5 → acontext-0.0.7}/src/acontext/_constants.py +0 -0
- {acontext-0.0.5 → acontext-0.0.7}/src/acontext/_utils.py +0 -0
- /acontext-0.0.5/src/acontext/py.typed → /acontext-0.0.7/src/acontext/agent/__init__.py +0 -0
- {acontext-0.0.5 → acontext-0.0.7}/src/acontext/async_client.py +0 -0
- {acontext-0.0.5 → acontext-0.0.7}/src/acontext/client.py +0 -0
- {acontext-0.0.5 → acontext-0.0.7}/src/acontext/client_types.py +0 -0
- {acontext-0.0.5 → acontext-0.0.7}/src/acontext/errors.py +0 -0
- {acontext-0.0.5 → acontext-0.0.7}/src/acontext/messages.py +0 -0
- {acontext-0.0.5 → acontext-0.0.7}/src/acontext/resources/__init__.py +0 -0
- {acontext-0.0.5 → acontext-0.0.7}/src/acontext/resources/async_blocks.py +0 -0
- {acontext-0.0.5 → acontext-0.0.7}/src/acontext/resources/async_disks.py +0 -0
- {acontext-0.0.5 → acontext-0.0.7}/src/acontext/resources/async_sessions.py +0 -0
- {acontext-0.0.5 → acontext-0.0.7}/src/acontext/resources/async_tools.py +0 -0
- {acontext-0.0.5 → acontext-0.0.7}/src/acontext/resources/blocks.py +0 -0
- {acontext-0.0.5 → acontext-0.0.7}/src/acontext/resources/disks.py +0 -0
- {acontext-0.0.5 → acontext-0.0.7}/src/acontext/resources/sessions.py +0 -0
- {acontext-0.0.5 → acontext-0.0.7}/src/acontext/resources/tools.py +0 -0
- {acontext-0.0.5 → acontext-0.0.7}/src/acontext/types/block.py +0 -0
- {acontext-0.0.5 → acontext-0.0.7}/src/acontext/types/disk.py +0 -0
- {acontext-0.0.5 → acontext-0.0.7}/src/acontext/types/session.py +0 -0
- {acontext-0.0.5 → acontext-0.0.7}/src/acontext/types/tool.py +0 -0
- {acontext-0.0.5 → acontext-0.0.7}/src/acontext/uploads.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: acontext
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.7
|
|
4
4
|
Summary: Python SDK for the Acontext API
|
|
5
5
|
Keywords: acontext,sdk,client,api
|
|
6
6
|
Requires-Dist: httpx>=0.28.1
|
|
@@ -304,6 +304,144 @@ result = client.tools.rename_tool_name(
|
|
|
304
304
|
print(result.status) # 0 for success
|
|
305
305
|
```
|
|
306
306
|
|
|
307
|
+
### Agent Tools
|
|
308
|
+
|
|
309
|
+
The SDK provides agent tools that allow LLMs (OpenAI, Anthropic) to interact with Acontext disks through function calling. These tools can be converted to OpenAI or Anthropic tool schemas and executed when the LLM calls them.
|
|
310
|
+
|
|
311
|
+
#### Pre-configured Disk Tools
|
|
312
|
+
|
|
313
|
+
The SDK includes a pre-configured `DISK_TOOLS` pool with four disk operation tools:
|
|
314
|
+
|
|
315
|
+
- **`write_file`**: Write text content to a file
|
|
316
|
+
- **`read_file`**: Read a text file with optional line offset and limit
|
|
317
|
+
- **`replace_string`**: Replace strings in a file
|
|
318
|
+
- **`list_artifacts`**: List files and directories in a path
|
|
319
|
+
|
|
320
|
+
#### Getting Tool Schemas for LLM APIs
|
|
321
|
+
|
|
322
|
+
Convert tools to the appropriate format for your LLM provider:
|
|
323
|
+
|
|
324
|
+
```python
|
|
325
|
+
from acontext import AcontextClient
|
|
326
|
+
from acontext.agent.disk import DISK_TOOLS
|
|
327
|
+
|
|
328
|
+
client = AcontextClient(api_key="sk-ac-your-root-api-bearer-token")
|
|
329
|
+
|
|
330
|
+
# Get OpenAI-compatible tool schemas
|
|
331
|
+
openai_tools = DISK_TOOLS.to_openai_tool_schema()
|
|
332
|
+
|
|
333
|
+
# Get Anthropic-compatible tool schemas
|
|
334
|
+
anthropic_tools = DISK_TOOLS.to_anthropic_tool_schema()
|
|
335
|
+
|
|
336
|
+
# Use with OpenAI API
|
|
337
|
+
import openai
|
|
338
|
+
openai_client = openai.OpenAI(api_key="your-openai-key")
|
|
339
|
+
completion = openai_client.chat.completions.create(
|
|
340
|
+
model="gpt-4",
|
|
341
|
+
messages=[{"role": "user", "content": 'Write a file called hello.txt with "Hello, World!"'}],
|
|
342
|
+
tools=openai_tools,
|
|
343
|
+
)
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
#### Executing Tools
|
|
347
|
+
|
|
348
|
+
When an LLM calls a tool, execute it using the tool pool:
|
|
349
|
+
|
|
350
|
+
```python
|
|
351
|
+
from acontext import AcontextClient
|
|
352
|
+
from acontext.agent.disk import DISK_TOOLS
|
|
353
|
+
|
|
354
|
+
client = AcontextClient(api_key="sk-ac-your-root-api-bearer-token")
|
|
355
|
+
|
|
356
|
+
# Create a disk for the tools to operate on
|
|
357
|
+
disk = client.disks.create()
|
|
358
|
+
|
|
359
|
+
# Create a context for the tools
|
|
360
|
+
ctx = DISK_TOOLS.format_context(client, disk.id)
|
|
361
|
+
|
|
362
|
+
# Execute a tool (e.g., after LLM returns a tool call)
|
|
363
|
+
result = DISK_TOOLS.execute_tool(
|
|
364
|
+
ctx,
|
|
365
|
+
"write_file",
|
|
366
|
+
{"filename": "hello.txt", "file_path": "/notes/", "content": "Hello, World!"}
|
|
367
|
+
)
|
|
368
|
+
print(result) # File 'hello.txt' written successfully to '/notes/hello.txt'
|
|
369
|
+
|
|
370
|
+
# Read the file
|
|
371
|
+
read_result = DISK_TOOLS.execute_tool(
|
|
372
|
+
ctx,
|
|
373
|
+
"read_file",
|
|
374
|
+
{"filename": "hello.txt", "file_path": "/notes/"}
|
|
375
|
+
)
|
|
376
|
+
print(read_result)
|
|
377
|
+
|
|
378
|
+
# List files in a directory
|
|
379
|
+
list_result = DISK_TOOLS.execute_tool(
|
|
380
|
+
ctx,
|
|
381
|
+
"list_artifacts",
|
|
382
|
+
{"file_path": "/notes/"}
|
|
383
|
+
)
|
|
384
|
+
print(list_result)
|
|
385
|
+
|
|
386
|
+
# Replace a string in a file
|
|
387
|
+
replace_result = DISK_TOOLS.execute_tool(
|
|
388
|
+
ctx,
|
|
389
|
+
"replace_string",
|
|
390
|
+
{
|
|
391
|
+
"filename": "hello.txt",
|
|
392
|
+
"file_path": "/notes/",
|
|
393
|
+
"old_string": "Hello",
|
|
394
|
+
"new_string": "Hi",
|
|
395
|
+
}
|
|
396
|
+
)
|
|
397
|
+
print(replace_result)
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
#### Creating Custom Tools
|
|
401
|
+
|
|
402
|
+
You can create custom tools by extending `BaseTool`:
|
|
403
|
+
|
|
404
|
+
```python
|
|
405
|
+
from acontext.agent.base import BaseTool, BaseToolPool, BaseContext
|
|
406
|
+
from typing import Dict, Any
|
|
407
|
+
|
|
408
|
+
class MyCustomTool(BaseTool):
|
|
409
|
+
@property
|
|
410
|
+
def name(self) -> str:
|
|
411
|
+
return "my_custom_tool"
|
|
412
|
+
|
|
413
|
+
@property
|
|
414
|
+
def description(self) -> str:
|
|
415
|
+
return "A custom tool that does something"
|
|
416
|
+
|
|
417
|
+
@property
|
|
418
|
+
def arguments(self) -> dict:
|
|
419
|
+
return {
|
|
420
|
+
"param1": {
|
|
421
|
+
"type": "string",
|
|
422
|
+
"description": "First parameter",
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
@property
|
|
427
|
+
def required_arguments(self) -> list[str]:
|
|
428
|
+
return ["param1"]
|
|
429
|
+
|
|
430
|
+
def execute(self, ctx: BaseContext, llm_arguments: dict) -> str:
|
|
431
|
+
param1 = llm_arguments.get("param1")
|
|
432
|
+
# Your custom logic here
|
|
433
|
+
return f"Result: {param1}"
|
|
434
|
+
|
|
435
|
+
# Create a custom tool pool
|
|
436
|
+
class MyToolPool(BaseToolPool):
|
|
437
|
+
def format_context(self, *args, **kwargs) -> BaseContext:
|
|
438
|
+
# Create and return your context
|
|
439
|
+
return BaseContext()
|
|
440
|
+
|
|
441
|
+
my_pool = MyToolPool()
|
|
442
|
+
my_pool.add_tool(MyCustomTool())
|
|
443
|
+
```
|
|
444
|
+
|
|
307
445
|
### Blocks API
|
|
308
446
|
|
|
309
447
|
#### List blocks
|
|
@@ -484,7 +622,7 @@ for artifact in artifacts.items:
|
|
|
484
622
|
|
|
485
623
|
### Semantic search within spaces
|
|
486
624
|
|
|
487
|
-
The SDK provides
|
|
625
|
+
The SDK provides a powerful semantic search API for finding content within your spaces:
|
|
488
626
|
|
|
489
627
|
#### 1. Experience Search (Advanced AI-powered search)
|
|
490
628
|
|
|
@@ -520,39 +658,4 @@ if result.final_answer:
|
|
|
520
658
|
print(f"AI Answer: {result.final_answer}")
|
|
521
659
|
```
|
|
522
660
|
|
|
523
|
-
#### 2. Semantic Glob (Search page/folder titles)
|
|
524
|
-
|
|
525
|
-
Search for pages and folders by their titles using semantic similarity (like a semantic version of `glob`):
|
|
526
|
-
|
|
527
|
-
```python
|
|
528
|
-
# Find pages about authentication
|
|
529
|
-
results = client.spaces.semantic_glob(
|
|
530
|
-
space_id="space-uuid",
|
|
531
|
-
query="authentication and authorization pages",
|
|
532
|
-
limit=10,
|
|
533
|
-
threshold=1.0, # Only show results with distance < 1.0
|
|
534
|
-
)
|
|
535
|
-
|
|
536
|
-
for block in results:
|
|
537
|
-
print(f"{block.title} - {block.type}")
|
|
538
|
-
```
|
|
539
|
-
|
|
540
|
-
#### 3. Semantic Grep (Search content blocks)
|
|
541
|
-
|
|
542
|
-
Search through actual content blocks using semantic similarity (like a semantic version of `grep`):
|
|
543
|
-
|
|
544
|
-
```python
|
|
545
|
-
# Find code examples for JWT validation
|
|
546
|
-
results = client.spaces.semantic_grep(
|
|
547
|
-
space_id="space-uuid",
|
|
548
|
-
query="JWT token validation code examples",
|
|
549
|
-
limit=15,
|
|
550
|
-
threshold=0.7,
|
|
551
|
-
)
|
|
552
|
-
|
|
553
|
-
for block in results:
|
|
554
|
-
print(f"{block.title} - distance: {block.distance}")
|
|
555
|
-
print(f"Content: {block.props.get('text', '')[:100]}...")
|
|
556
|
-
```
|
|
557
|
-
|
|
558
661
|
See `examples/search_usage.py` for more detailed examples including async usage.
|
|
@@ -289,6 +289,144 @@ result = client.tools.rename_tool_name(
|
|
|
289
289
|
print(result.status) # 0 for success
|
|
290
290
|
```
|
|
291
291
|
|
|
292
|
+
### Agent Tools
|
|
293
|
+
|
|
294
|
+
The SDK provides agent tools that allow LLMs (OpenAI, Anthropic) to interact with Acontext disks through function calling. These tools can be converted to OpenAI or Anthropic tool schemas and executed when the LLM calls them.
|
|
295
|
+
|
|
296
|
+
#### Pre-configured Disk Tools
|
|
297
|
+
|
|
298
|
+
The SDK includes a pre-configured `DISK_TOOLS` pool with four disk operation tools:
|
|
299
|
+
|
|
300
|
+
- **`write_file`**: Write text content to a file
|
|
301
|
+
- **`read_file`**: Read a text file with optional line offset and limit
|
|
302
|
+
- **`replace_string`**: Replace strings in a file
|
|
303
|
+
- **`list_artifacts`**: List files and directories in a path
|
|
304
|
+
|
|
305
|
+
#### Getting Tool Schemas for LLM APIs
|
|
306
|
+
|
|
307
|
+
Convert tools to the appropriate format for your LLM provider:
|
|
308
|
+
|
|
309
|
+
```python
|
|
310
|
+
from acontext import AcontextClient
|
|
311
|
+
from acontext.agent.disk import DISK_TOOLS
|
|
312
|
+
|
|
313
|
+
client = AcontextClient(api_key="sk-ac-your-root-api-bearer-token")
|
|
314
|
+
|
|
315
|
+
# Get OpenAI-compatible tool schemas
|
|
316
|
+
openai_tools = DISK_TOOLS.to_openai_tool_schema()
|
|
317
|
+
|
|
318
|
+
# Get Anthropic-compatible tool schemas
|
|
319
|
+
anthropic_tools = DISK_TOOLS.to_anthropic_tool_schema()
|
|
320
|
+
|
|
321
|
+
# Use with OpenAI API
|
|
322
|
+
import openai
|
|
323
|
+
openai_client = openai.OpenAI(api_key="your-openai-key")
|
|
324
|
+
completion = openai_client.chat.completions.create(
|
|
325
|
+
model="gpt-4",
|
|
326
|
+
messages=[{"role": "user", "content": 'Write a file called hello.txt with "Hello, World!"'}],
|
|
327
|
+
tools=openai_tools,
|
|
328
|
+
)
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
#### Executing Tools
|
|
332
|
+
|
|
333
|
+
When an LLM calls a tool, execute it using the tool pool:
|
|
334
|
+
|
|
335
|
+
```python
|
|
336
|
+
from acontext import AcontextClient
|
|
337
|
+
from acontext.agent.disk import DISK_TOOLS
|
|
338
|
+
|
|
339
|
+
client = AcontextClient(api_key="sk-ac-your-root-api-bearer-token")
|
|
340
|
+
|
|
341
|
+
# Create a disk for the tools to operate on
|
|
342
|
+
disk = client.disks.create()
|
|
343
|
+
|
|
344
|
+
# Create a context for the tools
|
|
345
|
+
ctx = DISK_TOOLS.format_context(client, disk.id)
|
|
346
|
+
|
|
347
|
+
# Execute a tool (e.g., after LLM returns a tool call)
|
|
348
|
+
result = DISK_TOOLS.execute_tool(
|
|
349
|
+
ctx,
|
|
350
|
+
"write_file",
|
|
351
|
+
{"filename": "hello.txt", "file_path": "/notes/", "content": "Hello, World!"}
|
|
352
|
+
)
|
|
353
|
+
print(result) # File 'hello.txt' written successfully to '/notes/hello.txt'
|
|
354
|
+
|
|
355
|
+
# Read the file
|
|
356
|
+
read_result = DISK_TOOLS.execute_tool(
|
|
357
|
+
ctx,
|
|
358
|
+
"read_file",
|
|
359
|
+
{"filename": "hello.txt", "file_path": "/notes/"}
|
|
360
|
+
)
|
|
361
|
+
print(read_result)
|
|
362
|
+
|
|
363
|
+
# List files in a directory
|
|
364
|
+
list_result = DISK_TOOLS.execute_tool(
|
|
365
|
+
ctx,
|
|
366
|
+
"list_artifacts",
|
|
367
|
+
{"file_path": "/notes/"}
|
|
368
|
+
)
|
|
369
|
+
print(list_result)
|
|
370
|
+
|
|
371
|
+
# Replace a string in a file
|
|
372
|
+
replace_result = DISK_TOOLS.execute_tool(
|
|
373
|
+
ctx,
|
|
374
|
+
"replace_string",
|
|
375
|
+
{
|
|
376
|
+
"filename": "hello.txt",
|
|
377
|
+
"file_path": "/notes/",
|
|
378
|
+
"old_string": "Hello",
|
|
379
|
+
"new_string": "Hi",
|
|
380
|
+
}
|
|
381
|
+
)
|
|
382
|
+
print(replace_result)
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
#### Creating Custom Tools
|
|
386
|
+
|
|
387
|
+
You can create custom tools by extending `BaseTool`:
|
|
388
|
+
|
|
389
|
+
```python
|
|
390
|
+
from acontext.agent.base import BaseTool, BaseToolPool, BaseContext
|
|
391
|
+
from typing import Dict, Any
|
|
392
|
+
|
|
393
|
+
class MyCustomTool(BaseTool):
|
|
394
|
+
@property
|
|
395
|
+
def name(self) -> str:
|
|
396
|
+
return "my_custom_tool"
|
|
397
|
+
|
|
398
|
+
@property
|
|
399
|
+
def description(self) -> str:
|
|
400
|
+
return "A custom tool that does something"
|
|
401
|
+
|
|
402
|
+
@property
|
|
403
|
+
def arguments(self) -> dict:
|
|
404
|
+
return {
|
|
405
|
+
"param1": {
|
|
406
|
+
"type": "string",
|
|
407
|
+
"description": "First parameter",
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
@property
|
|
412
|
+
def required_arguments(self) -> list[str]:
|
|
413
|
+
return ["param1"]
|
|
414
|
+
|
|
415
|
+
def execute(self, ctx: BaseContext, llm_arguments: dict) -> str:
|
|
416
|
+
param1 = llm_arguments.get("param1")
|
|
417
|
+
# Your custom logic here
|
|
418
|
+
return f"Result: {param1}"
|
|
419
|
+
|
|
420
|
+
# Create a custom tool pool
|
|
421
|
+
class MyToolPool(BaseToolPool):
|
|
422
|
+
def format_context(self, *args, **kwargs) -> BaseContext:
|
|
423
|
+
# Create and return your context
|
|
424
|
+
return BaseContext()
|
|
425
|
+
|
|
426
|
+
my_pool = MyToolPool()
|
|
427
|
+
my_pool.add_tool(MyCustomTool())
|
|
428
|
+
```
|
|
429
|
+
|
|
292
430
|
### Blocks API
|
|
293
431
|
|
|
294
432
|
#### List blocks
|
|
@@ -469,7 +607,7 @@ for artifact in artifacts.items:
|
|
|
469
607
|
|
|
470
608
|
### Semantic search within spaces
|
|
471
609
|
|
|
472
|
-
The SDK provides
|
|
610
|
+
The SDK provides a powerful semantic search API for finding content within your spaces:
|
|
473
611
|
|
|
474
612
|
#### 1. Experience Search (Advanced AI-powered search)
|
|
475
613
|
|
|
@@ -505,39 +643,4 @@ if result.final_answer:
|
|
|
505
643
|
print(f"AI Answer: {result.final_answer}")
|
|
506
644
|
```
|
|
507
645
|
|
|
508
|
-
#### 2. Semantic Glob (Search page/folder titles)
|
|
509
|
-
|
|
510
|
-
Search for pages and folders by their titles using semantic similarity (like a semantic version of `glob`):
|
|
511
|
-
|
|
512
|
-
```python
|
|
513
|
-
# Find pages about authentication
|
|
514
|
-
results = client.spaces.semantic_glob(
|
|
515
|
-
space_id="space-uuid",
|
|
516
|
-
query="authentication and authorization pages",
|
|
517
|
-
limit=10,
|
|
518
|
-
threshold=1.0, # Only show results with distance < 1.0
|
|
519
|
-
)
|
|
520
|
-
|
|
521
|
-
for block in results:
|
|
522
|
-
print(f"{block.title} - {block.type}")
|
|
523
|
-
```
|
|
524
|
-
|
|
525
|
-
#### 3. Semantic Grep (Search content blocks)
|
|
526
|
-
|
|
527
|
-
Search through actual content blocks using semantic similarity (like a semantic version of `grep`):
|
|
528
|
-
|
|
529
|
-
```python
|
|
530
|
-
# Find code examples for JWT validation
|
|
531
|
-
results = client.spaces.semantic_grep(
|
|
532
|
-
space_id="space-uuid",
|
|
533
|
-
query="JWT token validation code examples",
|
|
534
|
-
limit=15,
|
|
535
|
-
threshold=0.7,
|
|
536
|
-
)
|
|
537
|
-
|
|
538
|
-
for block in results:
|
|
539
|
-
print(f"{block.title} - distance: {block.distance}")
|
|
540
|
-
print(f"Content: {block.props.get('text', '')[:100]}...")
|
|
541
|
-
```
|
|
542
|
-
|
|
543
646
|
See `examples/search_usage.py` for more detailed examples including async usage.
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
class BaseContext:
|
|
2
|
+
pass
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class BaseConverter:
|
|
6
|
+
def to_openai_tool_schema(self) -> dict:
|
|
7
|
+
raise NotImplementedError
|
|
8
|
+
|
|
9
|
+
def to_anthropic_tool_schema(self) -> dict:
|
|
10
|
+
raise NotImplementedError
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class BaseTool(BaseConverter):
|
|
14
|
+
@property
|
|
15
|
+
def name(self) -> str:
|
|
16
|
+
raise NotImplementedError
|
|
17
|
+
|
|
18
|
+
@property
|
|
19
|
+
def description(self) -> str:
|
|
20
|
+
raise NotImplementedError
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def arguments(self) -> dict:
|
|
24
|
+
raise NotImplementedError
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def required_arguments(self) -> list[str]:
|
|
28
|
+
raise NotImplementedError
|
|
29
|
+
|
|
30
|
+
def execute(self, ctx: BaseContext, llm_arguments: dict) -> str:
|
|
31
|
+
raise NotImplementedError
|
|
32
|
+
|
|
33
|
+
def to_openai_tool_schema(self) -> dict:
|
|
34
|
+
return {
|
|
35
|
+
"type": "function",
|
|
36
|
+
"function": {
|
|
37
|
+
"name": self.name,
|
|
38
|
+
"description": self.description,
|
|
39
|
+
"parameters": {
|
|
40
|
+
"type": "object",
|
|
41
|
+
"properties": self.arguments,
|
|
42
|
+
"required": self.required_arguments,
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
def to_anthropic_tool_schema(self) -> dict:
|
|
48
|
+
return {
|
|
49
|
+
"name": self.name,
|
|
50
|
+
"description": self.description,
|
|
51
|
+
"input_schema": {
|
|
52
|
+
"type": "object",
|
|
53
|
+
"properties": self.arguments,
|
|
54
|
+
"required": self.required_arguments,
|
|
55
|
+
},
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class BaseToolPool(BaseConverter):
|
|
60
|
+
def __init__(self):
|
|
61
|
+
self.tools: dict[str, BaseTool] = {}
|
|
62
|
+
|
|
63
|
+
def add_tool(self, tool: BaseTool):
|
|
64
|
+
self.tools[tool.name] = tool
|
|
65
|
+
|
|
66
|
+
def remove_tool(self, tool_name: str):
|
|
67
|
+
self.tools.pop(tool_name)
|
|
68
|
+
|
|
69
|
+
def extent_tool_pool(self, pool: "BaseToolPool"):
|
|
70
|
+
self.tools.update(pool.tools)
|
|
71
|
+
|
|
72
|
+
def execute_tool(
|
|
73
|
+
self, ctx: BaseContext, tool_name: str, llm_arguments: dict
|
|
74
|
+
) -> str:
|
|
75
|
+
tool = self.tools[tool_name]
|
|
76
|
+
r = tool.execute(ctx, llm_arguments)
|
|
77
|
+
return r.strip()
|
|
78
|
+
|
|
79
|
+
def tool_exists(self, tool_name: str) -> bool:
|
|
80
|
+
return tool_name in self.tools
|
|
81
|
+
|
|
82
|
+
def to_openai_tool_schema(self) -> list[dict]:
|
|
83
|
+
return [tool.to_openai_tool_schema() for tool in self.tools.values()]
|
|
84
|
+
|
|
85
|
+
def to_anthropic_tool_schema(self) -> list[dict]:
|
|
86
|
+
return [tool.to_anthropic_tool_schema() for tool in self.tools.values()]
|
|
87
|
+
|
|
88
|
+
def format_context(self, *args, **kwargs) -> BaseContext:
|
|
89
|
+
raise NotImplementedError
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
from .base import BaseContext, BaseTool, BaseToolPool
|
|
4
|
+
from ..client import AcontextClient
|
|
5
|
+
from ..uploads import FileUpload
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class DiskContext(BaseContext):
|
|
10
|
+
client: AcontextClient
|
|
11
|
+
disk_id: str
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _normalize_path(path: str | None) -> str:
|
|
15
|
+
"""Normalize a file path to ensure it starts with '/'."""
|
|
16
|
+
if not path:
|
|
17
|
+
return "/"
|
|
18
|
+
normalized = path if path.startswith("/") else f"/{path}"
|
|
19
|
+
if not normalized.endswith("/"):
|
|
20
|
+
normalized += "/"
|
|
21
|
+
return normalized
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class WriteFileTool(BaseTool):
|
|
25
|
+
"""Tool for writing text content to a file on the Acontext disk."""
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def name(self) -> str:
|
|
29
|
+
return "write_file"
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def description(self) -> str:
|
|
33
|
+
return "Write text content to a file in the file system. Creates the file if it doesn't exist, overwrites if it does."
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def arguments(self) -> dict:
|
|
37
|
+
return {
|
|
38
|
+
"file_path": {
|
|
39
|
+
"type": "string",
|
|
40
|
+
"description": "Optional folder path to organize files, e.g. '/notes/' or '/documents/'. Defaults to root '/' if not specified.",
|
|
41
|
+
},
|
|
42
|
+
"filename": {
|
|
43
|
+
"type": "string",
|
|
44
|
+
"description": "Filename such as 'report.md' or 'demo.txt'.",
|
|
45
|
+
},
|
|
46
|
+
"content": {
|
|
47
|
+
"type": "string",
|
|
48
|
+
"description": "Text content to write to the file.",
|
|
49
|
+
},
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def required_arguments(self) -> list[str]:
|
|
54
|
+
return ["filename", "content"]
|
|
55
|
+
|
|
56
|
+
def execute(self, ctx: DiskContext, llm_arguments: dict) -> str:
|
|
57
|
+
"""Write text content to a file."""
|
|
58
|
+
filename = llm_arguments.get("filename")
|
|
59
|
+
content = llm_arguments.get("content")
|
|
60
|
+
file_path = llm_arguments.get("file_path")
|
|
61
|
+
|
|
62
|
+
if not filename:
|
|
63
|
+
raise ValueError("filename is required")
|
|
64
|
+
if not content:
|
|
65
|
+
raise ValueError("content is required")
|
|
66
|
+
|
|
67
|
+
normalized_path = _normalize_path(file_path)
|
|
68
|
+
payload = FileUpload(filename=filename, content=content.encode("utf-8"))
|
|
69
|
+
artifact = ctx.client.disks.artifacts.upsert(
|
|
70
|
+
ctx.disk_id,
|
|
71
|
+
file=payload,
|
|
72
|
+
file_path=normalized_path,
|
|
73
|
+
)
|
|
74
|
+
return f"File '{artifact.filename}' written successfully to '{artifact.path}'"
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class ReadFileTool(BaseTool):
|
|
78
|
+
"""Tool for reading a text file from the Acontext disk."""
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def name(self) -> str:
|
|
82
|
+
return "read_file"
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def description(self) -> str:
|
|
86
|
+
return "Read a text file from the file system and return its content."
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def arguments(self) -> dict:
|
|
90
|
+
return {
|
|
91
|
+
"file_path": {
|
|
92
|
+
"type": "string",
|
|
93
|
+
"description": "Optional directory path where the file is located, e.g. '/notes/'. Defaults to root '/' if not specified.",
|
|
94
|
+
},
|
|
95
|
+
"filename": {
|
|
96
|
+
"type": "string",
|
|
97
|
+
"description": "Filename to read.",
|
|
98
|
+
},
|
|
99
|
+
"line_offset": {
|
|
100
|
+
"type": "integer",
|
|
101
|
+
"description": "The line number to start reading from. Default to 0",
|
|
102
|
+
},
|
|
103
|
+
"line_limit": {
|
|
104
|
+
"type": "integer",
|
|
105
|
+
"description": "The maximum number of lines to return. Default to 100",
|
|
106
|
+
},
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
@property
|
|
110
|
+
def required_arguments(self) -> list[str]:
|
|
111
|
+
return ["filename"]
|
|
112
|
+
|
|
113
|
+
def execute(self, ctx: DiskContext, llm_arguments: dict) -> str:
|
|
114
|
+
"""Read a text file and return its content preview."""
|
|
115
|
+
filename = llm_arguments.get("filename")
|
|
116
|
+
file_path = llm_arguments.get("file_path")
|
|
117
|
+
line_offset = llm_arguments.get("line_offset", 0)
|
|
118
|
+
line_limit = llm_arguments.get("line_limit", 100)
|
|
119
|
+
|
|
120
|
+
if not filename:
|
|
121
|
+
raise ValueError("filename is required")
|
|
122
|
+
|
|
123
|
+
normalized_path = _normalize_path(file_path)
|
|
124
|
+
result = ctx.client.disks.artifacts.get(
|
|
125
|
+
ctx.disk_id,
|
|
126
|
+
file_path=normalized_path,
|
|
127
|
+
filename=filename,
|
|
128
|
+
with_content=True,
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
if not result.content:
|
|
132
|
+
raise RuntimeError("Failed to read file: server did not return content.")
|
|
133
|
+
|
|
134
|
+
content_str = result.content.raw
|
|
135
|
+
lines = content_str.split("\n")
|
|
136
|
+
line_start = min(line_offset, len(lines) - 1)
|
|
137
|
+
line_end = min(line_start + line_limit, len(lines))
|
|
138
|
+
preview = "\n".join(lines[line_start:line_end])
|
|
139
|
+
return f"[{normalized_path}{filename} - showing L{line_start}-{line_end} of {len(lines)} lines]\n{preview}"
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class ReplaceStringTool(BaseTool):
|
|
143
|
+
"""Tool for replacing an old string with a new string in a file on the Acontext disk."""
|
|
144
|
+
|
|
145
|
+
@property
|
|
146
|
+
def name(self) -> str:
|
|
147
|
+
return "replace_string"
|
|
148
|
+
|
|
149
|
+
@property
|
|
150
|
+
def description(self) -> str:
|
|
151
|
+
return "Replace an old string with a new string in a file. Reads the file, performs the replacement, and writes it back."
|
|
152
|
+
|
|
153
|
+
@property
|
|
154
|
+
def arguments(self) -> dict:
|
|
155
|
+
return {
|
|
156
|
+
"file_path": {
|
|
157
|
+
"type": "string",
|
|
158
|
+
"description": "Optional directory path where the file is located, e.g. '/notes/'. Defaults to root '/' if not specified.",
|
|
159
|
+
},
|
|
160
|
+
"filename": {
|
|
161
|
+
"type": "string",
|
|
162
|
+
"description": "Filename to modify.",
|
|
163
|
+
},
|
|
164
|
+
"old_string": {
|
|
165
|
+
"type": "string",
|
|
166
|
+
"description": "The string to be replaced.",
|
|
167
|
+
},
|
|
168
|
+
"new_string": {
|
|
169
|
+
"type": "string",
|
|
170
|
+
"description": "The string to replace the old_string with.",
|
|
171
|
+
},
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
@property
|
|
175
|
+
def required_arguments(self) -> list[str]:
|
|
176
|
+
return ["filename", "old_string", "new_string"]
|
|
177
|
+
|
|
178
|
+
def execute(self, ctx: DiskContext, llm_arguments: dict) -> str:
|
|
179
|
+
"""Replace an old string with a new string in a file."""
|
|
180
|
+
filename = llm_arguments.get("filename")
|
|
181
|
+
file_path = llm_arguments.get("file_path")
|
|
182
|
+
old_string = llm_arguments.get("old_string")
|
|
183
|
+
new_string = llm_arguments.get("new_string")
|
|
184
|
+
|
|
185
|
+
if not filename:
|
|
186
|
+
raise ValueError("filename is required")
|
|
187
|
+
if old_string is None:
|
|
188
|
+
raise ValueError("old_string is required")
|
|
189
|
+
if new_string is None:
|
|
190
|
+
raise ValueError("new_string is required")
|
|
191
|
+
|
|
192
|
+
normalized_path = _normalize_path(file_path)
|
|
193
|
+
|
|
194
|
+
# Read the file content
|
|
195
|
+
result = ctx.client.disks.artifacts.get(
|
|
196
|
+
ctx.disk_id,
|
|
197
|
+
file_path=normalized_path,
|
|
198
|
+
filename=filename,
|
|
199
|
+
with_content=True,
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
if not result.content:
|
|
203
|
+
raise RuntimeError("Failed to read file: server did not return content.")
|
|
204
|
+
|
|
205
|
+
content_str = result.content.raw
|
|
206
|
+
|
|
207
|
+
# Perform the replacement
|
|
208
|
+
if old_string not in content_str:
|
|
209
|
+
return f"String '{old_string}' not found in file '{filename}'"
|
|
210
|
+
|
|
211
|
+
updated_content = content_str.replace(old_string, new_string)
|
|
212
|
+
replacement_count = content_str.count(old_string)
|
|
213
|
+
|
|
214
|
+
# Write the updated content back
|
|
215
|
+
payload = FileUpload(filename=filename, content=updated_content.encode("utf-8"))
|
|
216
|
+
ctx.client.disks.artifacts.upsert(
|
|
217
|
+
ctx.disk_id,
|
|
218
|
+
file=payload,
|
|
219
|
+
file_path=normalized_path,
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
return f"Found {replacement_count} old_string in {normalized_path}{filename} and replaced it."
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
class ListTool(BaseTool):
|
|
226
|
+
"""Tool for listing files in a directory on the Acontext disk."""
|
|
227
|
+
|
|
228
|
+
@property
|
|
229
|
+
def name(self) -> str:
|
|
230
|
+
return "list_artifacts"
|
|
231
|
+
|
|
232
|
+
@property
|
|
233
|
+
def description(self) -> str:
|
|
234
|
+
return "List all files and directories in a specified path on the disk."
|
|
235
|
+
|
|
236
|
+
@property
|
|
237
|
+
def arguments(self) -> dict:
|
|
238
|
+
return {
|
|
239
|
+
"file_path": {
|
|
240
|
+
"type": "string",
|
|
241
|
+
"description": "Optional directory path to list, e.g. '/todo/' or '/notes/'. Root is '/'",
|
|
242
|
+
},
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
@property
|
|
246
|
+
def required_arguments(self) -> list[str]:
|
|
247
|
+
return ["file_path"]
|
|
248
|
+
|
|
249
|
+
def execute(self, ctx: DiskContext, llm_arguments: dict) -> str:
|
|
250
|
+
"""List all files in a specified path."""
|
|
251
|
+
file_path = llm_arguments.get("file_path")
|
|
252
|
+
normalized_path = _normalize_path(file_path)
|
|
253
|
+
|
|
254
|
+
result = ctx.client.disks.artifacts.list(
|
|
255
|
+
ctx.disk_id,
|
|
256
|
+
path=normalized_path,
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
artifacts_list = [artifact.filename for artifact in result.artifacts]
|
|
260
|
+
|
|
261
|
+
if not artifacts_list and not result.directories:
|
|
262
|
+
return f"No files or directories found in '{normalized_path}'"
|
|
263
|
+
|
|
264
|
+
output_parts = []
|
|
265
|
+
if artifacts_list:
|
|
266
|
+
output_parts.append(f"Files: {', '.join(artifacts_list)}")
|
|
267
|
+
if result.directories:
|
|
268
|
+
output_parts.append(f"Directories: {', '.join(result.directories)}")
|
|
269
|
+
|
|
270
|
+
ls_sect = "\n".join(output_parts)
|
|
271
|
+
return f"""[Listing in {normalized_path}]
|
|
272
|
+
{ls_sect}"""
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
class DiskToolPool(BaseToolPool):
|
|
276
|
+
"""Tool pool for disk operations on Acontext disks."""
|
|
277
|
+
|
|
278
|
+
def format_context(self, client: AcontextClient, disk_id: str) -> DiskContext:
|
|
279
|
+
return DiskContext(client=client, disk_id=disk_id)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
DISK_TOOLS = DiskToolPool()
|
|
283
|
+
DISK_TOOLS.add_tool(WriteFileTool())
|
|
284
|
+
DISK_TOOLS.add_tool(ReadFileTool())
|
|
285
|
+
DISK_TOOLS.add_tool(ReplaceStringTool())
|
|
286
|
+
DISK_TOOLS.add_tool(ListTool())
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
if __name__ == "__main__":
|
|
290
|
+
client = AcontextClient(
|
|
291
|
+
api_key="sk-ac-your-root-api-bearer-token",
|
|
292
|
+
base_url="http://localhost:8029/api/v1",
|
|
293
|
+
)
|
|
294
|
+
print(client.ping())
|
|
295
|
+
new_disk = client.disks.create()
|
|
296
|
+
|
|
297
|
+
ctx = DISK_TOOLS.format_context(client, new_disk.id)
|
|
298
|
+
r = DISK_TOOLS.execute_tool(
|
|
299
|
+
ctx,
|
|
300
|
+
"write_file",
|
|
301
|
+
{"filename": "test.txt", "file_path": "/try/", "content": "Hello, world!"},
|
|
302
|
+
)
|
|
303
|
+
print(r)
|
|
304
|
+
r = DISK_TOOLS.execute_tool(
|
|
305
|
+
ctx, "read_file", {"filename": "test.txt", "file_path": "/try/"}
|
|
306
|
+
)
|
|
307
|
+
print(r)
|
|
308
|
+
r = DISK_TOOLS.execute_tool(ctx, "list_artifacts", {"file_path": "/"})
|
|
309
|
+
print(r)
|
|
310
|
+
|
|
311
|
+
r = DISK_TOOLS.execute_tool(
|
|
312
|
+
ctx,
|
|
313
|
+
"replace_string",
|
|
314
|
+
{
|
|
315
|
+
"filename": "test.txt",
|
|
316
|
+
"file_path": "/try/",
|
|
317
|
+
"old_string": "Hello",
|
|
318
|
+
"new_string": "Hi",
|
|
319
|
+
},
|
|
320
|
+
)
|
|
321
|
+
print(r)
|
|
322
|
+
r = DISK_TOOLS.execute_tool(
|
|
323
|
+
ctx, "read_file", {"filename": "test.txt", "file_path": "/try/"}
|
|
324
|
+
)
|
|
325
|
+
print(r)
|
|
File without changes
|
|
@@ -3,13 +3,14 @@ Spaces endpoints (async).
|
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
5
|
from collections.abc import Mapping
|
|
6
|
-
from typing import Any
|
|
6
|
+
from typing import Any
|
|
7
7
|
|
|
8
8
|
from .._utils import build_params
|
|
9
9
|
from ..client_types import AsyncRequesterProtocol
|
|
10
10
|
from ..types.space import (
|
|
11
|
+
ExperienceConfirmation,
|
|
12
|
+
ListExperienceConfirmationsOutput,
|
|
11
13
|
ListSpacesOutput,
|
|
12
|
-
SearchResultBlockItem,
|
|
13
14
|
Space,
|
|
14
15
|
SpaceSearchResult,
|
|
15
16
|
)
|
|
@@ -131,58 +132,59 @@ class AsyncSpacesAPI:
|
|
|
131
132
|
)
|
|
132
133
|
return SpaceSearchResult.model_validate(data)
|
|
133
134
|
|
|
134
|
-
async def
|
|
135
|
+
async def get_unconfirmed_experiences(
|
|
135
136
|
self,
|
|
136
137
|
space_id: str,
|
|
137
138
|
*,
|
|
138
|
-
query: str,
|
|
139
139
|
limit: int | None = None,
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
Searches specifically for page/folder titles using semantic similarity,
|
|
145
|
-
similar to a semantic version of the glob command.
|
|
140
|
+
cursor: str | None = None,
|
|
141
|
+
time_desc: bool | None = None,
|
|
142
|
+
) -> ListExperienceConfirmationsOutput:
|
|
143
|
+
"""Get all unconfirmed experiences in a space with cursor-based pagination.
|
|
146
144
|
|
|
147
145
|
Args:
|
|
148
146
|
space_id: The UUID of the space.
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
147
|
+
limit: Maximum number of confirmations to return (1-200, default 20).
|
|
148
|
+
cursor: Cursor for pagination. Use the cursor from the previous response to get the next page.
|
|
149
|
+
time_desc: Order by created_at descending if True, ascending if False (default False).
|
|
152
150
|
|
|
153
151
|
Returns:
|
|
154
|
-
|
|
152
|
+
ListExperienceConfirmationsOutput containing the list of experience confirmations and pagination information.
|
|
155
153
|
"""
|
|
156
|
-
params = build_params(
|
|
154
|
+
params = build_params(limit=limit, cursor=cursor, time_desc=time_desc)
|
|
157
155
|
data = await self._requester.request(
|
|
158
|
-
"GET",
|
|
156
|
+
"GET",
|
|
157
|
+
f"/space/{space_id}/experience_confirmations",
|
|
158
|
+
params=params or None,
|
|
159
159
|
)
|
|
160
|
-
return
|
|
160
|
+
return ListExperienceConfirmationsOutput.model_validate(data)
|
|
161
161
|
|
|
162
|
-
async def
|
|
162
|
+
async def confirm_experience(
|
|
163
163
|
self,
|
|
164
164
|
space_id: str,
|
|
165
|
+
experience_id: str,
|
|
165
166
|
*,
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
) -> List[SearchResultBlockItem]:
|
|
170
|
-
"""Perform semantic grep search for content blocks.
|
|
167
|
+
save: bool,
|
|
168
|
+
) -> ExperienceConfirmation | None:
|
|
169
|
+
"""Confirm an experience confirmation.
|
|
171
170
|
|
|
172
|
-
|
|
173
|
-
|
|
171
|
+
If save is False, delete the row. If save is True, get the data first,
|
|
172
|
+
then delete the row.
|
|
174
173
|
|
|
175
174
|
Args:
|
|
176
175
|
space_id: The UUID of the space.
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
threshold: Cosine distance threshold (0=identical, 2=opposite).
|
|
176
|
+
experience_id: The UUID of the experience confirmation.
|
|
177
|
+
save: If True, get data before deleting. If False, just delete.
|
|
180
178
|
|
|
181
179
|
Returns:
|
|
182
|
-
|
|
180
|
+
ExperienceConfirmation object if save is True, None otherwise.
|
|
183
181
|
"""
|
|
184
|
-
|
|
182
|
+
payload = {"save": save}
|
|
185
183
|
data = await self._requester.request(
|
|
186
|
-
"
|
|
184
|
+
"PATCH",
|
|
185
|
+
f"/space/{space_id}/experience_confirmations/{experience_id}",
|
|
186
|
+
json_data=payload,
|
|
187
187
|
)
|
|
188
|
-
|
|
188
|
+
if data is None:
|
|
189
|
+
return None
|
|
190
|
+
return ExperienceConfirmation.model_validate(data)
|
|
@@ -3,13 +3,14 @@ Spaces endpoints.
|
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
5
|
from collections.abc import Mapping
|
|
6
|
-
from typing import Any
|
|
6
|
+
from typing import Any
|
|
7
7
|
|
|
8
8
|
from .._utils import build_params
|
|
9
9
|
from ..client_types import RequesterProtocol
|
|
10
10
|
from ..types.space import (
|
|
11
|
+
ExperienceConfirmation,
|
|
12
|
+
ListExperienceConfirmationsOutput,
|
|
11
13
|
ListSpacesOutput,
|
|
12
|
-
SearchResultBlockItem,
|
|
13
14
|
Space,
|
|
14
15
|
SpaceSearchResult,
|
|
15
16
|
)
|
|
@@ -129,58 +130,59 @@ class SpacesAPI:
|
|
|
129
130
|
)
|
|
130
131
|
return SpaceSearchResult.model_validate(data)
|
|
131
132
|
|
|
132
|
-
def
|
|
133
|
+
def get_unconfirmed_experiences(
|
|
133
134
|
self,
|
|
134
135
|
space_id: str,
|
|
135
136
|
*,
|
|
136
|
-
query: str,
|
|
137
137
|
limit: int | None = None,
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
Searches specifically for page/folder titles using semantic similarity,
|
|
143
|
-
similar to a semantic version of the glob command.
|
|
138
|
+
cursor: str | None = None,
|
|
139
|
+
time_desc: bool | None = None,
|
|
140
|
+
) -> ListExperienceConfirmationsOutput:
|
|
141
|
+
"""Get all unconfirmed experiences in a space with cursor-based pagination.
|
|
144
142
|
|
|
145
143
|
Args:
|
|
146
144
|
space_id: The UUID of the space.
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
145
|
+
limit: Maximum number of confirmations to return (1-200, default 20).
|
|
146
|
+
cursor: Cursor for pagination. Use the cursor from the previous response to get the next page.
|
|
147
|
+
time_desc: Order by created_at descending if True, ascending if False (default False).
|
|
150
148
|
|
|
151
149
|
Returns:
|
|
152
|
-
|
|
150
|
+
ListExperienceConfirmationsOutput containing the list of experience confirmations and pagination information.
|
|
153
151
|
"""
|
|
154
|
-
params = build_params(
|
|
152
|
+
params = build_params(limit=limit, cursor=cursor, time_desc=time_desc)
|
|
155
153
|
data = self._requester.request(
|
|
156
|
-
"GET",
|
|
154
|
+
"GET",
|
|
155
|
+
f"/space/{space_id}/experience_confirmations",
|
|
156
|
+
params=params or None,
|
|
157
157
|
)
|
|
158
|
-
return
|
|
158
|
+
return ListExperienceConfirmationsOutput.model_validate(data)
|
|
159
159
|
|
|
160
|
-
def
|
|
160
|
+
def confirm_experience(
|
|
161
161
|
self,
|
|
162
162
|
space_id: str,
|
|
163
|
+
experience_id: str,
|
|
163
164
|
*,
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
) -> List[SearchResultBlockItem]:
|
|
168
|
-
"""Perform semantic grep search for content blocks.
|
|
165
|
+
save: bool,
|
|
166
|
+
) -> ExperienceConfirmation | None:
|
|
167
|
+
"""Confirm an experience confirmation.
|
|
169
168
|
|
|
170
|
-
|
|
171
|
-
|
|
169
|
+
If save is False, delete the row. If save is True, get the data first,
|
|
170
|
+
then delete the row.
|
|
172
171
|
|
|
173
172
|
Args:
|
|
174
173
|
space_id: The UUID of the space.
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
threshold: Cosine distance threshold (0=identical, 2=opposite).
|
|
174
|
+
experience_id: The UUID of the experience confirmation.
|
|
175
|
+
save: If True, get data before deleting. If False, just delete.
|
|
178
176
|
|
|
179
177
|
Returns:
|
|
180
|
-
|
|
178
|
+
ExperienceConfirmation object if save is True, None otherwise.
|
|
181
179
|
"""
|
|
182
|
-
|
|
180
|
+
payload = {"save": save}
|
|
183
181
|
data = self._requester.request(
|
|
184
|
-
"
|
|
182
|
+
"PATCH",
|
|
183
|
+
f"/space/{space_id}/experience_confirmations/{experience_id}",
|
|
184
|
+
json_data=payload,
|
|
185
185
|
)
|
|
186
|
-
|
|
186
|
+
if data is None:
|
|
187
|
+
return None
|
|
188
|
+
return ExperienceConfirmation.model_validate(data)
|
|
@@ -24,6 +24,8 @@ from .session import (
|
|
|
24
24
|
)
|
|
25
25
|
from .block import Block
|
|
26
26
|
from .space import (
|
|
27
|
+
ExperienceConfirmation,
|
|
28
|
+
ListExperienceConfirmationsOutput,
|
|
27
29
|
ListSpacesOutput,
|
|
28
30
|
SearchResultBlockItem,
|
|
29
31
|
Space,
|
|
@@ -58,6 +60,8 @@ __all__ = [
|
|
|
58
60
|
"Task",
|
|
59
61
|
"TokenCounts",
|
|
60
62
|
# Space types
|
|
63
|
+
"ExperienceConfirmation",
|
|
64
|
+
"ListExperienceConfirmationsOutput",
|
|
61
65
|
"ListSpacesOutput",
|
|
62
66
|
"SearchResultBlockItem",
|
|
63
67
|
"Space",
|
|
@@ -44,3 +44,26 @@ class SpaceSearchResult(BaseModel):
|
|
|
44
44
|
..., description="List of cited blocks"
|
|
45
45
|
)
|
|
46
46
|
final_answer: str | None = Field(None, description="AI-generated final answer")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class ExperienceConfirmation(BaseModel):
|
|
50
|
+
"""Experience confirmation model."""
|
|
51
|
+
|
|
52
|
+
id: str = Field(..., description="Experience confirmation UUID")
|
|
53
|
+
space_id: str = Field(..., description="Space UUID")
|
|
54
|
+
task_id: str | None = Field(None, description="Task UUID (optional)")
|
|
55
|
+
experience_data: dict[str, Any] = Field(
|
|
56
|
+
..., description="Experience data dictionary"
|
|
57
|
+
)
|
|
58
|
+
created_at: str = Field(..., description="ISO 8601 formatted creation timestamp")
|
|
59
|
+
updated_at: str = Field(..., description="ISO 8601 formatted update timestamp")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class ListExperienceConfirmationsOutput(BaseModel):
|
|
63
|
+
"""Response model for listing experience confirmations."""
|
|
64
|
+
|
|
65
|
+
items: list[ExperienceConfirmation] = Field(
|
|
66
|
+
..., description="List of experience confirmations"
|
|
67
|
+
)
|
|
68
|
+
next_cursor: str | None = Field(None, description="Cursor for pagination")
|
|
69
|
+
has_more: bool = Field(..., description="Whether there are more items")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|