deeptutor-knowledge 1.7.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.
- deeptutor/api/routers/knowledge.py +5225 -0
- deeptutor/api/routers/marginnote4.py +278 -0
- deeptutor/capabilities/ima/__init__.py +6 -0
- deeptutor/capabilities/ima/binding.py +118 -0
- deeptutor/capabilities/ima/capability.py +92 -0
- deeptutor/capabilities/ima/prompts/en/system.md +33 -0
- deeptutor/capabilities/ima/prompts/zh/system.md +24 -0
- deeptutor/capabilities/ima/tools.py +476 -0
- deeptutor/capabilities/marginnote4/__init__.py +13 -0
- deeptutor/capabilities/marginnote4/binding.py +66 -0
- deeptutor/capabilities/marginnote4/capability.py +80 -0
- deeptutor/capabilities/marginnote4/models.py +173 -0
- deeptutor/capabilities/marginnote4/prompts/en/system.md +54 -0
- deeptutor/capabilities/marginnote4/prompts/zh/system.md +44 -0
- deeptutor/capabilities/marginnote4/store.py +562 -0
- deeptutor/capabilities/marginnote4/tools.py +329 -0
- deeptutor/capabilities/obsidian/__init__.py +6 -0
- deeptutor/capabilities/obsidian/binding.py +75 -0
- deeptutor/capabilities/obsidian/capability.py +82 -0
- deeptutor/capabilities/obsidian/prompts/en/system.md +32 -0
- deeptutor/capabilities/obsidian/prompts/zh/system.md +28 -0
- deeptutor/capabilities/obsidian/tools.py +399 -0
- deeptutor/capabilities/obsidian/vault.py +341 -0
- deeptutor/knowledge/__init__.py +28 -0
- deeptutor/knowledge/add_documents.py +734 -0
- deeptutor/knowledge/indexing_run.py +432 -0
- deeptutor/knowledge/initializer.py +448 -0
- deeptutor/knowledge/manager.py +2570 -0
- deeptutor/knowledge/module_service.py +29 -0
- deeptutor/knowledge/naming.py +40 -0
- deeptutor/knowledge/progress_events.py +45 -0
- deeptutor/knowledge/progress_tracker.py +355 -0
- deeptutor/knowledge/staging.py +69 -0
- deeptutor/services/config/knowledge_base_config.py +239 -0
- deeptutor/services/embedding/__init__.py +46 -0
- deeptutor/services/embedding/adapters/__init__.py +40 -0
- deeptutor/services/embedding/adapters/base.py +196 -0
- deeptutor/services/embedding/adapters/cohere.py +176 -0
- deeptutor/services/embedding/adapters/dashscope_native.py +242 -0
- deeptutor/services/embedding/adapters/gemini.py +462 -0
- deeptutor/services/embedding/adapters/jina.py +161 -0
- deeptutor/services/embedding/adapters/ollama.py +145 -0
- deeptutor/services/embedding/adapters/openai_compatible.py +406 -0
- deeptutor/services/embedding/adapters/openai_sdk.py +150 -0
- deeptutor/services/embedding/client.py +323 -0
- deeptutor/services/embedding/config.py +114 -0
- deeptutor/services/embedding/request_options.py +46 -0
- deeptutor/services/embedding/validation.py +128 -0
- deeptutor/services/github_source/__init__.py +5 -0
- deeptutor/services/github_source/client.py +164 -0
- deeptutor/services/github_source/sync.py +384 -0
- deeptutor/services/github_source/sync_service.py +72 -0
- deeptutor/services/rag/__init__.py +21 -0
- deeptutor/services/rag/embedding_binding.py +331 -0
- deeptutor/services/rag/embedding_signature.py +63 -0
- deeptutor/services/rag/eval/__init__.py +92 -0
- deeptutor/services/rag/eval/dataset.py +244 -0
- deeptutor/services/rag/eval/diff.py +312 -0
- deeptutor/services/rag/eval/matching.py +218 -0
- deeptutor/services/rag/eval/metrics.py +213 -0
- deeptutor/services/rag/eval/report.py +111 -0
- deeptutor/services/rag/eval/runner.py +309 -0
- deeptutor/services/rag/factory.py +369 -0
- deeptutor/services/rag/file_routing.py +5 -0
- deeptutor/services/rag/index_probe.py +327 -0
- deeptutor/services/rag/index_versioning.py +387 -0
- deeptutor/services/rag/kb_paths.py +50 -0
- deeptutor/services/rag/linked_kb.py +237 -0
- deeptutor/services/rag/pipelines/__init__.py +7 -0
- deeptutor/services/rag/pipelines/base.py +41 -0
- deeptutor/services/rag/pipelines/graphrag/__init__.py +18 -0
- deeptutor/services/rag/pipelines/graphrag/compatibility.py +42 -0
- deeptutor/services/rag/pipelines/graphrag/completion_adapter.py +251 -0
- deeptutor/services/rag/pipelines/graphrag/config.py +357 -0
- deeptutor/services/rag/pipelines/graphrag/engine.py +479 -0
- deeptutor/services/rag/pipelines/graphrag/errors.py +328 -0
- deeptutor/services/rag/pipelines/graphrag/ingestion.py +135 -0
- deeptutor/services/rag/pipelines/graphrag/pandas_compat.py +68 -0
- deeptutor/services/rag/pipelines/graphrag/pipeline.py +284 -0
- deeptutor/services/rag/pipelines/graphrag/provider.py +134 -0
- deeptutor/services/rag/pipelines/graphrag/storage.py +127 -0
- deeptutor/services/rag/pipelines/ima/__init__.py +28 -0
- deeptutor/services/rag/pipelines/ima/client.py +364 -0
- deeptutor/services/rag/pipelines/ima/config.py +140 -0
- deeptutor/services/rag/pipelines/ima/envelope.py +109 -0
- deeptutor/services/rag/pipelines/ima/inventory.py +209 -0
- deeptutor/services/rag/pipelines/ima/media.py +190 -0
- deeptutor/services/rag/pipelines/ima/models.py +348 -0
- deeptutor/services/rag/pipelines/ima/notes.py +240 -0
- deeptutor/services/rag/pipelines/ima/pipeline.py +238 -0
- deeptutor/services/rag/pipelines/ima/probe.py +105 -0
- deeptutor/services/rag/pipelines/ima/sources.py +105 -0
- deeptutor/services/rag/pipelines/ima/transport.py +130 -0
- deeptutor/services/rag/pipelines/kiwix/__init__.py +1 -0
- deeptutor/services/rag/pipelines/kiwix/client.py +369 -0
- deeptutor/services/rag/pipelines/kiwix/pipeline.py +75 -0
- deeptutor/services/rag/pipelines/lightrag/README.md +78 -0
- deeptutor/services/rag/pipelines/lightrag/__init__.py +15 -0
- deeptutor/services/rag/pipelines/lightrag/block_policy.py +266 -0
- deeptutor/services/rag/pipelines/lightrag/cache_reuse.py +137 -0
- deeptutor/services/rag/pipelines/lightrag/config.py +453 -0
- deeptutor/services/rag/pipelines/lightrag/engine.py +536 -0
- deeptutor/services/rag/pipelines/lightrag/indexing_policy.py +696 -0
- deeptutor/services/rag/pipelines/lightrag/ingress.py +364 -0
- deeptutor/services/rag/pipelines/lightrag/parser.py +99 -0
- deeptutor/services/rag/pipelines/lightrag/pipeline.py +692 -0
- deeptutor/services/rag/pipelines/lightrag/roles.py +132 -0
- deeptutor/services/rag/pipelines/lightrag/sidecar.py +320 -0
- deeptutor/services/rag/pipelines/lightrag/storage.py +338 -0
- deeptutor/services/rag/pipelines/lightrag/worker.py +259 -0
- deeptutor/services/rag/pipelines/lightrag/write_lock.py +55 -0
- deeptutor/services/rag/pipelines/lightrag_server/__init__.py +15 -0
- deeptutor/services/rag/pipelines/lightrag_server/client.py +149 -0
- deeptutor/services/rag/pipelines/lightrag_server/config.py +68 -0
- deeptutor/services/rag/pipelines/lightrag_server/pipeline.py +120 -0
- deeptutor/services/rag/pipelines/lightrag_server/probe.py +110 -0
- deeptutor/services/rag/pipelines/llamaindex/__init__.py +3 -0
- deeptutor/services/rag/pipelines/llamaindex/config.py +199 -0
- deeptutor/services/rag/pipelines/llamaindex/document_loader.py +648 -0
- deeptutor/services/rag/pipelines/llamaindex/embedding_adapter.py +264 -0
- deeptutor/services/rag/pipelines/llamaindex/errors.py +76 -0
- deeptutor/services/rag/pipelines/llamaindex/exercise_lookup.py +373 -0
- deeptutor/services/rag/pipelines/llamaindex/ingestion.py +129 -0
- deeptutor/services/rag/pipelines/llamaindex/pipeline.py +556 -0
- deeptutor/services/rag/pipelines/llamaindex/rerank.py +130 -0
- deeptutor/services/rag/pipelines/llamaindex/retrievers.py +360 -0
- deeptutor/services/rag/pipelines/llamaindex/storage.py +655 -0
- deeptutor/services/rag/pipelines/llamaindex/vector_store.py +288 -0
- deeptutor/services/rag/pipelines/modes.py +48 -0
- deeptutor/services/rag/pipelines/pageindex/__init__.py +47 -0
- deeptutor/services/rag/pipelines/pageindex/client.py +146 -0
- deeptutor/services/rag/pipelines/pageindex/config.py +49 -0
- deeptutor/services/rag/pipelines/pageindex/pipeline.py +313 -0
- deeptutor/services/rag/pipelines/pageindex/reasoning.py +262 -0
- deeptutor/services/rag/pipelines/pageindex/storage.py +133 -0
- deeptutor/services/rag/pipelines/pageindex/tools.py +221 -0
- deeptutor/services/rag/pipelines/weknora/__init__.py +13 -0
- deeptutor/services/rag/pipelines/weknora/client.py +106 -0
- deeptutor/services/rag/pipelines/weknora/config.py +56 -0
- deeptutor/services/rag/pipelines/weknora/pipeline.py +74 -0
- deeptutor/services/rag/pipelines/weknora/probe.py +80 -0
- deeptutor/services/rag/preflight.py +274 -0
- deeptutor/services/rag/provider_binding.py +70 -0
- deeptutor/services/rag/query_rewrite.py +186 -0
- deeptutor/services/rag/service.py +379 -0
- deeptutor/services/rag/smart_retriever.py +81 -0
- deeptutor/services/rag/source_pages.py +292 -0
- deeptutor/services/rag/source_visuals.py +434 -0
- deeptutor/services/rag/visual_assets.py +430 -0
- deeptutor/services/rag/visual_coverage.py +165 -0
- deeptutor/services/web_source/__init__.py +1 -0
- deeptutor/services/web_source/bilingual.py +255 -0
- deeptutor/services/web_source/crawler.py +681 -0
- deeptutor/services/web_source/html_extractor.py +643 -0
- deeptutor/services/web_source/markdown.py +28 -0
- deeptutor/services/web_source/navigation.py +72 -0
- deeptutor/services/web_source/repository.py +640 -0
- deeptutor/services/web_source/robots.py +182 -0
- deeptutor/services/web_source/scheduler.py +336 -0
- deeptutor/services/web_source/snapshot_assets.py +171 -0
- deeptutor/services/web_source/sync.py +253 -0
- deeptutor/services/workspace/kb_move.py +446 -0
- deeptutor/tools/rag_tool.py +110 -0
- deeptutor_cli/kb.py +756 -0
- deeptutor_knowledge-1.7.0.dist-info/METADATA +15 -0
- deeptutor_knowledge-1.7.0.dist-info/RECORD +168 -0
- deeptutor_knowledge-1.7.0.dist-info/WHEEL +5 -0
- deeptutor_knowledge-1.7.0.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,476 @@
|
|
|
1
|
+
"""IMA tools — the agentic surface over a connected Tencent IMA library.
|
|
2
|
+
|
|
3
|
+
Five tools, auto-mounted only when a ``type: ima`` knowledge base is selected
|
|
4
|
+
(see :class:`~deeptutor.capabilities.ima.capability.ImaCapability`). They cover
|
|
5
|
+
the questions retrieval structurally cannot answer, plus the two additive writes:
|
|
6
|
+
|
|
7
|
+
* ``ima_list`` — browse what the library holds, folder by folder. "What's in
|
|
8
|
+
here", "did I add X", "show me the folder Y" are inventory questions; a
|
|
9
|
+
similarity search cannot answer them.
|
|
10
|
+
* ``ima_read`` — the full source text of one item, when a retrieved snippet is
|
|
11
|
+
not enough.
|
|
12
|
+
* ``ima_note_search`` — search or list IMA notes, newest first. Notes are the
|
|
13
|
+
only IMA objects carrying timestamps, so this is what answers "my most recent
|
|
14
|
+
notes".
|
|
15
|
+
* ``ima_add_url`` — collect a web page / WeChat article into the library.
|
|
16
|
+
* ``ima_write_note`` — create a note, or append to one the user named.
|
|
17
|
+
|
|
18
|
+
The turn's available libraries are injected server-side as ``_ima_bindings``;
|
|
19
|
+
the model supplies at most a ``kb_name`` to pick between them, and credentials
|
|
20
|
+
are loaded per call from the KB's own config (never passed through kwargs).
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import json
|
|
26
|
+
from typing import Any
|
|
27
|
+
|
|
28
|
+
from deeptutor.capabilities.ima.binding import ImaBinding, resolve_client, select_binding
|
|
29
|
+
from deeptutor.core.tool_protocol import BaseTool, ToolDefinition, ToolParameter, ToolResult
|
|
30
|
+
|
|
31
|
+
# The IMA service modules are imported lazily inside the methods that need them:
|
|
32
|
+
# this module is imported while ``deeptutor.tools.builtin`` builds the global tool
|
|
33
|
+
# table, and reaching into ``deeptutor.services.rag`` at import time would close
|
|
34
|
+
# a cycle through the runtime tool registry.
|
|
35
|
+
|
|
36
|
+
# Tool names mounted together when an IMA library is selected. Single source of
|
|
37
|
+
# truth so the mount policy and the registration list can't disagree.
|
|
38
|
+
IMA_TOOL_NAMES: tuple[str, ...] = (
|
|
39
|
+
"ima_list",
|
|
40
|
+
"ima_read",
|
|
41
|
+
"ima_note_search",
|
|
42
|
+
"ima_add_url",
|
|
43
|
+
"ima_write_note",
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
# Injected by the capability: the turn's selected IMA libraries.
|
|
47
|
+
BINDINGS_KWARG = "_ima_bindings"
|
|
48
|
+
|
|
49
|
+
_KB_NAME_PARAM = ToolParameter(
|
|
50
|
+
name="kb_name",
|
|
51
|
+
type="string",
|
|
52
|
+
description=("Which attached IMA knowledge base to use. Optional when only one is attached."),
|
|
53
|
+
required=False,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _note_sort(name: Any) -> int:
|
|
58
|
+
"""Map a tool-level sort name onto IMA's numeric ``sort_type``."""
|
|
59
|
+
from deeptutor.services.rag.pipelines.ima.notes import (
|
|
60
|
+
SORT_BY_CREATED,
|
|
61
|
+
SORT_BY_TITLE,
|
|
62
|
+
SORT_BY_UPDATED,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
"updated": SORT_BY_UPDATED,
|
|
67
|
+
"created": SORT_BY_CREATED,
|
|
68
|
+
"title": SORT_BY_TITLE,
|
|
69
|
+
}.get(str(name or "updated"), SORT_BY_UPDATED)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class _ImaTool(BaseTool):
|
|
73
|
+
"""Shared binding resolution and uniform error handling for IMA tools."""
|
|
74
|
+
|
|
75
|
+
#: Writes re-check the user's write access to the knowledge base.
|
|
76
|
+
writes = False
|
|
77
|
+
|
|
78
|
+
async def execute(self, **kwargs: Any) -> ToolResult:
|
|
79
|
+
from deeptutor.services.rag.pipelines.ima.config import ImaNotConfiguredError
|
|
80
|
+
from deeptutor.services.rag.pipelines.ima.envelope import (
|
|
81
|
+
ImaAPIError,
|
|
82
|
+
ImaAuthError,
|
|
83
|
+
ImaRateLimitError,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
bindings = _bindings(kwargs)
|
|
87
|
+
binding = select_binding(bindings, kwargs.get("kb_name"))
|
|
88
|
+
if binding is None:
|
|
89
|
+
return _err(_no_binding_message(bindings))
|
|
90
|
+
try:
|
|
91
|
+
client = resolve_client(binding.kb_ref, for_write=self.writes)
|
|
92
|
+
except ImaNotConfiguredError as exc:
|
|
93
|
+
return _err(str(exc))
|
|
94
|
+
except Exception:
|
|
95
|
+
return _err(
|
|
96
|
+
f"'{binding.name}' is not available on this turn"
|
|
97
|
+
f"{' with write access' if self.writes else ''}."
|
|
98
|
+
)
|
|
99
|
+
try:
|
|
100
|
+
return await self._run(client, binding, kwargs)
|
|
101
|
+
except ImaAuthError:
|
|
102
|
+
return _err("Tencent IMA rejected the credentials for this knowledge base.")
|
|
103
|
+
except ImaRateLimitError:
|
|
104
|
+
return _err("Tencent IMA is rate-limiting requests. Try again shortly.")
|
|
105
|
+
except ImaAPIError as exc:
|
|
106
|
+
return _err(str(exc))
|
|
107
|
+
except ValueError as exc:
|
|
108
|
+
return _err(str(exc))
|
|
109
|
+
|
|
110
|
+
async def _run(
|
|
111
|
+
self,
|
|
112
|
+
client: Any,
|
|
113
|
+
binding: ImaBinding,
|
|
114
|
+
kwargs: dict[str, Any],
|
|
115
|
+
) -> ToolResult: # pragma: no cover - abstract
|
|
116
|
+
raise NotImplementedError
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class ImaListTool(_ImaTool):
|
|
120
|
+
"""Browse a connected IMA library's contents."""
|
|
121
|
+
|
|
122
|
+
def get_definition(self) -> ToolDefinition:
|
|
123
|
+
return ToolDefinition(
|
|
124
|
+
name="ima_list",
|
|
125
|
+
description=(
|
|
126
|
+
"List what a connected Tencent IMA knowledge base actually holds: its "
|
|
127
|
+
"documents and folders. Use this for inventory questions ('what is in "
|
|
128
|
+
"here', 'is document X present', 'show me folder Y') — retrieval only "
|
|
129
|
+
"reports what a query matched. Pass a folder_id from a previous call to "
|
|
130
|
+
"look inside that folder."
|
|
131
|
+
),
|
|
132
|
+
parameters=[
|
|
133
|
+
_KB_NAME_PARAM,
|
|
134
|
+
ToolParameter(
|
|
135
|
+
name="folder_id",
|
|
136
|
+
type="string",
|
|
137
|
+
description="Folder to open, from an earlier listing. Omit for the top level.",
|
|
138
|
+
required=False,
|
|
139
|
+
),
|
|
140
|
+
ToolParameter(
|
|
141
|
+
name="limit",
|
|
142
|
+
type="integer",
|
|
143
|
+
description="Max entries to return (1-50, default 50).",
|
|
144
|
+
required=False,
|
|
145
|
+
),
|
|
146
|
+
ToolParameter(
|
|
147
|
+
name="cursor",
|
|
148
|
+
type="string",
|
|
149
|
+
description="Continuation cursor from an earlier call's next_cursor.",
|
|
150
|
+
required=False,
|
|
151
|
+
),
|
|
152
|
+
],
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
async def _run(self, client, binding, kwargs):
|
|
156
|
+
page = await client.get_knowledge_list(
|
|
157
|
+
folder_id=str(kwargs.get("folder_id") or ""),
|
|
158
|
+
cursor=str(kwargs.get("cursor") or ""),
|
|
159
|
+
limit=kwargs.get("limit") or 50,
|
|
160
|
+
)
|
|
161
|
+
return _ok(
|
|
162
|
+
{
|
|
163
|
+
"knowledge_base": binding.name,
|
|
164
|
+
"path": list(page.path),
|
|
165
|
+
"folders": [
|
|
166
|
+
{
|
|
167
|
+
"folder_id": folder.folder_id,
|
|
168
|
+
"name": folder.name,
|
|
169
|
+
"documents": folder.file_number,
|
|
170
|
+
"subfolders": folder.folder_number,
|
|
171
|
+
}
|
|
172
|
+
for folder in page.folders
|
|
173
|
+
],
|
|
174
|
+
"documents": [
|
|
175
|
+
{"media_id": document.media_id, "title": document.title}
|
|
176
|
+
for document in page.documents
|
|
177
|
+
],
|
|
178
|
+
"next_cursor": page.next_cursor,
|
|
179
|
+
"is_end": page.is_end,
|
|
180
|
+
}
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class ImaReadTool(_ImaTool):
|
|
185
|
+
"""Read one IMA item's full source text."""
|
|
186
|
+
|
|
187
|
+
def get_definition(self) -> ToolDefinition:
|
|
188
|
+
return ToolDefinition(
|
|
189
|
+
name="ima_read",
|
|
190
|
+
description=(
|
|
191
|
+
"Read the full text of one item in a connected Tencent IMA knowledge "
|
|
192
|
+
"base, identified by the media_id from ima_list or a retrieval citation. "
|
|
193
|
+
"Use it when a retrieved snippet is too short to answer from."
|
|
194
|
+
),
|
|
195
|
+
parameters=[
|
|
196
|
+
ToolParameter(
|
|
197
|
+
name="media_id",
|
|
198
|
+
type="string",
|
|
199
|
+
description="Item id from ima_list or a retrieval result.",
|
|
200
|
+
),
|
|
201
|
+
_KB_NAME_PARAM,
|
|
202
|
+
],
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
async def _run(self, client, binding, kwargs):
|
|
206
|
+
from deeptutor.services.rag.pipelines.ima import media as media_ops
|
|
207
|
+
from deeptutor.services.rag.pipelines.ima import sources as source_policy
|
|
208
|
+
|
|
209
|
+
media_id = str(kwargs.get("media_id") or "").strip()
|
|
210
|
+
if not media_id:
|
|
211
|
+
raise ValueError("media_id is required.")
|
|
212
|
+
media = await client.get_media_content(media_id)
|
|
213
|
+
text = await media_ops.extract_text(
|
|
214
|
+
media,
|
|
215
|
+
max_chars=source_policy.MAX_FULLTEXT_CHARS,
|
|
216
|
+
)
|
|
217
|
+
if not text:
|
|
218
|
+
return _err(
|
|
219
|
+
"That item has no readable text — it may be an image, an audio file, "
|
|
220
|
+
"or a format Tencent IMA does not expose over the API."
|
|
221
|
+
)
|
|
222
|
+
return _ok(
|
|
223
|
+
{
|
|
224
|
+
"knowledge_base": binding.name,
|
|
225
|
+
"media_id": media_id,
|
|
226
|
+
"truncated": len(text) >= source_policy.MAX_FULLTEXT_CHARS,
|
|
227
|
+
"content": text,
|
|
228
|
+
}
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
class ImaNoteSearchTool(_ImaTool):
|
|
233
|
+
"""Search or list the user's IMA notes."""
|
|
234
|
+
|
|
235
|
+
def get_definition(self) -> ToolDefinition:
|
|
236
|
+
return ToolDefinition(
|
|
237
|
+
name="ima_note_search",
|
|
238
|
+
description=(
|
|
239
|
+
"Search the user's Tencent IMA notes, or list them newest-first when no "
|
|
240
|
+
"query is given. Notes are the only IMA objects with timestamps, so this "
|
|
241
|
+
"is how to answer 'my latest notes' or 'what did I write about X'."
|
|
242
|
+
),
|
|
243
|
+
parameters=[
|
|
244
|
+
ToolParameter(
|
|
245
|
+
name="query",
|
|
246
|
+
type="string",
|
|
247
|
+
description="Text to match. Omit to list notes in sort order.",
|
|
248
|
+
required=False,
|
|
249
|
+
),
|
|
250
|
+
ToolParameter(
|
|
251
|
+
name="in_content",
|
|
252
|
+
type="boolean",
|
|
253
|
+
description="Search note bodies instead of titles (default false).",
|
|
254
|
+
required=False,
|
|
255
|
+
),
|
|
256
|
+
ToolParameter(
|
|
257
|
+
name="sort",
|
|
258
|
+
type="string",
|
|
259
|
+
description="Order: updated (default), created, or title.",
|
|
260
|
+
required=False,
|
|
261
|
+
enum=["updated", "created", "title"],
|
|
262
|
+
),
|
|
263
|
+
ToolParameter(
|
|
264
|
+
name="limit",
|
|
265
|
+
type="integer",
|
|
266
|
+
description="Max notes to return (1-50, default 20).",
|
|
267
|
+
required=False,
|
|
268
|
+
),
|
|
269
|
+
_KB_NAME_PARAM,
|
|
270
|
+
],
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
async def _run(self, client, binding, kwargs):
|
|
274
|
+
notes, is_end = await client.notes.search_notes(
|
|
275
|
+
str(kwargs.get("query") or ""),
|
|
276
|
+
by_content=bool(kwargs.get("in_content")),
|
|
277
|
+
sort_type=_note_sort(kwargs.get("sort")),
|
|
278
|
+
limit=kwargs.get("limit") or 20,
|
|
279
|
+
)
|
|
280
|
+
return _ok(
|
|
281
|
+
{
|
|
282
|
+
"notes": [
|
|
283
|
+
{
|
|
284
|
+
"note_id": note.note_id,
|
|
285
|
+
"title": note.title,
|
|
286
|
+
"summary": note.summary,
|
|
287
|
+
"notebook": note.folder_name,
|
|
288
|
+
"created_at": note.created_at,
|
|
289
|
+
"updated_at": note.updated_at,
|
|
290
|
+
}
|
|
291
|
+
for note in notes
|
|
292
|
+
],
|
|
293
|
+
"is_end": is_end,
|
|
294
|
+
}
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
class ImaAddUrlTool(_ImaTool):
|
|
299
|
+
"""Collect web pages into a connected IMA library."""
|
|
300
|
+
|
|
301
|
+
writes = True
|
|
302
|
+
|
|
303
|
+
def get_definition(self) -> ToolDefinition:
|
|
304
|
+
return ToolDefinition(
|
|
305
|
+
name="ima_add_url",
|
|
306
|
+
description=(
|
|
307
|
+
"Add web pages or WeChat articles (up to 10 URLs) to a connected Tencent "
|
|
308
|
+
"IMA knowledge base, so IMA indexes them for later retrieval. Only call "
|
|
309
|
+
"this when the user asked to save or collect something — it modifies "
|
|
310
|
+
"their library. Video pages (Bilibili / YouTube) and local files are not "
|
|
311
|
+
"supported by IMA's API."
|
|
312
|
+
),
|
|
313
|
+
parameters=[
|
|
314
|
+
ToolParameter(
|
|
315
|
+
name="urls",
|
|
316
|
+
type="array",
|
|
317
|
+
description="1-10 http(s) URLs to add.",
|
|
318
|
+
items={"type": "string"},
|
|
319
|
+
),
|
|
320
|
+
_KB_NAME_PARAM,
|
|
321
|
+
ToolParameter(
|
|
322
|
+
name="folder_id",
|
|
323
|
+
type="string",
|
|
324
|
+
description="Target folder from ima_list. Omit for the library root.",
|
|
325
|
+
required=False,
|
|
326
|
+
),
|
|
327
|
+
],
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
async def _run(self, client, binding, kwargs):
|
|
331
|
+
urls = _string_list(kwargs.get("urls"))
|
|
332
|
+
unsupported = [url for url in urls if not _is_supported_url(url)]
|
|
333
|
+
if unsupported:
|
|
334
|
+
return _err(
|
|
335
|
+
"Tencent IMA cannot add these over its API (video pages and local files "
|
|
336
|
+
f"must be added in the IMA desktop app): {', '.join(unsupported)}"
|
|
337
|
+
)
|
|
338
|
+
results = await client.import_urls(urls, folder_id=str(kwargs.get("folder_id") or ""))
|
|
339
|
+
added = [result for result in results if result.ok]
|
|
340
|
+
failed = [result for result in results if not result.ok]
|
|
341
|
+
return ToolResult(
|
|
342
|
+
content=json.dumps(
|
|
343
|
+
{
|
|
344
|
+
"knowledge_base": binding.name,
|
|
345
|
+
"added": [{"url": item.url, "media_id": item.media_id} for item in added],
|
|
346
|
+
"failed": [{"url": item.url, "code": item.code} for item in failed],
|
|
347
|
+
},
|
|
348
|
+
ensure_ascii=False,
|
|
349
|
+
),
|
|
350
|
+
success=bool(added),
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
class ImaWriteNoteTool(_ImaTool):
|
|
355
|
+
"""Create an IMA note, or append to an existing one."""
|
|
356
|
+
|
|
357
|
+
writes = True
|
|
358
|
+
|
|
359
|
+
def get_definition(self) -> ToolDefinition:
|
|
360
|
+
return ToolDefinition(
|
|
361
|
+
name="ima_write_note",
|
|
362
|
+
description=(
|
|
363
|
+
"Write a Markdown note into the user's Tencent IMA account. Creates a new "
|
|
364
|
+
"note by default; pass note_id to append to that note instead. Appending "
|
|
365
|
+
"cannot be undone, so only append to a note the user explicitly named — "
|
|
366
|
+
"otherwise create a new one. Only call this when the user asked to save "
|
|
367
|
+
"something."
|
|
368
|
+
),
|
|
369
|
+
parameters=[
|
|
370
|
+
ToolParameter(
|
|
371
|
+
name="content",
|
|
372
|
+
type="string",
|
|
373
|
+
description="Markdown body. Start with a '# Heading' to title a new note.",
|
|
374
|
+
),
|
|
375
|
+
ToolParameter(
|
|
376
|
+
name="note_id",
|
|
377
|
+
type="string",
|
|
378
|
+
description=(
|
|
379
|
+
"Append to this note (from ima_note_search) instead of creating one."
|
|
380
|
+
),
|
|
381
|
+
required=False,
|
|
382
|
+
),
|
|
383
|
+
ToolParameter(
|
|
384
|
+
name="notebook_id",
|
|
385
|
+
type="string",
|
|
386
|
+
description="Notebook for a new note. Omit for the default location.",
|
|
387
|
+
required=False,
|
|
388
|
+
),
|
|
389
|
+
_KB_NAME_PARAM,
|
|
390
|
+
],
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
async def _run(self, client, binding, kwargs):
|
|
394
|
+
content = str(kwargs.get("content") or "").strip()
|
|
395
|
+
note_id = str(kwargs.get("note_id") or "").strip()
|
|
396
|
+
if note_id:
|
|
397
|
+
written = await client.notes.append_note(note_id, content)
|
|
398
|
+
return _ok({"note_id": written, "action": "appended"})
|
|
399
|
+
written = await client.notes.create_note(
|
|
400
|
+
content,
|
|
401
|
+
folder_id=str(kwargs.get("notebook_id") or ""),
|
|
402
|
+
)
|
|
403
|
+
if not written:
|
|
404
|
+
return _err("Tencent IMA did not return a note id; the note may not have been saved.")
|
|
405
|
+
return _ok({"note_id": written, "action": "created"})
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def _bindings(kwargs: dict[str, Any]) -> tuple[ImaBinding, ...]:
|
|
409
|
+
raw = kwargs.get(BINDINGS_KWARG)
|
|
410
|
+
if not raw:
|
|
411
|
+
return ()
|
|
412
|
+
return tuple(item for item in raw if isinstance(item, ImaBinding))
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def _no_binding_message(bindings: tuple[ImaBinding, ...]) -> str:
|
|
416
|
+
if not bindings:
|
|
417
|
+
return "No Tencent IMA knowledge base is attached to this turn; IMA tools are unavailable."
|
|
418
|
+
names = ", ".join(binding.name for binding in bindings)
|
|
419
|
+
return f"Specify which IMA knowledge base to use with kb_name. Attached: {names}."
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def _string_list(value: Any) -> list[str]:
|
|
423
|
+
if isinstance(value, str):
|
|
424
|
+
candidates = [value]
|
|
425
|
+
elif isinstance(value, (list, tuple)):
|
|
426
|
+
candidates = [str(item) for item in value]
|
|
427
|
+
else:
|
|
428
|
+
raise ValueError("urls must be a list of strings.")
|
|
429
|
+
urls = [item.strip() for item in candidates if str(item).strip()]
|
|
430
|
+
if not urls:
|
|
431
|
+
raise ValueError("At least one URL is required.")
|
|
432
|
+
return urls
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
# IMA's API refuses these; failing locally keeps the model from reporting a
|
|
436
|
+
# confusing upstream error for something that was never going to work.
|
|
437
|
+
_UNSUPPORTED_URL_MARKERS: tuple[str, ...] = (
|
|
438
|
+
"bilibili.com/video/",
|
|
439
|
+
"youtube.com/watch",
|
|
440
|
+
"youtu.be/",
|
|
441
|
+
)
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
def _is_supported_url(url: str) -> bool:
|
|
445
|
+
lowered = url.lower()
|
|
446
|
+
if not lowered.startswith(("http://", "https://")):
|
|
447
|
+
return False
|
|
448
|
+
return not any(marker in lowered for marker in _UNSUPPORTED_URL_MARKERS)
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def _ok(payload: Any) -> ToolResult:
|
|
452
|
+
return ToolResult(content=json.dumps(payload, ensure_ascii=False), success=True)
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
def _err(message: str) -> ToolResult:
|
|
456
|
+
return ToolResult(content=message, success=False)
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
IMA_TOOL_TYPES: tuple[type[BaseTool], ...] = (
|
|
460
|
+
ImaListTool,
|
|
461
|
+
ImaReadTool,
|
|
462
|
+
ImaNoteSearchTool,
|
|
463
|
+
ImaAddUrlTool,
|
|
464
|
+
ImaWriteNoteTool,
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
__all__ = [
|
|
468
|
+
"BINDINGS_KWARG",
|
|
469
|
+
"IMA_TOOL_NAMES",
|
|
470
|
+
"IMA_TOOL_TYPES",
|
|
471
|
+
"ImaAddUrlTool",
|
|
472
|
+
"ImaListTool",
|
|
473
|
+
"ImaNoteSearchTool",
|
|
474
|
+
"ImaReadTool",
|
|
475
|
+
"ImaWriteNoteTool",
|
|
476
|
+
]
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""MarginNote 4 loop capability -- agentic retrieval over a synced MN4 library."""
|
|
2
|
+
|
|
3
|
+
from deeptutor.capabilities.marginnote4.capability import MarginNoteCapability
|
|
4
|
+
from deeptutor.capabilities.marginnote4.tools import (
|
|
5
|
+
MARGINNOTE_TOOL_NAMES,
|
|
6
|
+
MARGINNOTE_TOOL_TYPES,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"MARGINNOTE_TOOL_NAMES",
|
|
11
|
+
"MARGINNOTE_TOOL_TYPES",
|
|
12
|
+
"MarginNoteCapability",
|
|
13
|
+
]
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Resolve which connected MarginNote 4 library (if any) the current turn targets.
|
|
2
|
+
|
|
3
|
+
Mirrors the Obsidian binding pattern: the first selected KB whose metadata is
|
|
4
|
+
``type == marginnote4`` wins. The KB's ``db_path`` (or a computed default)
|
|
5
|
+
becomes the live store the MarginNote tools query. The result is cached on
|
|
6
|
+
the extension namespace so ``is_active`` / ``augment_kwargs`` / ``system_block``
|
|
7
|
+
share a single resolution.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from deeptutor.core.context import UnifiedContext
|
|
13
|
+
from deeptutor.knowledge.kb_types import MARGINNOTE4_KB_TYPE
|
|
14
|
+
|
|
15
|
+
_CACHE_KEY = "_marginnote4_binding"
|
|
16
|
+
_UNSET = object()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def marginnote_binding(context: UnifiedContext) -> dict[str, str] | None:
|
|
20
|
+
"""Return ``{"name", "db_path"}`` of the selected MN4 KB, or ``None``."""
|
|
21
|
+
state = context.extension("marginnote4")
|
|
22
|
+
cached = state.get(_CACHE_KEY, _UNSET)
|
|
23
|
+
if cached is not _UNSET:
|
|
24
|
+
return cached or None
|
|
25
|
+
resolved = _resolve(context)
|
|
26
|
+
state[_CACHE_KEY] = resolved or ""
|
|
27
|
+
return resolved
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _resolve(context: UnifiedContext) -> dict[str, str] | None:
|
|
31
|
+
from deeptutor.multi_user.knowledge_access import resolve_kb_metadata
|
|
32
|
+
|
|
33
|
+
for ref in context.knowledge_bases or []:
|
|
34
|
+
ref = str(ref).strip()
|
|
35
|
+
if not ref:
|
|
36
|
+
continue
|
|
37
|
+
meta = resolve_kb_metadata(ref)
|
|
38
|
+
if not meta or meta.get("type") != MARGINNOTE4_KB_TYPE:
|
|
39
|
+
continue
|
|
40
|
+
from deeptutor.capabilities.marginnote4.store import resolve_db_path
|
|
41
|
+
|
|
42
|
+
db_path = str(resolve_db_path(ref, metadata=meta))
|
|
43
|
+
return {"name": str(meta.get("name") or ref), "db_path": db_path}
|
|
44
|
+
return None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def marginnote_kb_refs(context: UnifiedContext) -> set[str]:
|
|
48
|
+
"""Return every selected KB ref that resolves to a connected MN4 library.
|
|
49
|
+
|
|
50
|
+
All MN4 refs are reported so the chat pipeline can exclude them from the
|
|
51
|
+
``rag`` surface, exactly like Obsidian vault refs.
|
|
52
|
+
"""
|
|
53
|
+
from deeptutor.multi_user.knowledge_access import resolve_kb_metadata
|
|
54
|
+
|
|
55
|
+
refs: set[str] = set()
|
|
56
|
+
for ref in context.knowledge_bases or []:
|
|
57
|
+
ref = str(ref).strip()
|
|
58
|
+
if not ref:
|
|
59
|
+
continue
|
|
60
|
+
meta = resolve_kb_metadata(ref)
|
|
61
|
+
if meta and meta.get("type") == MARGINNOTE4_KB_TYPE:
|
|
62
|
+
refs.add(ref)
|
|
63
|
+
return refs
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
__all__ = ["marginnote_binding", "marginnote_kb_refs"]
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""MarginNote 4 loop capability -- agentic retrieval over a synced MN4 library.
|
|
2
|
+
|
|
3
|
+
Active whenever the user's selected knowledge base is a connected MarginNote 4
|
|
4
|
+
library (resolved by :mod:`deeptutor.capabilities.marginnote4.binding`). As a
|
|
5
|
+
:class:`KnowledgeCapability` it owns the turn: the chat loop runs exclusively
|
|
6
|
+
on the seven MarginNote tools (plus the ``ask_user`` floor), navigating the
|
|
7
|
+
synced study data rather than retrieving flattened RAG chunks.
|
|
8
|
+
|
|
9
|
+
The store path is injected into each tool call as ``_db_path`` server-side;
|
|
10
|
+
the model never supplies it.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from importlib import resources
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
from deeptutor.capabilities.marginnote4.binding import (
|
|
19
|
+
marginnote_binding,
|
|
20
|
+
marginnote_kb_refs,
|
|
21
|
+
)
|
|
22
|
+
from deeptutor.capabilities.marginnote4.tools import MARGINNOTE_TOOL_NAMES
|
|
23
|
+
from deeptutor.capabilities.protocol import KnowledgeCapability, PromptBlock
|
|
24
|
+
from deeptutor.core.context import UnifiedContext
|
|
25
|
+
from deeptutor.services.prompt.lookup import prompt_text as _prompt_text
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class MarginNoteCapability(KnowledgeCapability):
|
|
29
|
+
"""Turn-scoped integration for a connected MarginNote 4 library."""
|
|
30
|
+
|
|
31
|
+
name = "marginnote4"
|
|
32
|
+
owned_tools = MARGINNOTE_TOOL_NAMES
|
|
33
|
+
|
|
34
|
+
def is_active(self, context: UnifiedContext) -> bool:
|
|
35
|
+
return marginnote_binding(context) is not None
|
|
36
|
+
|
|
37
|
+
def owned_kbs(self, context: UnifiedContext) -> set[str]:
|
|
38
|
+
return marginnote_kb_refs(context)
|
|
39
|
+
|
|
40
|
+
def system_block(
|
|
41
|
+
self,
|
|
42
|
+
context: UnifiedContext,
|
|
43
|
+
*,
|
|
44
|
+
language: str,
|
|
45
|
+
prompts: dict[str, Any],
|
|
46
|
+
) -> PromptBlock | None:
|
|
47
|
+
binding = marginnote_binding(context)
|
|
48
|
+
if binding is None:
|
|
49
|
+
return None
|
|
50
|
+
override = _prompt_text(prompts, ("marginnote4", "system"))
|
|
51
|
+
content = override or _load_system_prompt(language)
|
|
52
|
+
return PromptBlock("marginnote4", content.replace("{library_name}", binding["name"]))
|
|
53
|
+
|
|
54
|
+
def augment_kwargs(
|
|
55
|
+
self,
|
|
56
|
+
tool_name: str,
|
|
57
|
+
kwargs: dict[str, Any],
|
|
58
|
+
context: UnifiedContext,
|
|
59
|
+
) -> dict[str, Any]:
|
|
60
|
+
if tool_name not in MARGINNOTE_TOOL_NAMES:
|
|
61
|
+
return kwargs
|
|
62
|
+
binding = marginnote_binding(context)
|
|
63
|
+
if binding is None:
|
|
64
|
+
return kwargs
|
|
65
|
+
updated = dict(kwargs)
|
|
66
|
+
updated["_db_path"] = binding["db_path"]
|
|
67
|
+
return updated
|
|
68
|
+
|
|
69
|
+
def pre_loop_seed(self, context: UnifiedContext) -> str:
|
|
70
|
+
_ = context
|
|
71
|
+
return ""
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _load_system_prompt(language: str) -> str:
|
|
75
|
+
lang = "zh" if language.lower().startswith("zh") else "en"
|
|
76
|
+
prompt = resources.files(__package__).joinpath("prompts", lang, "system.md")
|
|
77
|
+
return prompt.read_text(encoding="utf-8").strip()
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
__all__ = ["MarginNoteCapability"]
|