trustgraph-cli 2.1.9__tar.gz → 2.1.10__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.
Files changed (82) hide show
  1. {trustgraph_cli-2.1.9 → trustgraph_cli-2.1.10}/PKG-INFO +1 -1
  2. trustgraph_cli-2.1.10/trustgraph/cli/invoke_graph_rag.py +780 -0
  3. trustgraph_cli-2.1.10/trustgraph/cli_version.py +1 -0
  4. {trustgraph_cli-2.1.9 → trustgraph_cli-2.1.10}/trustgraph_cli.egg-info/PKG-INFO +1 -1
  5. trustgraph_cli-2.1.9/trustgraph/cli/invoke_graph_rag.py +0 -164
  6. trustgraph_cli-2.1.9/trustgraph/cli_version.py +0 -1
  7. {trustgraph_cli-2.1.9 → trustgraph_cli-2.1.10}/README.md +0 -0
  8. {trustgraph_cli-2.1.9 → trustgraph_cli-2.1.10}/pyproject.toml +0 -0
  9. {trustgraph_cli-2.1.9 → trustgraph_cli-2.1.10}/setup.cfg +0 -0
  10. {trustgraph_cli-2.1.9 → trustgraph_cli-2.1.10}/trustgraph/cli/__init__.py +0 -0
  11. {trustgraph_cli-2.1.9 → trustgraph_cli-2.1.10}/trustgraph/cli/add_library_document.py +0 -0
  12. {trustgraph_cli-2.1.9 → trustgraph_cli-2.1.10}/trustgraph/cli/delete_collection.py +0 -0
  13. {trustgraph_cli-2.1.9 → trustgraph_cli-2.1.10}/trustgraph/cli/delete_config_item.py +0 -0
  14. {trustgraph_cli-2.1.9 → trustgraph_cli-2.1.10}/trustgraph/cli/delete_flow_blueprint.py +0 -0
  15. {trustgraph_cli-2.1.9 → trustgraph_cli-2.1.10}/trustgraph/cli/delete_kg_core.py +0 -0
  16. {trustgraph_cli-2.1.9 → trustgraph_cli-2.1.10}/trustgraph/cli/delete_mcp_tool.py +0 -0
  17. {trustgraph_cli-2.1.9 → trustgraph_cli-2.1.10}/trustgraph/cli/delete_tool.py +0 -0
  18. {trustgraph_cli-2.1.9 → trustgraph_cli-2.1.10}/trustgraph/cli/dump_msgpack.py +0 -0
  19. {trustgraph_cli-2.1.9 → trustgraph_cli-2.1.10}/trustgraph/cli/dump_queues.py +0 -0
  20. {trustgraph_cli-2.1.9 → trustgraph_cli-2.1.10}/trustgraph/cli/get_config_item.py +0 -0
  21. {trustgraph_cli-2.1.9 → trustgraph_cli-2.1.10}/trustgraph/cli/get_document_content.py +0 -0
  22. {trustgraph_cli-2.1.9 → trustgraph_cli-2.1.10}/trustgraph/cli/get_flow_blueprint.py +0 -0
  23. {trustgraph_cli-2.1.9 → trustgraph_cli-2.1.10}/trustgraph/cli/get_kg_core.py +0 -0
  24. {trustgraph_cli-2.1.9 → trustgraph_cli-2.1.10}/trustgraph/cli/graph_to_turtle.py +0 -0
  25. {trustgraph_cli-2.1.9 → trustgraph_cli-2.1.10}/trustgraph/cli/init_pulsar_manager.py +0 -0
  26. {trustgraph_cli-2.1.9 → trustgraph_cli-2.1.10}/trustgraph/cli/init_trustgraph.py +0 -0
  27. {trustgraph_cli-2.1.9 → trustgraph_cli-2.1.10}/trustgraph/cli/invoke_agent.py +0 -0
  28. {trustgraph_cli-2.1.9 → trustgraph_cli-2.1.10}/trustgraph/cli/invoke_document_embeddings.py +0 -0
  29. {trustgraph_cli-2.1.9 → trustgraph_cli-2.1.10}/trustgraph/cli/invoke_document_rag.py +0 -0
  30. {trustgraph_cli-2.1.9 → trustgraph_cli-2.1.10}/trustgraph/cli/invoke_embeddings.py +0 -0
  31. {trustgraph_cli-2.1.9 → trustgraph_cli-2.1.10}/trustgraph/cli/invoke_graph_embeddings.py +0 -0
  32. {trustgraph_cli-2.1.9 → trustgraph_cli-2.1.10}/trustgraph/cli/invoke_llm.py +0 -0
  33. {trustgraph_cli-2.1.9 → trustgraph_cli-2.1.10}/trustgraph/cli/invoke_mcp_tool.py +0 -0
  34. {trustgraph_cli-2.1.9 → trustgraph_cli-2.1.10}/trustgraph/cli/invoke_nlp_query.py +0 -0
  35. {trustgraph_cli-2.1.9 → trustgraph_cli-2.1.10}/trustgraph/cli/invoke_prompt.py +0 -0
  36. {trustgraph_cli-2.1.9 → trustgraph_cli-2.1.10}/trustgraph/cli/invoke_row_embeddings.py +0 -0
  37. {trustgraph_cli-2.1.9 → trustgraph_cli-2.1.10}/trustgraph/cli/invoke_rows_query.py +0 -0
  38. {trustgraph_cli-2.1.9 → trustgraph_cli-2.1.10}/trustgraph/cli/invoke_structured_query.py +0 -0
  39. {trustgraph_cli-2.1.9 → trustgraph_cli-2.1.10}/trustgraph/cli/list_collections.py +0 -0
  40. {trustgraph_cli-2.1.9 → trustgraph_cli-2.1.10}/trustgraph/cli/list_config_items.py +0 -0
  41. {trustgraph_cli-2.1.9 → trustgraph_cli-2.1.10}/trustgraph/cli/load_doc_embeds.py +0 -0
  42. {trustgraph_cli-2.1.9 → trustgraph_cli-2.1.10}/trustgraph/cli/load_kg_core.py +0 -0
  43. {trustgraph_cli-2.1.9 → trustgraph_cli-2.1.10}/trustgraph/cli/load_knowledge.py +0 -0
  44. {trustgraph_cli-2.1.9 → trustgraph_cli-2.1.10}/trustgraph/cli/load_sample_documents.py +0 -0
  45. {trustgraph_cli-2.1.9 → trustgraph_cli-2.1.10}/trustgraph/cli/load_structured_data.py +0 -0
  46. {trustgraph_cli-2.1.9 → trustgraph_cli-2.1.10}/trustgraph/cli/load_turtle.py +0 -0
  47. {trustgraph_cli-2.1.9 → trustgraph_cli-2.1.10}/trustgraph/cli/put_config_item.py +0 -0
  48. {trustgraph_cli-2.1.9 → trustgraph_cli-2.1.10}/trustgraph/cli/put_flow_blueprint.py +0 -0
  49. {trustgraph_cli-2.1.9 → trustgraph_cli-2.1.10}/trustgraph/cli/put_kg_core.py +0 -0
  50. {trustgraph_cli-2.1.9 → trustgraph_cli-2.1.10}/trustgraph/cli/remove_library_document.py +0 -0
  51. {trustgraph_cli-2.1.9 → trustgraph_cli-2.1.10}/trustgraph/cli/save_doc_embeds.py +0 -0
  52. {trustgraph_cli-2.1.9 → trustgraph_cli-2.1.10}/trustgraph/cli/set_collection.py +0 -0
  53. {trustgraph_cli-2.1.9 → trustgraph_cli-2.1.10}/trustgraph/cli/set_mcp_tool.py +0 -0
  54. {trustgraph_cli-2.1.9 → trustgraph_cli-2.1.10}/trustgraph/cli/set_prompt.py +0 -0
  55. {trustgraph_cli-2.1.9 → trustgraph_cli-2.1.10}/trustgraph/cli/set_token_costs.py +0 -0
  56. {trustgraph_cli-2.1.9 → trustgraph_cli-2.1.10}/trustgraph/cli/set_tool.py +0 -0
  57. {trustgraph_cli-2.1.9 → trustgraph_cli-2.1.10}/trustgraph/cli/show_config.py +0 -0
  58. {trustgraph_cli-2.1.9 → trustgraph_cli-2.1.10}/trustgraph/cli/show_flow_blueprints.py +0 -0
  59. {trustgraph_cli-2.1.9 → trustgraph_cli-2.1.10}/trustgraph/cli/show_flow_state.py +0 -0
  60. {trustgraph_cli-2.1.9 → trustgraph_cli-2.1.10}/trustgraph/cli/show_flows.py +0 -0
  61. {trustgraph_cli-2.1.9 → trustgraph_cli-2.1.10}/trustgraph/cli/show_graph.py +0 -0
  62. {trustgraph_cli-2.1.9 → trustgraph_cli-2.1.10}/trustgraph/cli/show_kg_cores.py +0 -0
  63. {trustgraph_cli-2.1.9 → trustgraph_cli-2.1.10}/trustgraph/cli/show_library_documents.py +0 -0
  64. {trustgraph_cli-2.1.9 → trustgraph_cli-2.1.10}/trustgraph/cli/show_library_processing.py +0 -0
  65. {trustgraph_cli-2.1.9 → trustgraph_cli-2.1.10}/trustgraph/cli/show_mcp_tools.py +0 -0
  66. {trustgraph_cli-2.1.9 → trustgraph_cli-2.1.10}/trustgraph/cli/show_parameter_types.py +0 -0
  67. {trustgraph_cli-2.1.9 → trustgraph_cli-2.1.10}/trustgraph/cli/show_processor_state.py +0 -0
  68. {trustgraph_cli-2.1.9 → trustgraph_cli-2.1.10}/trustgraph/cli/show_prompts.py +0 -0
  69. {trustgraph_cli-2.1.9 → trustgraph_cli-2.1.10}/trustgraph/cli/show_token_costs.py +0 -0
  70. {trustgraph_cli-2.1.9 → trustgraph_cli-2.1.10}/trustgraph/cli/show_token_rate.py +0 -0
  71. {trustgraph_cli-2.1.9 → trustgraph_cli-2.1.10}/trustgraph/cli/show_tools.py +0 -0
  72. {trustgraph_cli-2.1.9 → trustgraph_cli-2.1.10}/trustgraph/cli/start_flow.py +0 -0
  73. {trustgraph_cli-2.1.9 → trustgraph_cli-2.1.10}/trustgraph/cli/start_library_processing.py +0 -0
  74. {trustgraph_cli-2.1.9 → trustgraph_cli-2.1.10}/trustgraph/cli/stop_flow.py +0 -0
  75. {trustgraph_cli-2.1.9 → trustgraph_cli-2.1.10}/trustgraph/cli/stop_library_processing.py +0 -0
  76. {trustgraph_cli-2.1.9 → trustgraph_cli-2.1.10}/trustgraph/cli/unload_kg_core.py +0 -0
  77. {trustgraph_cli-2.1.9 → trustgraph_cli-2.1.10}/trustgraph/cli/verify_system_status.py +0 -0
  78. {trustgraph_cli-2.1.9 → trustgraph_cli-2.1.10}/trustgraph_cli.egg-info/SOURCES.txt +0 -0
  79. {trustgraph_cli-2.1.9 → trustgraph_cli-2.1.10}/trustgraph_cli.egg-info/dependency_links.txt +0 -0
  80. {trustgraph_cli-2.1.9 → trustgraph_cli-2.1.10}/trustgraph_cli.egg-info/entry_points.txt +0 -0
  81. {trustgraph_cli-2.1.9 → trustgraph_cli-2.1.10}/trustgraph_cli.egg-info/requires.txt +0 -0
  82. {trustgraph_cli-2.1.9 → trustgraph_cli-2.1.10}/trustgraph_cli.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: trustgraph-cli
3
- Version: 2.1.9
3
+ Version: 2.1.10
4
4
  Summary: TrustGraph provides a means to run a pipeline of flexible AI processing components in a flexible means to achieve a processing pipeline.
5
5
  Author-email: "trustgraph.ai" <security@trustgraph.ai>
6
6
  Project-URL: Homepage, https://github.com/trustgraph-ai/trustgraph
@@ -0,0 +1,780 @@
1
+ """
2
+ Uses the GraphRAG service to answer a question
3
+ """
4
+
5
+ import argparse
6
+ import json
7
+ import os
8
+ import sys
9
+ import websockets
10
+ import asyncio
11
+ from trustgraph.api import Api
12
+
13
+ default_url = os.getenv("TRUSTGRAPH_URL", 'http://localhost:8088/')
14
+ default_token = os.getenv("TRUSTGRAPH_TOKEN", None)
15
+ default_user = 'trustgraph'
16
+ default_collection = 'default'
17
+ default_entity_limit = 50
18
+ default_triple_limit = 30
19
+ default_max_subgraph_size = 150
20
+ default_max_path_length = 2
21
+
22
+ # Provenance predicates
23
+ TG = "https://trustgraph.ai/ns/"
24
+ TG_QUERY = TG + "query"
25
+ TG_EDGE_COUNT = TG + "edgeCount"
26
+ TG_SELECTED_EDGE = TG + "selectedEdge"
27
+ TG_EDGE = TG + "edge"
28
+ TG_REASONING = TG + "reasoning"
29
+ TG_CONTENT = TG + "content"
30
+ TG_REIFIES = TG + "reifies"
31
+ PROV = "http://www.w3.org/ns/prov#"
32
+ PROV_STARTED_AT_TIME = PROV + "startedAtTime"
33
+ PROV_WAS_DERIVED_FROM = PROV + "wasDerivedFrom"
34
+ RDFS_LABEL = "http://www.w3.org/2000/01/rdf-schema#label"
35
+
36
+
37
+ def _get_event_type(prov_id):
38
+ """Extract event type from provenance_id"""
39
+ if "session" in prov_id:
40
+ return "session"
41
+ elif "retrieval" in prov_id:
42
+ return "retrieval"
43
+ elif "selection" in prov_id:
44
+ return "selection"
45
+ elif "answer" in prov_id:
46
+ return "answer"
47
+ return "provenance"
48
+
49
+
50
+ def _format_provenance_details(event_type, triples):
51
+ """Format provenance details based on event type and triples"""
52
+ lines = []
53
+
54
+ if event_type == "session":
55
+ # Show query and timestamp
56
+ for s, p, o in triples:
57
+ if p == TG_QUERY:
58
+ lines.append(f" Query: {o}")
59
+ elif p == PROV_STARTED_AT_TIME:
60
+ lines.append(f" Time: {o}")
61
+
62
+ elif event_type == "retrieval":
63
+ # Show edge count
64
+ for s, p, o in triples:
65
+ if p == TG_EDGE_COUNT:
66
+ lines.append(f" Edges retrieved: {o}")
67
+
68
+ elif event_type == "selection":
69
+ # For selection, just count edge selection URIs
70
+ # The actual edge details are fetched separately via edge_selections parameter
71
+ edge_sel_uris = []
72
+ for s, p, o in triples:
73
+ if p == TG_SELECTED_EDGE:
74
+ edge_sel_uris.append(o)
75
+ if edge_sel_uris:
76
+ lines.append(f" Selected {len(edge_sel_uris)} edge(s)")
77
+
78
+ elif event_type == "answer":
79
+ # Show content length (not full content - it's already streamed)
80
+ for s, p, o in triples:
81
+ if p == TG_CONTENT:
82
+ lines.append(f" Answer length: {len(o)} chars")
83
+
84
+ return lines
85
+
86
+
87
+ async def _query_triples_once(ws_url, flow_id, prov_id, user, collection, debug=False):
88
+ """Query triples for a provenance node (single attempt)"""
89
+ request = {
90
+ "id": "triples-request",
91
+ "service": "triples",
92
+ "flow": flow_id,
93
+ "request": {
94
+ "s": {"t": "i", "i": prov_id},
95
+ "user": user,
96
+ "collection": collection,
97
+ "limit": 100
98
+ }
99
+ }
100
+
101
+ if debug:
102
+ print(f" [debug] querying triples for s={prov_id}", file=sys.stderr)
103
+
104
+ triples = []
105
+ try:
106
+ async with websockets.connect(ws_url, ping_interval=20, ping_timeout=30) as websocket:
107
+ await websocket.send(json.dumps(request))
108
+
109
+ async for raw_message in websocket:
110
+ response = json.loads(raw_message)
111
+
112
+ if debug:
113
+ print(f" [debug] response: {json.dumps(response)[:200]}", file=sys.stderr)
114
+
115
+ if response.get("id") != "triples-request":
116
+ continue
117
+
118
+ if "error" in response:
119
+ if debug:
120
+ print(f" [debug] error: {response['error']}", file=sys.stderr)
121
+ break
122
+
123
+ if "response" in response:
124
+ resp = response["response"]
125
+ # Handle triples response
126
+ # Response format: {"response": [triples...]}
127
+ # Each triple uses compact keys: "i" for iri, "v" for value, "t" for type
128
+ triple_list = resp.get("response", [])
129
+ for t in triple_list:
130
+ s = t.get("s", {}).get("i", t.get("s", {}).get("v", ""))
131
+ p = t.get("p", {}).get("i", t.get("p", {}).get("v", ""))
132
+ # Handle quoted triples (type "t") and regular values
133
+ o_term = t.get("o", {})
134
+ if o_term.get("t") == "t":
135
+ # Quoted triple - extract s, p, o from nested structure
136
+ tr = o_term.get("tr", {})
137
+ o = {
138
+ "s": tr.get("s", {}).get("i", ""),
139
+ "p": tr.get("p", {}).get("i", ""),
140
+ "o": tr.get("o", {}).get("i", tr.get("o", {}).get("v", "")),
141
+ }
142
+ else:
143
+ o = o_term.get("i", o_term.get("v", ""))
144
+ triples.append((s, p, o))
145
+
146
+ if resp.get("complete") or response.get("complete"):
147
+ break
148
+ except Exception as e:
149
+ if debug:
150
+ print(f" [debug] exception: {e}", file=sys.stderr)
151
+
152
+ if debug:
153
+ print(f" [debug] got {len(triples)} triples", file=sys.stderr)
154
+
155
+ return triples
156
+
157
+
158
+ async def _query_triples(ws_url, flow_id, prov_id, user, collection, max_retries=5, retry_delay=0.2, debug=False):
159
+ """Query triples for a provenance node with retries for race condition"""
160
+ for attempt in range(max_retries):
161
+ triples = await _query_triples_once(ws_url, flow_id, prov_id, user, collection, debug)
162
+ if triples:
163
+ return triples
164
+ # Wait before retry if empty (triples may not be stored yet)
165
+ if attempt < max_retries - 1:
166
+ if debug:
167
+ print(f" [debug] retry {attempt + 1}/{max_retries}...", file=sys.stderr)
168
+ await asyncio.sleep(retry_delay)
169
+ return []
170
+
171
+
172
+ async def _query_edge_provenance(ws_url, flow_id, edge_s, edge_p, edge_o, user, collection, debug=False):
173
+ """
174
+ Query for provenance of an edge (s, p, o) in the knowledge graph.
175
+
176
+ Finds statements that reify the edge via tg:reifies, then follows
177
+ prov:wasDerivedFrom to find source documents.
178
+
179
+ Returns list of source URIs (chunks, pages, documents).
180
+ """
181
+ # Query for statements that reify this edge: ?stmt tg:reifies <<s p o>>
182
+ request = {
183
+ "id": "edge-prov-request",
184
+ "service": "triples",
185
+ "flow": flow_id,
186
+ "request": {
187
+ "p": {"t": "i", "i": TG_REIFIES},
188
+ "o": {
189
+ "t": "t", # Quoted triple type
190
+ "tr": {
191
+ "s": {"t": "i", "i": edge_s},
192
+ "p": {"t": "i", "i": edge_p},
193
+ "o": {"t": "i", "i": edge_o} if edge_o.startswith("http") or edge_o.startswith("urn:") else {"t": "l", "v": edge_o},
194
+ }
195
+ },
196
+ "user": user,
197
+ "collection": collection,
198
+ "limit": 10
199
+ }
200
+ }
201
+
202
+ if debug:
203
+ print(f" [debug] querying edge provenance for ({edge_s}, {edge_p}, {edge_o})", file=sys.stderr)
204
+
205
+ stmt_uris = []
206
+ try:
207
+ async with websockets.connect(ws_url, ping_interval=20, ping_timeout=30) as websocket:
208
+ await websocket.send(json.dumps(request))
209
+
210
+ async for raw_message in websocket:
211
+ response = json.loads(raw_message)
212
+
213
+ if response.get("id") != "edge-prov-request":
214
+ continue
215
+
216
+ if "error" in response:
217
+ if debug:
218
+ print(f" [debug] error: {response['error']}", file=sys.stderr)
219
+ break
220
+
221
+ if "response" in response:
222
+ resp = response["response"]
223
+ triple_list = resp.get("response", [])
224
+ for t in triple_list:
225
+ s = t.get("s", {}).get("i", "")
226
+ if s:
227
+ stmt_uris.append(s)
228
+
229
+ if resp.get("complete") or response.get("complete"):
230
+ break
231
+ except Exception as e:
232
+ if debug:
233
+ print(f" [debug] exception querying edge provenance: {e}", file=sys.stderr)
234
+
235
+ if debug:
236
+ print(f" [debug] found {len(stmt_uris)} reifying statements", file=sys.stderr)
237
+
238
+ # For each statement, query wasDerivedFrom to find sources
239
+ sources = []
240
+ for stmt_uri in stmt_uris:
241
+ # Query: stmt_uri prov:wasDerivedFrom ?source
242
+ request = {
243
+ "id": "derived-from-request",
244
+ "service": "triples",
245
+ "flow": flow_id,
246
+ "request": {
247
+ "s": {"t": "i", "i": stmt_uri},
248
+ "p": {"t": "i", "i": PROV_WAS_DERIVED_FROM},
249
+ "user": user,
250
+ "collection": collection,
251
+ "limit": 10
252
+ }
253
+ }
254
+
255
+ try:
256
+ async with websockets.connect(ws_url, ping_interval=20, ping_timeout=30) as websocket:
257
+ await websocket.send(json.dumps(request))
258
+
259
+ async for raw_message in websocket:
260
+ response = json.loads(raw_message)
261
+
262
+ if response.get("id") != "derived-from-request":
263
+ continue
264
+
265
+ if "error" in response:
266
+ break
267
+
268
+ if "response" in response:
269
+ resp = response["response"]
270
+ triple_list = resp.get("response", [])
271
+ for t in triple_list:
272
+ o = t.get("o", {}).get("i", "")
273
+ if o:
274
+ sources.append(o)
275
+
276
+ if resp.get("complete") or response.get("complete"):
277
+ break
278
+ except Exception as e:
279
+ if debug:
280
+ print(f" [debug] exception querying wasDerivedFrom: {e}", file=sys.stderr)
281
+
282
+ if debug:
283
+ print(f" [debug] found {len(sources)} source(s): {sources}", file=sys.stderr)
284
+
285
+ return sources
286
+
287
+
288
+ async def _query_derived_from(ws_url, flow_id, uri, user, collection, debug=False):
289
+ """Query for the prov:wasDerivedFrom parent of a URI. Returns None if no parent."""
290
+ request = {
291
+ "id": "parent-request",
292
+ "service": "triples",
293
+ "flow": flow_id,
294
+ "request": {
295
+ "s": {"t": "i", "i": uri},
296
+ "p": {"t": "i", "i": PROV_WAS_DERIVED_FROM},
297
+ "user": user,
298
+ "collection": collection,
299
+ "limit": 1
300
+ }
301
+ }
302
+
303
+ try:
304
+ async with websockets.connect(ws_url, ping_interval=20, ping_timeout=30) as websocket:
305
+ await websocket.send(json.dumps(request))
306
+
307
+ async for raw_message in websocket:
308
+ response = json.loads(raw_message)
309
+
310
+ if response.get("id") != "parent-request":
311
+ continue
312
+
313
+ if "error" in response:
314
+ break
315
+
316
+ if "response" in response:
317
+ resp = response["response"]
318
+ triple_list = resp.get("response", [])
319
+ if triple_list:
320
+ return triple_list[0].get("o", {}).get("i", None)
321
+
322
+ if resp.get("complete") or response.get("complete"):
323
+ break
324
+ except Exception as e:
325
+ if debug:
326
+ print(f" [debug] exception querying parent: {e}", file=sys.stderr)
327
+
328
+ return None
329
+
330
+
331
+ async def _trace_provenance_chain(ws_url, flow_id, source_uri, user, collection, label_cache, debug=False):
332
+ """
333
+ Trace the full provenance chain from a source URI up to the root document.
334
+ Returns a list of (uri, label) tuples from leaf to root.
335
+ """
336
+ chain = []
337
+ current = source_uri
338
+ max_depth = 10 # Prevent infinite loops
339
+
340
+ for _ in range(max_depth):
341
+ if not current:
342
+ break
343
+
344
+ # Get label for current entity
345
+ label = await _query_label(ws_url, flow_id, current, user, collection, label_cache, debug)
346
+ chain.append((current, label))
347
+
348
+ # Get parent
349
+ parent = await _query_derived_from(ws_url, flow_id, current, user, collection, debug)
350
+ if not parent or parent == current:
351
+ break
352
+ current = parent
353
+
354
+ return chain
355
+
356
+
357
+ def _format_provenance_chain(chain):
358
+ """
359
+ Format a provenance chain as a human-readable string.
360
+ Chain is [(uri, label), ...] from leaf to root.
361
+ """
362
+ if not chain:
363
+ return ""
364
+
365
+ # Show labels, from leaf to root
366
+ labels = [label for uri, label in chain]
367
+ return " → ".join(labels)
368
+
369
+
370
+ def _is_iri(value):
371
+ """Check if a value looks like an IRI."""
372
+ if not isinstance(value, str):
373
+ return False
374
+ return value.startswith("http://") or value.startswith("https://") or value.startswith("urn:")
375
+
376
+
377
+ async def _query_label(ws_url, flow_id, iri, user, collection, label_cache, debug=False):
378
+ """
379
+ Query for the rdfs:label of an IRI.
380
+ Uses label_cache to avoid repeated queries.
381
+ Returns the label if found, otherwise returns the IRI.
382
+ """
383
+ if not _is_iri(iri):
384
+ return iri
385
+
386
+ # Check cache first
387
+ if iri in label_cache:
388
+ return label_cache[iri]
389
+
390
+ request = {
391
+ "id": "label-request",
392
+ "service": "triples",
393
+ "flow": flow_id,
394
+ "request": {
395
+ "s": {"t": "i", "i": iri},
396
+ "p": {"t": "i", "i": RDFS_LABEL},
397
+ "user": user,
398
+ "collection": collection,
399
+ "limit": 1
400
+ }
401
+ }
402
+
403
+ label = iri # Default to IRI if no label found
404
+ try:
405
+ async with websockets.connect(ws_url, ping_interval=20, ping_timeout=30) as websocket:
406
+ await websocket.send(json.dumps(request))
407
+
408
+ async for raw_message in websocket:
409
+ response = json.loads(raw_message)
410
+
411
+ if response.get("id") != "label-request":
412
+ continue
413
+
414
+ if "error" in response:
415
+ break
416
+
417
+ if "response" in response:
418
+ resp = response["response"]
419
+ triple_list = resp.get("response", [])
420
+ if triple_list:
421
+ # Get the label value
422
+ o = triple_list[0].get("o", {})
423
+ label = o.get("v", o.get("i", iri))
424
+
425
+ if resp.get("complete") or response.get("complete"):
426
+ break
427
+ except Exception as e:
428
+ if debug:
429
+ print(f" [debug] exception querying label for {iri}: {e}", file=sys.stderr)
430
+
431
+ # Cache the result
432
+ label_cache[iri] = label
433
+ return label
434
+
435
+
436
+ async def _resolve_edge_labels(ws_url, flow_id, edge_triple, user, collection, label_cache, debug=False):
437
+ """
438
+ Resolve labels for all IRI components of an edge triple.
439
+ Returns (s_label, p_label, o_label).
440
+ """
441
+ s = edge_triple.get("s", "?")
442
+ p = edge_triple.get("p", "?")
443
+ o = edge_triple.get("o", "?")
444
+
445
+ s_label = await _query_label(ws_url, flow_id, s, user, collection, label_cache, debug)
446
+ p_label = await _query_label(ws_url, flow_id, p, user, collection, label_cache, debug)
447
+ o_label = await _query_label(ws_url, flow_id, o, user, collection, label_cache, debug)
448
+
449
+ return s_label, p_label, o_label
450
+
451
+
452
+ async def _question_explainable(
453
+ url, flow_id, question, user, collection, entity_limit, triple_limit,
454
+ max_subgraph_size, max_path_length, token=None, debug=False
455
+ ):
456
+ """Execute graph RAG with explainability - shows provenance events with details"""
457
+ # Convert HTTP URL to WebSocket URL
458
+ if url.startswith("http://"):
459
+ ws_url = url.replace("http://", "ws://", 1)
460
+ elif url.startswith("https://"):
461
+ ws_url = url.replace("https://", "wss://", 1)
462
+ else:
463
+ ws_url = f"ws://{url}"
464
+
465
+ ws_url = f"{ws_url.rstrip('/')}/api/v1/socket"
466
+ if token:
467
+ ws_url = f"{ws_url}?token={token}"
468
+
469
+ # Cache for label lookups to avoid repeated queries
470
+ label_cache = {}
471
+
472
+ request = {
473
+ "id": "cli-request",
474
+ "service": "graph-rag",
475
+ "flow": flow_id,
476
+ "request": {
477
+ "query": question,
478
+ "user": user,
479
+ "collection": collection,
480
+ "entity-limit": entity_limit,
481
+ "triple-limit": triple_limit,
482
+ "max-subgraph-size": max_subgraph_size,
483
+ "max-path-length": max_path_length,
484
+ "streaming": True
485
+ }
486
+ }
487
+
488
+ async with websockets.connect(ws_url, ping_interval=20, ping_timeout=300) as websocket:
489
+ await websocket.send(json.dumps(request))
490
+
491
+ async for raw_message in websocket:
492
+ response = json.loads(raw_message)
493
+
494
+ if response.get("id") != "cli-request":
495
+ continue
496
+
497
+ if "error" in response:
498
+ print(f"\nError: {response['error']}", file=sys.stderr)
499
+ break
500
+
501
+ if "response" in response:
502
+ resp = response["response"]
503
+
504
+ # Check for errors in response
505
+ if "error" in resp and resp["error"]:
506
+ err = resp["error"]
507
+ print(f"\nError: {err.get('message', 'Unknown error')}", file=sys.stderr)
508
+ break
509
+
510
+ message_type = resp.get("message_type", "")
511
+
512
+ if debug:
513
+ print(f" [debug] message_type={message_type}, keys={list(resp.keys())}", file=sys.stderr)
514
+
515
+ if message_type == "explain":
516
+ # Display explain event with details
517
+ explain_id = resp.get("explain_id", "")
518
+ explain_collection = resp.get("explain_collection", "explainability")
519
+ if explain_id:
520
+ event_type = _get_event_type(explain_id)
521
+ print(f"\n [{event_type}] {explain_id}", file=sys.stderr)
522
+
523
+ # Query triples for this explain node (using explain collection from event)
524
+ triples = await _query_triples(
525
+ ws_url, flow_id, explain_id, user, explain_collection, debug=debug
526
+ )
527
+
528
+ # Format and display details
529
+ details = _format_provenance_details(event_type, triples)
530
+ for line in details:
531
+ print(line, file=sys.stderr)
532
+
533
+ # For selection events, query each edge selection for details
534
+ if event_type == "selection":
535
+ for s, p, o in triples:
536
+ if debug:
537
+ print(f" [debug] triple: p={p}, o={o}, o_type={type(o).__name__}", file=sys.stderr)
538
+ if p == TG_SELECTED_EDGE and isinstance(o, str):
539
+ if debug:
540
+ print(f" [debug] querying edge selection: {o}", file=sys.stderr)
541
+ # Query the edge selection entity (using explain collection from event)
542
+ edge_triples = await _query_triples(
543
+ ws_url, flow_id, o, user, explain_collection, debug=debug
544
+ )
545
+ if debug:
546
+ print(f" [debug] got {len(edge_triples)} edge triples", file=sys.stderr)
547
+ # Extract edge and reasoning
548
+ edge_triple = None # Store the actual triple for provenance lookup
549
+ reasoning = None
550
+ for es, ep, eo in edge_triples:
551
+ if debug:
552
+ print(f" [debug] edge triple: ep={ep}, eo={eo}", file=sys.stderr)
553
+ if ep == TG_EDGE and isinstance(eo, dict):
554
+ # eo is a quoted triple dict
555
+ edge_triple = eo
556
+ elif ep == TG_REASONING:
557
+ reasoning = eo
558
+ if edge_triple:
559
+ # Resolve labels for edge components
560
+ s_label, p_label, o_label = await _resolve_edge_labels(
561
+ ws_url, flow_id, edge_triple, user, collection,
562
+ label_cache, debug=debug
563
+ )
564
+ print(f" Edge: ({s_label}, {p_label}, {o_label})", file=sys.stderr)
565
+ if reasoning:
566
+ r_short = reasoning[:100] + "..." if len(reasoning) > 100 else reasoning
567
+ print(f" Reason: {r_short}", file=sys.stderr)
568
+
569
+ # Trace edge provenance in the user's collection (not explainability)
570
+ if edge_triple:
571
+ sources = await _query_edge_provenance(
572
+ ws_url, flow_id,
573
+ edge_triple.get("s", ""),
574
+ edge_triple.get("p", ""),
575
+ edge_triple.get("o", ""),
576
+ user, collection, # Use the query collection, not explainability
577
+ debug=debug
578
+ )
579
+ if sources:
580
+ for src in sources:
581
+ # Trace full chain from source to root document
582
+ chain = await _trace_provenance_chain(
583
+ ws_url, flow_id, src, user, collection,
584
+ label_cache, debug=debug
585
+ )
586
+ chain_str = _format_provenance_chain(chain)
587
+ print(f" Source: {chain_str}", file=sys.stderr)
588
+
589
+ elif message_type == "chunk" or not message_type:
590
+ # Display response chunk
591
+ chunk = resp.get("response", "")
592
+ if chunk:
593
+ print(chunk, end="", flush=True)
594
+
595
+ # Check if session is complete
596
+ if resp.get("end_of_session"):
597
+ break
598
+
599
+ print() # Final newline
600
+
601
+
602
+ def question(
603
+ url, flow_id, question, user, collection, entity_limit, triple_limit,
604
+ max_subgraph_size, max_path_length, streaming=True, token=None,
605
+ explainable=False, debug=False
606
+ ):
607
+
608
+ # Explainable mode uses direct websocket to capture provenance events
609
+ if explainable:
610
+ asyncio.run(_question_explainable(
611
+ url=url,
612
+ flow_id=flow_id,
613
+ question=question,
614
+ user=user,
615
+ collection=collection,
616
+ entity_limit=entity_limit,
617
+ triple_limit=triple_limit,
618
+ max_subgraph_size=max_subgraph_size,
619
+ max_path_length=max_path_length,
620
+ token=token,
621
+ debug=debug
622
+ ))
623
+ return
624
+
625
+ # Create API client
626
+ api = Api(url=url, token=token)
627
+
628
+ if streaming:
629
+ # Use socket client for streaming
630
+ socket = api.socket()
631
+ flow = socket.flow(flow_id)
632
+
633
+ try:
634
+ response = flow.graph_rag(
635
+ query=question,
636
+ user=user,
637
+ collection=collection,
638
+ entity_limit=entity_limit,
639
+ triple_limit=triple_limit,
640
+ max_subgraph_size=max_subgraph_size,
641
+ max_path_length=max_path_length,
642
+ streaming=True
643
+ )
644
+
645
+ # Stream output
646
+ for chunk in response:
647
+ print(chunk, end="", flush=True)
648
+ print() # Final newline
649
+
650
+ finally:
651
+ socket.close()
652
+ else:
653
+ # Use REST API for non-streaming
654
+ flow = api.flow().id(flow_id)
655
+ resp = flow.graph_rag(
656
+ query=question,
657
+ user=user,
658
+ collection=collection,
659
+ entity_limit=entity_limit,
660
+ triple_limit=triple_limit,
661
+ max_subgraph_size=max_subgraph_size,
662
+ max_path_length=max_path_length
663
+ )
664
+ print(resp)
665
+
666
+ def main():
667
+
668
+ parser = argparse.ArgumentParser(
669
+ prog='tg-invoke-graph-rag',
670
+ description=__doc__,
671
+ )
672
+
673
+ parser.add_argument(
674
+ '-u', '--url',
675
+ default=default_url,
676
+ help=f'API URL (default: {default_url})',
677
+ )
678
+
679
+ parser.add_argument(
680
+ '-t', '--token',
681
+ default=default_token,
682
+ help='Authentication token (default: $TRUSTGRAPH_TOKEN)',
683
+ )
684
+
685
+ parser.add_argument(
686
+ '-f', '--flow-id',
687
+ default="default",
688
+ help=f'Flow ID (default: default)'
689
+ )
690
+
691
+ parser.add_argument(
692
+ '-q', '--question',
693
+ required=True,
694
+ help=f'Question to answer',
695
+ )
696
+
697
+ parser.add_argument(
698
+ '-U', '--user',
699
+ default=default_user,
700
+ help=f'User ID (default: {default_user})'
701
+ )
702
+
703
+ parser.add_argument(
704
+ '-C', '--collection',
705
+ default=default_collection,
706
+ help=f'Collection ID (default: {default_collection})'
707
+ )
708
+
709
+ parser.add_argument(
710
+ '-e', '--entity-limit',
711
+ type=int,
712
+ default=default_entity_limit,
713
+ help=f'Entity limit (default: {default_entity_limit})'
714
+ )
715
+
716
+ parser.add_argument(
717
+ '--triple-limit',
718
+ type=int,
719
+ default=default_triple_limit,
720
+ help=f'Triple limit (default: {default_triple_limit})'
721
+ )
722
+
723
+ parser.add_argument(
724
+ '-s', '--max-subgraph-size',
725
+ type=int,
726
+ default=default_max_subgraph_size,
727
+ help=f'Max subgraph size (default: {default_max_subgraph_size})'
728
+ )
729
+
730
+ parser.add_argument(
731
+ '-p', '--max-path-length',
732
+ type=int,
733
+ default=default_max_path_length,
734
+ help=f'Max path length (default: {default_max_path_length})'
735
+ )
736
+
737
+ parser.add_argument(
738
+ '--no-streaming',
739
+ action='store_true',
740
+ help='Disable streaming (use non-streaming mode)'
741
+ )
742
+
743
+ parser.add_argument(
744
+ '-x', '--explainable',
745
+ action='store_true',
746
+ help='Show provenance events for explainability (implies streaming)'
747
+ )
748
+
749
+ parser.add_argument(
750
+ '--debug',
751
+ action='store_true',
752
+ help='Show debug output for troubleshooting'
753
+ )
754
+
755
+ args = parser.parse_args()
756
+
757
+ try:
758
+
759
+ question(
760
+ url=args.url,
761
+ flow_id=args.flow_id,
762
+ question=args.question,
763
+ user=args.user,
764
+ collection=args.collection,
765
+ entity_limit=args.entity_limit,
766
+ triple_limit=args.triple_limit,
767
+ max_subgraph_size=args.max_subgraph_size,
768
+ max_path_length=args.max_path_length,
769
+ streaming=not args.no_streaming,
770
+ token=args.token,
771
+ explainable=args.explainable,
772
+ debug=args.debug,
773
+ )
774
+
775
+ except Exception as e:
776
+
777
+ print("Exception:", e, flush=True)
778
+
779
+ if __name__ == "__main__":
780
+ main()
@@ -0,0 +1 @@
1
+ __version__ = "2.1.10"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: trustgraph-cli
3
- Version: 2.1.9
3
+ Version: 2.1.10
4
4
  Summary: TrustGraph provides a means to run a pipeline of flexible AI processing components in a flexible means to achieve a processing pipeline.
5
5
  Author-email: "trustgraph.ai" <security@trustgraph.ai>
6
6
  Project-URL: Homepage, https://github.com/trustgraph-ai/trustgraph
@@ -1,164 +0,0 @@
1
- """
2
- Uses the GraphRAG service to answer a question
3
- """
4
-
5
- import argparse
6
- import os
7
- from trustgraph.api import Api
8
-
9
- default_url = os.getenv("TRUSTGRAPH_URL", 'http://localhost:8088/')
10
- default_token = os.getenv("TRUSTGRAPH_TOKEN", None)
11
- default_user = 'trustgraph'
12
- default_collection = 'default'
13
- default_entity_limit = 50
14
- default_triple_limit = 30
15
- default_max_subgraph_size = 150
16
- default_max_path_length = 2
17
-
18
- def question(
19
- url, flow_id, question, user, collection, entity_limit, triple_limit,
20
- max_subgraph_size, max_path_length, streaming=True, token=None
21
- ):
22
-
23
- # Create API client
24
- api = Api(url=url, token=token)
25
-
26
- if streaming:
27
- # Use socket client for streaming
28
- socket = api.socket()
29
- flow = socket.flow(flow_id)
30
-
31
- try:
32
- response = flow.graph_rag(
33
- query=question,
34
- user=user,
35
- collection=collection,
36
- entity_limit=entity_limit,
37
- triple_limit=triple_limit,
38
- max_subgraph_size=max_subgraph_size,
39
- max_path_length=max_path_length,
40
- streaming=True
41
- )
42
-
43
- # Stream output
44
- for chunk in response:
45
- print(chunk, end="", flush=True)
46
- print() # Final newline
47
-
48
- finally:
49
- socket.close()
50
- else:
51
- # Use REST API for non-streaming
52
- flow = api.flow().id(flow_id)
53
- resp = flow.graph_rag(
54
- query=question,
55
- user=user,
56
- collection=collection,
57
- entity_limit=entity_limit,
58
- triple_limit=triple_limit,
59
- max_subgraph_size=max_subgraph_size,
60
- max_path_length=max_path_length
61
- )
62
- print(resp)
63
-
64
- def main():
65
-
66
- parser = argparse.ArgumentParser(
67
- prog='tg-invoke-graph-rag',
68
- description=__doc__,
69
- )
70
-
71
- parser.add_argument(
72
- '-u', '--url',
73
- default=default_url,
74
- help=f'API URL (default: {default_url})',
75
- )
76
-
77
- parser.add_argument(
78
- '-t', '--token',
79
- default=default_token,
80
- help='Authentication token (default: $TRUSTGRAPH_TOKEN)',
81
- )
82
-
83
- parser.add_argument(
84
- '-f', '--flow-id',
85
- default="default",
86
- help=f'Flow ID (default: default)'
87
- )
88
-
89
- parser.add_argument(
90
- '-q', '--question',
91
- required=True,
92
- help=f'Question to answer',
93
- )
94
-
95
- parser.add_argument(
96
- '-U', '--user',
97
- default=default_user,
98
- help=f'User ID (default: {default_user})'
99
- )
100
-
101
- parser.add_argument(
102
- '-C', '--collection',
103
- default=default_collection,
104
- help=f'Collection ID (default: {default_collection})'
105
- )
106
-
107
- parser.add_argument(
108
- '-e', '--entity-limit',
109
- type=int,
110
- default=default_entity_limit,
111
- help=f'Entity limit (default: {default_entity_limit})'
112
- )
113
-
114
- parser.add_argument(
115
- '--triple-limit',
116
- type=int,
117
- default=default_triple_limit,
118
- help=f'Triple limit (default: {default_triple_limit})'
119
- )
120
-
121
- parser.add_argument(
122
- '-s', '--max-subgraph-size',
123
- type=int,
124
- default=default_max_subgraph_size,
125
- help=f'Max subgraph size (default: {default_max_subgraph_size})'
126
- )
127
-
128
- parser.add_argument(
129
- '-p', '--max-path-length',
130
- type=int,
131
- default=default_max_path_length,
132
- help=f'Max path length (default: {default_max_path_length})'
133
- )
134
-
135
- parser.add_argument(
136
- '--no-streaming',
137
- action='store_true',
138
- help='Disable streaming (use non-streaming mode)'
139
- )
140
-
141
- args = parser.parse_args()
142
-
143
- try:
144
-
145
- question(
146
- url=args.url,
147
- flow_id=args.flow_id,
148
- question=args.question,
149
- user=args.user,
150
- collection=args.collection,
151
- entity_limit=args.entity_limit,
152
- triple_limit=args.triple_limit,
153
- max_subgraph_size=args.max_subgraph_size,
154
- max_path_length=args.max_path_length,
155
- streaming=not args.no_streaming,
156
- token=args.token,
157
- )
158
-
159
- except Exception as e:
160
-
161
- print("Exception:", e, flush=True)
162
-
163
- if __name__ == "__main__":
164
- main()
@@ -1 +0,0 @@
1
- __version__ = "2.1.9"