know-do-graph 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agents/__init__.py +0 -0
- agents/extraction_agent/__init__.py +0 -0
- agents/extraction_agent/agent.py +170 -0
- agents/graph_agent/__init__.py +5 -0
- agents/graph_agent/agent.py +373 -0
- agents/graph_agent/tools.py +2106 -0
- agents/maintenance_agent/__init__.py +0 -0
- agents/maintenance_agent/agent.py +283 -0
- agents/orchestrator/__init__.py +0 -0
- agents/orchestrator/agent.py +217 -0
- agents/review_agent/__init__.py +0 -0
- agents/review_agent/agent.py +188 -0
- agents/review_agent/tools.py +472 -0
- api/__init__.py +0 -0
- api/main.py +136 -0
- api/routes/__init__.py +0 -0
- api/routes/agent.py +81 -0
- api/routes/entries.py +411 -0
- api/routes/graph.py +132 -0
- api/routes/mem.py +179 -0
- api/routes/remote.py +815 -0
- api/routes/remote_sync.py +230 -0
- api/routes/retrieve.py +88 -0
- core/__init__.py +0 -0
- core/app_state.py +9 -0
- core/events.py +84 -0
- core/extraction/__init__.py +0 -0
- core/extraction/wikilink_parser.py +48 -0
- core/graph/__init__.py +0 -0
- core/graph/graph.py +204 -0
- core/memory/__init__.py +0 -0
- core/memory/memgraph.py +458 -0
- core/resources/starter.db +0 -0
- core/retrieval/__init__.py +0 -0
- core/retrieval/embedder.py +122 -0
- core/retrieval/fusion.py +52 -0
- core/retrieval/progressive.py +399 -0
- core/retrieval/retrieval.py +346 -0
- core/retrieval/vector_store.py +91 -0
- core/schemas/__init__.py +0 -0
- core/schemas/edge.py +46 -0
- core/schemas/entry.py +388 -0
- core/storage/__init__.py +0 -0
- core/storage/database.py +104 -0
- core/storage/models.py +66 -0
- core/storage/repository.py +243 -0
- core/sync/__init__.py +20 -0
- core/sync/autolink.py +301 -0
- core/sync/db_merge.py +297 -0
- core/sync/db_watcher.py +84 -0
- core/sync/remote_sync.py +345 -0
- examples/__init__.py +0 -0
- examples/example_entries.py +206 -0
- examples/pymatgen_interface_examples.py +811 -0
- frontend/dist/assets/index-BLfo7ZZu.css +1 -0
- frontend/dist/assets/index-G-mYbZ9R.js +83 -0
- frontend/dist/assets/index-G-mYbZ9R.js.map +1 -0
- frontend/dist/index.html +92 -0
- know_do_graph-0.1.0.dist-info/METADATA +765 -0
- know_do_graph-0.1.0.dist-info/RECORD +63 -0
- know_do_graph-0.1.0.dist-info/WHEEL +4 -0
- know_do_graph-0.1.0.dist-info/entry_points.txt +2 -0
- main.py +944 -0
agents/__init__.py
ADDED
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"""Extraction agent.
|
|
2
|
+
|
|
3
|
+
Reads source material (files, raw text) and populates the graph with
|
|
4
|
+
structured Entry objects. After insertion it can resolve [[wikilinks]]
|
|
5
|
+
to create typed Edge relations between entries.
|
|
6
|
+
|
|
7
|
+
Supported extraction meta-skills
|
|
8
|
+
---------------------------------
|
|
9
|
+
* File reading/writing
|
|
10
|
+
* Wikilink parsing
|
|
11
|
+
* External reference extraction
|
|
12
|
+
* Entry creation
|
|
13
|
+
* Edge creation / dependency linking
|
|
14
|
+
* Source provenance tracking
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import Optional
|
|
21
|
+
|
|
22
|
+
from core.extraction.wikilink_parser import (
|
|
23
|
+
extract_external_refs,
|
|
24
|
+
parse_wikilinks,
|
|
25
|
+
slug_from_title,
|
|
26
|
+
)
|
|
27
|
+
from core.graph.graph import KnowDoGraph
|
|
28
|
+
from core.schemas.edge import Edge, EdgeRelation
|
|
29
|
+
from core.schemas.entry import Entry, EntryMetadata, EntryType, RefinementStatus
|
|
30
|
+
from core.storage.database import SessionLocal
|
|
31
|
+
from core.storage.repository import EdgeRepository, EntryRepository
|
|
32
|
+
|
|
33
|
+
_TEXT_EXTENSIONS = {".md", ".txt", ".rst", ".yaml", ".yml", ".json"}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class ExtractionAgent:
|
|
37
|
+
"""Reads source documents and extracts Entry objects into the graph.
|
|
38
|
+
|
|
39
|
+
Parameters
|
|
40
|
+
----------
|
|
41
|
+
graph:
|
|
42
|
+
The shared in-process KnowDoGraph instance to keep in sync.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def __init__(self, graph: KnowDoGraph) -> None:
|
|
46
|
+
self._graph = graph
|
|
47
|
+
|
|
48
|
+
# ------------------------------------------------------------------
|
|
49
|
+
# Public extraction methods
|
|
50
|
+
# ------------------------------------------------------------------
|
|
51
|
+
|
|
52
|
+
def extract_from_file(
|
|
53
|
+
self,
|
|
54
|
+
path: Path,
|
|
55
|
+
entry_type: EntryType = EntryType.generic,
|
|
56
|
+
tags: Optional[list[str]] = None,
|
|
57
|
+
source_provenance: Optional[str] = None,
|
|
58
|
+
) -> Entry:
|
|
59
|
+
"""Create an Entry from a single text file."""
|
|
60
|
+
content = path.read_text(encoding="utf-8", errors="replace")
|
|
61
|
+
title = path.stem.replace("-", " ").replace("_", " ").title()
|
|
62
|
+
entry = Entry(
|
|
63
|
+
title=title,
|
|
64
|
+
slug=slug_from_title(title),
|
|
65
|
+
entry_type=entry_type,
|
|
66
|
+
content=content,
|
|
67
|
+
tags=tags or [],
|
|
68
|
+
metadata=EntryMetadata(
|
|
69
|
+
source_provenance=source_provenance or str(path),
|
|
70
|
+
extraction_method="file_read",
|
|
71
|
+
refinement_status=RefinementStatus.raw,
|
|
72
|
+
external_refs=extract_external_refs(content),
|
|
73
|
+
),
|
|
74
|
+
)
|
|
75
|
+
return self._persist_entry(entry)
|
|
76
|
+
|
|
77
|
+
def extract_from_directory(
|
|
78
|
+
self,
|
|
79
|
+
directory: Path,
|
|
80
|
+
entry_type: EntryType = EntryType.generic,
|
|
81
|
+
tags: Optional[list[str]] = None,
|
|
82
|
+
recursive: bool = True,
|
|
83
|
+
) -> list[Entry]:
|
|
84
|
+
"""Extract entries from all text files in *directory*."""
|
|
85
|
+
glob = directory.rglob("*") if recursive else directory.glob("*")
|
|
86
|
+
files = [
|
|
87
|
+
f
|
|
88
|
+
for f in glob
|
|
89
|
+
if f.is_file() and f.suffix.lower() in _TEXT_EXTENSIONS
|
|
90
|
+
]
|
|
91
|
+
return [
|
|
92
|
+
self.extract_from_file(f, entry_type=entry_type, tags=tags)
|
|
93
|
+
for f in files
|
|
94
|
+
]
|
|
95
|
+
|
|
96
|
+
def extract_from_text(
|
|
97
|
+
self,
|
|
98
|
+
title: str,
|
|
99
|
+
content: str,
|
|
100
|
+
entry_type: EntryType = EntryType.generic,
|
|
101
|
+
tags: Optional[list[str]] = None,
|
|
102
|
+
source_provenance: Optional[str] = None,
|
|
103
|
+
) -> Entry:
|
|
104
|
+
"""Create an Entry from raw text."""
|
|
105
|
+
entry = Entry(
|
|
106
|
+
title=title,
|
|
107
|
+
slug=slug_from_title(title),
|
|
108
|
+
entry_type=entry_type,
|
|
109
|
+
content=content,
|
|
110
|
+
tags=tags or [],
|
|
111
|
+
metadata=EntryMetadata(
|
|
112
|
+
source_provenance=source_provenance,
|
|
113
|
+
extraction_method="text_input",
|
|
114
|
+
refinement_status=RefinementStatus.raw,
|
|
115
|
+
external_refs=extract_external_refs(content),
|
|
116
|
+
),
|
|
117
|
+
)
|
|
118
|
+
return self._persist_entry(entry)
|
|
119
|
+
|
|
120
|
+
# ------------------------------------------------------------------
|
|
121
|
+
# Wikilink resolution
|
|
122
|
+
# ------------------------------------------------------------------
|
|
123
|
+
|
|
124
|
+
def resolve_wikilinks(self) -> int:
|
|
125
|
+
"""Resolve all [[wikilinks]] across entries and create edges.
|
|
126
|
+
|
|
127
|
+
Returns the number of new edges created.
|
|
128
|
+
"""
|
|
129
|
+
created = 0
|
|
130
|
+
with SessionLocal() as db:
|
|
131
|
+
entry_repo = EntryRepository(db)
|
|
132
|
+
edge_repo = EdgeRepository(db)
|
|
133
|
+
all_entries = entry_repo.get_all()
|
|
134
|
+
slug_map = {e.slug: e.id for e in all_entries}
|
|
135
|
+
title_map = {e.title.lower(): e.id for e in all_entries}
|
|
136
|
+
alias_map: dict[str, str] = {}
|
|
137
|
+
for e in all_entries:
|
|
138
|
+
for a in e.aliases:
|
|
139
|
+
alias_map.setdefault(a.lower(), e.id)
|
|
140
|
+
|
|
141
|
+
for entry in all_entries:
|
|
142
|
+
for ref in entry.internal_refs:
|
|
143
|
+
ref_slug = slug_from_title(ref)
|
|
144
|
+
ref_lower = ref.lower()
|
|
145
|
+
target_id = (
|
|
146
|
+
slug_map.get(ref_slug)
|
|
147
|
+
or title_map.get(ref_lower)
|
|
148
|
+
or alias_map.get(ref_lower)
|
|
149
|
+
)
|
|
150
|
+
if target_id and target_id != entry.id:
|
|
151
|
+
edge = Edge(
|
|
152
|
+
source_id=entry.id,
|
|
153
|
+
target_id=target_id,
|
|
154
|
+
relation=EdgeRelation.wikilink,
|
|
155
|
+
)
|
|
156
|
+
saved = edge_repo.create(edge)
|
|
157
|
+
self._graph.add_edge(saved)
|
|
158
|
+
created += 1
|
|
159
|
+
return created
|
|
160
|
+
|
|
161
|
+
# ------------------------------------------------------------------
|
|
162
|
+
# Internal helpers
|
|
163
|
+
# ------------------------------------------------------------------
|
|
164
|
+
|
|
165
|
+
def _persist_entry(self, entry: Entry) -> Entry:
|
|
166
|
+
with SessionLocal() as db:
|
|
167
|
+
repo = EntryRepository(db)
|
|
168
|
+
saved = repo.create(entry)
|
|
169
|
+
self._graph.add_entry(saved)
|
|
170
|
+
return saved
|
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
"""GraphAgent — LLM-driven agent for know-do graph management.
|
|
2
|
+
|
|
3
|
+
Uses the OpenAI function-calling API (compatible with any OpenAI-compatible
|
|
4
|
+
endpoint, e.g. Alibaba Cloud DashScope) to let a language model manipulate the
|
|
5
|
+
graph through structured tool calls.
|
|
6
|
+
|
|
7
|
+
Configuration is read from environment variables:
|
|
8
|
+
OPENAI_API_KEY — API key (required)
|
|
9
|
+
OPENAI_API_BASE — base URL override (optional, defaults to OpenAI)
|
|
10
|
+
GRAPH_AGENT_MODEL — model name (optional, defaults to openai/glm-5.1)
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import os
|
|
17
|
+
from typing import Any, Callable, Iterator
|
|
18
|
+
|
|
19
|
+
from openai import OpenAI
|
|
20
|
+
|
|
21
|
+
from core import events as _events
|
|
22
|
+
from core.graph.graph import KnowDoGraph
|
|
23
|
+
from agents.graph_agent.tools import TOOL_DISPATCH, TOOL_SCHEMAS
|
|
24
|
+
|
|
25
|
+
_DEFAULT_MODEL = "qwen-plus"
|
|
26
|
+
|
|
27
|
+
# Tool calls that mutate the graph; the frontend is notified via SSE so it
|
|
28
|
+
# can refresh after each such call.
|
|
29
|
+
_MUTATING_TOOLS: set[str] = {
|
|
30
|
+
"create_entry",
|
|
31
|
+
"update_entry",
|
|
32
|
+
"delete_entry",
|
|
33
|
+
"create_edge",
|
|
34
|
+
"delete_edge",
|
|
35
|
+
"merge_entries",
|
|
36
|
+
"resolve_wikilinks",
|
|
37
|
+
"remove_dangling_edges",
|
|
38
|
+
"create_script_entry",
|
|
39
|
+
"add_script_to_entry",
|
|
40
|
+
"attach_script_to_entry",
|
|
41
|
+
"add_asset_to_entry",
|
|
42
|
+
"build_material_interface_workflow",
|
|
43
|
+
"create_material_entry",
|
|
44
|
+
"submit_feedback",
|
|
45
|
+
"create_heuristic",
|
|
46
|
+
"create_constraint",
|
|
47
|
+
"decompose_capability",
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
# Read-only tools exposed when the agent is instantiated in query-only mode.
|
|
51
|
+
# Mutating tools are excluded so external agents can query the graph without
|
|
52
|
+
# accidentally writing nodes or edges.
|
|
53
|
+
_READ_ONLY_TOOLS: set[str] = {
|
|
54
|
+
"get_entry",
|
|
55
|
+
"search_entries",
|
|
56
|
+
"list_entries",
|
|
57
|
+
"get_neighbors",
|
|
58
|
+
"graph_stats",
|
|
59
|
+
"fetch_url",
|
|
60
|
+
"web_search",
|
|
61
|
+
"find_similar_nodes",
|
|
62
|
+
"get_graph_overview",
|
|
63
|
+
"list_nodes_by_type",
|
|
64
|
+
"get_script",
|
|
65
|
+
"list_scripts",
|
|
66
|
+
"list_assets",
|
|
67
|
+
"list_by_verification",
|
|
68
|
+
"list_needs_generalization",
|
|
69
|
+
"retrieve_plan",
|
|
70
|
+
"retrieve_heuristics",
|
|
71
|
+
"retrieve_constraints",
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
_READ_ONLY_SYSTEM_PROMPT = """You are a read-only knowledge-graph query assistant for the Know-Do Graph system.
|
|
75
|
+
|
|
76
|
+
Your role is to **answer questions** about the graph — searching, retrieving, and
|
|
77
|
+
summarising existing entries and relationships. You do NOT add, modify, or delete
|
|
78
|
+
any nodes or edges.
|
|
79
|
+
|
|
80
|
+
## Search strategy
|
|
81
|
+
Use ``search_entries`` or ``find_similar_nodes`` for free-text queries.
|
|
82
|
+
Use ``get_entry`` to fetch full details of a specific node.
|
|
83
|
+
Use ``get_neighbors`` to explore relationships.
|
|
84
|
+
Use ``get_graph_overview`` to orient yourself when asked general questions.
|
|
85
|
+
Use ``list_nodes_by_type`` to enumerate nodes of a given category.
|
|
86
|
+
|
|
87
|
+
## Important
|
|
88
|
+
- Do NOT attempt to create, update, delete, or merge any entries or edges.
|
|
89
|
+
- Do NOT call any write operations; only read/query tools are available.
|
|
90
|
+
- Summarise and return what exists in the graph as clearly as possible.
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
_SYSTEM_PROMPT = """You are an expert knowledge-graph management assistant for the Know-Do Graph system.
|
|
94
|
+
|
|
95
|
+
The graph stores structured *entries* (nodes) and typed *edges* between them.
|
|
96
|
+
Entries can represent capabilities, procedures, tools, workflows, dependencies,
|
|
97
|
+
scripts, materials, material interfaces, and more.
|
|
98
|
+
|
|
99
|
+
## Node naming conventions
|
|
100
|
+
- Titles must be short, canonical, and human-readable (3–7 words preferred).
|
|
101
|
+
- Do NOT embed abbreviations or acronyms inside parentheses in the title (e.g. avoid
|
|
102
|
+
"Density Functional Theory (DFT)"). Instead put the acronym in `aliases`.
|
|
103
|
+
- Tags must be lowercase, hyphenated, and domain-specific.
|
|
104
|
+
|
|
105
|
+
## Abstraction rule (CRITICAL — read carefully)
|
|
106
|
+
Skill nodes should describe a **reusable capability**, not a single concrete
|
|
107
|
+
instance. Concrete instances belong in the `content` (as examples) or as a
|
|
108
|
+
parameter, NOT as their own node.
|
|
109
|
+
|
|
110
|
+
BAD: "Build H2O molecule", "Build CH4 molecule", "Build NH3 molecule"
|
|
111
|
+
→ three near-identical nodes that pollute the graph.
|
|
112
|
+
GOOD: One node "Build molecule from formula" whose content explains the
|
|
113
|
+
general procedure and lists examples (H2O, CH4, NH3).
|
|
114
|
+
|
|
115
|
+
BAD: "TiO2/SrTiO3 Interface", "MgO/Fe Interface", "GaN/AlN Interface"
|
|
116
|
+
→ one node per material pair.
|
|
117
|
+
GOOD: One "Material interface construction" capability node + one
|
|
118
|
+
"Slab-stacking procedure" node, parameterised over material formulas.
|
|
119
|
+
|
|
120
|
+
Exception: a specific instance is worth its own node ONLY when (a) it has
|
|
121
|
+
unique constraints/data not derivable from the general procedure, OR (b) it
|
|
122
|
+
is a famous/canonical reference that other procedures cite.
|
|
123
|
+
|
|
124
|
+
Before calling `create_entry`:
|
|
125
|
+
1. Call `find_similar_nodes` with both the specific title AND a generalised
|
|
126
|
+
version (e.g. for "Build H2O", also search "build molecule").
|
|
127
|
+
2. If a generic match exists, do NOT create a new node — either link to the
|
|
128
|
+
existing one or extend its content with the new example.
|
|
129
|
+
3. If no generic match exists, ask yourself: "Could a sibling node for a
|
|
130
|
+
different parameter value exist?" If yes, create the **generic** node, not
|
|
131
|
+
the specific one.
|
|
132
|
+
|
|
133
|
+
`create_entry` will set a `needs_generalization` flag on any node whose title
|
|
134
|
+
overlaps an existing one — treat that as a signal to merge or rename.
|
|
135
|
+
|
|
136
|
+
## Entry types
|
|
137
|
+
- **capability** – what a system/tool can do; also used for material interfaces, known constructs, and runnable scripts (when `script_language` is set in metadata).
|
|
138
|
+
- **procedure** – step-by-step instructions.
|
|
139
|
+
- **workflow** – higher-level sequence linking multiple procedures.
|
|
140
|
+
- **tool** – software library, CLI tool, API, or instrument.
|
|
141
|
+
- **repository** – code repository or data repository.
|
|
142
|
+
- **environment** – computational or lab environment.
|
|
143
|
+
- **dependency** – package, library, or external service required by others.
|
|
144
|
+
- **data** – dataset, structural file, computed result, or reference material (crystals, compounds).
|
|
145
|
+
- **analytical** – analysis method or metric.
|
|
146
|
+
- **memory** – operational memory trace.
|
|
147
|
+
- **heuristic** – L3 conditional, empirical guidance attached to a skill (see hierarchical memory below).
|
|
148
|
+
- **constraint** – L4 known failure mode or limitation attached to a skill.
|
|
149
|
+
- **generic** – catch-all for entries that do not fit above.
|
|
150
|
+
|
|
151
|
+
## Hierarchical memory (L1–L4) — progressive disclosure
|
|
152
|
+
The graph is organised into four orthogonal levels so planners can pull only
|
|
153
|
+
the level of detail they need:
|
|
154
|
+
|
|
155
|
+
- **L1 — Capability** (`capability` / `workflow`)
|
|
156
|
+
Reusable high-level ability. Planner-friendly. Stays domain-agnostic
|
|
157
|
+
when possible. Example: "construct amorphous structures".
|
|
158
|
+
- **L2 — Procedure** (`procedure`)
|
|
159
|
+
Executable workflow decomposition / tool sequencing. Example:
|
|
160
|
+
"initialize random structure → anneal → controlled quench → relax".
|
|
161
|
+
- **L3 — Heuristic** (`heuristic`)
|
|
162
|
+
Operational experience: conditional, empirical guidance. NOT a universal
|
|
163
|
+
truth. Example: "cooling rate strongly affects sp2/sp3 ratio".
|
|
164
|
+
- **L4 — Constraint / Failure Mode** (`constraint`)
|
|
165
|
+
Known limitation or failure pattern. Example: "unsuitable for
|
|
166
|
+
bond-breaking processes". Verifier-guided debugging starts here.
|
|
167
|
+
|
|
168
|
+
**Critical rules**
|
|
169
|
+
1. Do NOT embed heuristics or failure modes inside a capability's `content`
|
|
170
|
+
blob. Create them as separate L3 / L4 nodes via `create_heuristic` /
|
|
171
|
+
`create_constraint` so progressive retrieval can surface them on demand.
|
|
172
|
+
2. Do NOT encode domain-specific details directly into L1 capability names.
|
|
173
|
+
Prefer "construct amorphous structures" over "construct amorphous carbon
|
|
174
|
+
via Tersoff melt-quench". System-specific knowledge lives in L3/L4.
|
|
175
|
+
3. When you create an L2 procedure that implements an existing L1 capability,
|
|
176
|
+
wire the link with `decompose_capability(capability, procedure)`.
|
|
177
|
+
4. For retrieval, prefer the staged tools:
|
|
178
|
+
- `retrieve_plan(goal)` for planning (returns L1 + L2 only).
|
|
179
|
+
- `retrieve_heuristics(skill)` once a candidate is chosen.
|
|
180
|
+
- `retrieve_constraints(skill)` when the verifier reports an issue or you
|
|
181
|
+
need to estimate execution risk.
|
|
182
|
+
This avoids polluting the planning context with the full knowledge dump.
|
|
183
|
+
|
|
184
|
+
## Verification & feedback
|
|
185
|
+
Every node carries `verification_status` (unverified | self_tested |
|
|
186
|
+
peer_reviewed | community_tested | bugged | deprecated). New nodes default to
|
|
187
|
+
`unverified`. When you or an external agent confirms a node works (or fails),
|
|
188
|
+
call `submit_feedback` with a verdict — this is how the graph self-evolves.
|
|
189
|
+
|
|
190
|
+
## Script workflow
|
|
191
|
+
Scripts are **capability** entries with `script_language` set in metadata.
|
|
192
|
+
1. Use ``create_script_entry`` to add runnable scripts.
|
|
193
|
+
2. Link scripts to procedures/capabilities via ``attach_script_to_entry``.
|
|
194
|
+
3. Any entry with `script_language` set can be downloaded at ``GET /entries/{id}/download``.
|
|
195
|
+
|
|
196
|
+
## Node assets (folder-style)
|
|
197
|
+
Every node behaves like a small folder containing typed assets, addressable as
|
|
198
|
+
`[entry]/[folder]/[filename]` and served at
|
|
199
|
+
``GET /entries/{id}/assets/{folder}/{filename}``.
|
|
200
|
+
|
|
201
|
+
Conventional folders (free-form names also allowed):
|
|
202
|
+
- ``scripts`` — runnable code (Python/bash/…)
|
|
203
|
+
- ``references`` — URLs to papers, repos, docs (use ``kind="link"``)
|
|
204
|
+
- ``docs`` — markdown/text documentation (``kind="text"``)
|
|
205
|
+
- ``examples`` — example input files, configs, notebooks
|
|
206
|
+
- ``data`` — small datasets / structural files
|
|
207
|
+
- ``notes`` — free-form annotations
|
|
208
|
+
|
|
209
|
+
Use ``add_asset_to_entry`` for anything beyond a script (URL, doc, example file).
|
|
210
|
+
Use ``add_script_to_entry`` for runnable scripts (auto-targets the ``scripts`` folder).
|
|
211
|
+
Use ``list_assets`` to inspect a node's folder tree.
|
|
212
|
+
|
|
213
|
+
## Search strategy
|
|
214
|
+
Both ``search_entries`` and ``find_similar_nodes`` support three modes:
|
|
215
|
+
- **hybrid** (default) — fuses embedding vector similarity (ANN) with keyword scoring via
|
|
216
|
+
Reciprocal Rank Fusion, then re-ranks by verification trust and usage count. Best general-purpose choice.
|
|
217
|
+
- **semantic** — embedding-only. Use when the exact words differ but the concept is the same
|
|
218
|
+
(paraphrases, synonyms, related domains). Good for catching near-duplicates with different wording.
|
|
219
|
+
- **keyword** — exact text matching on title, aliases, tags, content. Use for known acronyms,
|
|
220
|
+
formula strings, or when you need precise title lookup.
|
|
221
|
+
|
|
222
|
+
If an initial search returns poor results, try a different mode or rephrase/broaden the query
|
|
223
|
+
before assuming no match exists. For duplicate detection, run at least one semantic pass.
|
|
224
|
+
|
|
225
|
+
## Workflow for adding new knowledge
|
|
226
|
+
1. Call ``get_graph_overview`` to orient yourself.
|
|
227
|
+
2. For every concept you intend to create, search for both the specific and
|
|
228
|
+
generalised name with ``find_similar_nodes``.
|
|
229
|
+
3. Choose the most appropriate ``entry_type``; write clean lowercase
|
|
230
|
+
hyphenated tags; put abbreviations in ``aliases``.
|
|
231
|
+
4. Wire meaningful typed edges. Do not leave nodes isolated.
|
|
232
|
+
5. Resolve wikilinks when done.
|
|
233
|
+
|
|
234
|
+
## Workflow for restructuring / cleaning
|
|
235
|
+
- Use ``find_similar_nodes`` to detect near-duplicates before merging.
|
|
236
|
+
- Use ``merge_entries`` to consolidate duplicates.
|
|
237
|
+
- Use ``list_needs_generalization`` to find nodes flagged as too specific.
|
|
238
|
+
- Fix titles that contain parenthetical acronyms by moving the acronym to aliases.
|
|
239
|
+
|
|
240
|
+
Always confirm actions taken and briefly summarise what you did.
|
|
241
|
+
"""
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
class GraphAgent:
|
|
245
|
+
"""LLM-powered agent that manipulates the Know-Do Graph via tool calls.
|
|
246
|
+
|
|
247
|
+
Parameters
|
|
248
|
+
----------
|
|
249
|
+
graph:
|
|
250
|
+
The shared ``KnowDoGraph`` instance.
|
|
251
|
+
model:
|
|
252
|
+
Model identifier forwarded to the OpenAI client.
|
|
253
|
+
"""
|
|
254
|
+
|
|
255
|
+
def __init__(
|
|
256
|
+
self,
|
|
257
|
+
graph: KnowDoGraph,
|
|
258
|
+
model: str | None = None,
|
|
259
|
+
on_step: Callable[[str, dict], None] | None = None,
|
|
260
|
+
read_only: bool = False,
|
|
261
|
+
) -> None:
|
|
262
|
+
self._graph = graph
|
|
263
|
+
self._model = model or os.environ.get("GRAPH_AGENT_MODEL", _DEFAULT_MODEL)
|
|
264
|
+
self._client = OpenAI(
|
|
265
|
+
api_key=os.environ["OPENAI_API_KEY"],
|
|
266
|
+
base_url=os.environ.get("OPENAI_API_BASE"),
|
|
267
|
+
)
|
|
268
|
+
self._read_only = read_only
|
|
269
|
+
system_prompt = _READ_ONLY_SYSTEM_PROMPT if read_only else _SYSTEM_PROMPT
|
|
270
|
+
self._history: list[dict] = [{"role": "system", "content": system_prompt}]
|
|
271
|
+
self._on_step = on_step
|
|
272
|
+
# Filter tool schemas to read-only set when in query-only mode
|
|
273
|
+
self._tool_schemas = (
|
|
274
|
+
[s for s in TOOL_SCHEMAS if s["function"]["name"] in _READ_ONLY_TOOLS]
|
|
275
|
+
if read_only
|
|
276
|
+
else TOOL_SCHEMAS
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
# ------------------------------------------------------------------
|
|
280
|
+
# Public interface
|
|
281
|
+
# ------------------------------------------------------------------
|
|
282
|
+
|
|
283
|
+
def chat(self, user_message: str) -> str:
|
|
284
|
+
"""Send one turn and return the final assistant text response."""
|
|
285
|
+
self._history.append({"role": "user", "content": user_message})
|
|
286
|
+
response_text = self._run_loop()
|
|
287
|
+
self._history.append({"role": "assistant", "content": response_text})
|
|
288
|
+
return response_text
|
|
289
|
+
|
|
290
|
+
def reset(self) -> None:
|
|
291
|
+
"""Clear conversation history (keeps system prompt)."""
|
|
292
|
+
self._history = [self._history[0]]
|
|
293
|
+
|
|
294
|
+
# ------------------------------------------------------------------
|
|
295
|
+
# Internal agentic loop
|
|
296
|
+
# ------------------------------------------------------------------
|
|
297
|
+
|
|
298
|
+
def _run_loop(self) -> str:
|
|
299
|
+
"""Run the tool-call loop until the model produces a final reply."""
|
|
300
|
+
MAX_ITERATIONS = 20
|
|
301
|
+
for i in range(MAX_ITERATIONS):
|
|
302
|
+
if self._on_step:
|
|
303
|
+
self._on_step("thinking", {"iteration": i + 1})
|
|
304
|
+
|
|
305
|
+
response = self._client.chat.completions.create(
|
|
306
|
+
model=self._model,
|
|
307
|
+
messages=self._history,
|
|
308
|
+
tools=self._tool_schemas,
|
|
309
|
+
tool_choice="auto",
|
|
310
|
+
)
|
|
311
|
+
message = response.choices[0].message
|
|
312
|
+
|
|
313
|
+
# No tool calls — model is done
|
|
314
|
+
if not message.tool_calls:
|
|
315
|
+
return message.content or ""
|
|
316
|
+
|
|
317
|
+
# Append assistant message with tool_calls
|
|
318
|
+
self._history.append(message.model_dump(exclude_unset=True))
|
|
319
|
+
|
|
320
|
+
# Execute each tool call and collect results
|
|
321
|
+
for tc in message.tool_calls:
|
|
322
|
+
try:
|
|
323
|
+
display_args = {k: v for k, v in json.loads(tc.function.arguments or "{}").items() if k != "graph"}
|
|
324
|
+
except Exception:
|
|
325
|
+
display_args = {}
|
|
326
|
+
if self._on_step:
|
|
327
|
+
self._on_step("tool_call", {"name": tc.function.name, "args": display_args})
|
|
328
|
+
|
|
329
|
+
result = self._dispatch(tc.function.name, tc.function.arguments)
|
|
330
|
+
|
|
331
|
+
if self._on_step:
|
|
332
|
+
self._on_step("tool_result", {"name": tc.function.name, "result": result})
|
|
333
|
+
|
|
334
|
+
self._history.append(
|
|
335
|
+
{
|
|
336
|
+
"role": "tool",
|
|
337
|
+
"tool_call_id": tc.id,
|
|
338
|
+
"content": json.dumps(result, default=str),
|
|
339
|
+
}
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
return "Agent reached maximum iterations without a final answer."
|
|
343
|
+
|
|
344
|
+
def _dispatch(self, name: str, arguments_json: str) -> Any:
|
|
345
|
+
"""Call the named tool with the provided JSON arguments."""
|
|
346
|
+
# Guard: in read-only mode, reject any mutating tool that somehow slips through
|
|
347
|
+
if self._read_only and name in _MUTATING_TOOLS:
|
|
348
|
+
return {"error": f"Tool '{name}' is not available in read-only mode."}
|
|
349
|
+
|
|
350
|
+
func = TOOL_DISPATCH.get(name)
|
|
351
|
+
if func is None:
|
|
352
|
+
return {"error": f"Unknown tool: {name}"}
|
|
353
|
+
try:
|
|
354
|
+
kwargs = json.loads(arguments_json) if arguments_json else {}
|
|
355
|
+
except json.JSONDecodeError as exc:
|
|
356
|
+
return {"error": f"Bad arguments JSON: {exc}"}
|
|
357
|
+
|
|
358
|
+
# Inject the live graph instance into every call
|
|
359
|
+
kwargs["graph"] = self._graph
|
|
360
|
+
try:
|
|
361
|
+
result = func(**kwargs)
|
|
362
|
+
except Exception as exc: # noqa: BLE001
|
|
363
|
+
return {"error": str(exc)}
|
|
364
|
+
|
|
365
|
+
# Broadcast a refresh hint so connected frontends re-fetch the graph.
|
|
366
|
+
if name in _MUTATING_TOOLS:
|
|
367
|
+
is_error = isinstance(result, dict) and "error" in result
|
|
368
|
+
if not is_error:
|
|
369
|
+
try:
|
|
370
|
+
_events.emit("graph_changed", {"tool": name})
|
|
371
|
+
except Exception:
|
|
372
|
+
pass
|
|
373
|
+
return result
|