mirror of
https://github.com/trustgraph-ai/trustgraph.git
synced 2026-07-09 05:12:12 +02:00
Merge branch 'master' into docs
This commit is contained in:
commit
ae58fa7f98
270 changed files with 19639 additions and 4087 deletions
|
|
@ -35,6 +35,7 @@ tg-delete-kg-core = "trustgraph.cli.delete_kg_core:main"
|
|||
tg-delete-tool = "trustgraph.cli.delete_tool:main"
|
||||
tg-dump-msgpack = "trustgraph.cli.dump_msgpack:main"
|
||||
tg-dump-queues = "trustgraph.cli.dump_queues:main"
|
||||
tg-monitor-prompts = "trustgraph.cli.monitor_prompts:main"
|
||||
tg-get-flow-blueprint = "trustgraph.cli.get_flow_blueprint:main"
|
||||
tg-get-kg-core = "trustgraph.cli.get_kg_core:main"
|
||||
tg-get-document-content = "trustgraph.cli.get_document_content:main"
|
||||
|
|
@ -50,6 +51,7 @@ tg-invoke-document-embeddings = "trustgraph.cli.invoke_document_embeddings:main"
|
|||
tg-invoke-mcp-tool = "trustgraph.cli.invoke_mcp_tool:main"
|
||||
tg-invoke-nlp-query = "trustgraph.cli.invoke_nlp_query:main"
|
||||
tg-invoke-rows-query = "trustgraph.cli.invoke_rows_query:main"
|
||||
tg-invoke-sparql-query = "trustgraph.cli.invoke_sparql_query:main"
|
||||
tg-invoke-row-embeddings = "trustgraph.cli.invoke_row_embeddings:main"
|
||||
tg-invoke-prompt = "trustgraph.cli.invoke_prompt:main"
|
||||
tg-invoke-structured-query = "trustgraph.cli.invoke_structured_query:main"
|
||||
|
|
|
|||
|
|
@ -8,8 +8,6 @@ message flows, diagnosing stuck services, and understanding system behavior.
|
|||
Uses TrustGraph's Subscriber abstraction for future-proof pub/sub compatibility.
|
||||
"""
|
||||
|
||||
import pulsar
|
||||
from pulsar.schema import BytesSchema
|
||||
import sys
|
||||
import json
|
||||
import asyncio
|
||||
|
|
@ -17,45 +15,69 @@ from datetime import datetime
|
|||
import argparse
|
||||
|
||||
from trustgraph.base.subscriber import Subscriber
|
||||
from trustgraph.base.pubsub import get_pubsub
|
||||
from trustgraph.base.pubsub import get_pubsub, add_pubsub_args
|
||||
|
||||
def decode_json_strings(obj):
|
||||
"""Recursively decode JSON-encoded string values within a dict/list."""
|
||||
if isinstance(obj, dict):
|
||||
return {k: decode_json_strings(v) for k, v in obj.items()}
|
||||
if isinstance(obj, list):
|
||||
return [decode_json_strings(v) for v in obj]
|
||||
if isinstance(obj, str):
|
||||
try:
|
||||
parsed = json.loads(obj)
|
||||
if isinstance(parsed, (dict, list)):
|
||||
return decode_json_strings(parsed)
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
pass
|
||||
return obj
|
||||
|
||||
|
||||
def to_dict(value):
|
||||
"""Recursively convert a value to a JSON-serialisable structure."""
|
||||
|
||||
if value is None or isinstance(value, (bool, int, float)):
|
||||
return value
|
||||
|
||||
if isinstance(value, bytes):
|
||||
value = value.decode('utf-8')
|
||||
|
||||
if isinstance(value, str):
|
||||
try:
|
||||
return json.loads(value)
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
return value
|
||||
|
||||
if isinstance(value, dict):
|
||||
return {k: to_dict(v) for k, v in value.items()}
|
||||
|
||||
if isinstance(value, (list, tuple)):
|
||||
return [to_dict(v) for v in value]
|
||||
|
||||
# Pulsar schema objects expose fields via __dict__
|
||||
if hasattr(value, '__dict__'):
|
||||
return {
|
||||
k: to_dict(v) for k, v in value.__dict__.items()
|
||||
if not k.startswith('_')
|
||||
}
|
||||
|
||||
return str(value)
|
||||
|
||||
|
||||
def format_message(queue_name, msg):
|
||||
"""Format a message with timestamp and queue name."""
|
||||
timestamp = datetime.now().isoformat()
|
||||
|
||||
# Try to parse as JSON and pretty-print
|
||||
try:
|
||||
# Handle both Message objects and raw bytes
|
||||
if hasattr(msg, 'value'):
|
||||
# Message object with .value() method
|
||||
value = msg.value()
|
||||
else:
|
||||
# Raw bytes from schema-less subscription
|
||||
value = msg
|
||||
value = msg.value() if hasattr(msg, 'value') else msg
|
||||
parsed = to_dict(value)
|
||||
|
||||
# If it's bytes, decode it
|
||||
if isinstance(value, bytes):
|
||||
value = value.decode('utf-8')
|
||||
|
||||
# If it's a string, try to parse as JSON
|
||||
if isinstance(value, str):
|
||||
try:
|
||||
parsed = json.loads(value)
|
||||
body = json.dumps(parsed, indent=2)
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
body = value
|
||||
# Unwrap nested JSON strings (e.g. terms values)
|
||||
if isinstance(parsed, (dict, list)):
|
||||
parsed = decode_json_strings(parsed)
|
||||
body = json.dumps(parsed, indent=2, default=str)
|
||||
else:
|
||||
# Try to convert to dict for pretty printing
|
||||
try:
|
||||
# Pulsar schema objects have __dict__ or similar
|
||||
if hasattr(value, '__dict__'):
|
||||
parsed = {k: v for k, v in value.__dict__.items()
|
||||
if not k.startswith('_')}
|
||||
else:
|
||||
parsed = str(value)
|
||||
body = json.dumps(parsed, indent=2, default=str)
|
||||
except (TypeError, AttributeError):
|
||||
body = str(value)
|
||||
body = str(parsed)
|
||||
|
||||
except Exception as e:
|
||||
body = f"<Error formatting message: {e}>\n{str(msg)}"
|
||||
|
|
@ -148,15 +170,13 @@ async def log_writer(central_queue, file_handle, shutdown_event, console_output=
|
|||
break
|
||||
|
||||
|
||||
async def async_main(queues, output_file, pulsar_host, listener_name, subscriber_name, append_mode):
|
||||
async def async_main(queues, output_file, subscriber_name, append_mode, **pubsub_config):
|
||||
"""
|
||||
Main async function to monitor multiple queues concurrently.
|
||||
|
||||
Args:
|
||||
queues: List of queue names to monitor
|
||||
output_file: Path to output file
|
||||
pulsar_host: Pulsar connection URL
|
||||
listener_name: Pulsar listener name
|
||||
subscriber_name: Base name for subscribers
|
||||
append_mode: Whether to append to existing file
|
||||
"""
|
||||
|
|
@ -170,9 +190,9 @@ async def async_main(queues, output_file, pulsar_host, listener_name, subscriber
|
|||
|
||||
# Create backend connection
|
||||
try:
|
||||
backend = get_pubsub(pulsar_host=pulsar_host, pulsar_listener=listener_name, pubsub_backend='pulsar')
|
||||
backend = get_pubsub(**pubsub_config)
|
||||
except Exception as e:
|
||||
print(f"Error connecting to backend at {pulsar_host}: {e}", file=sys.stderr)
|
||||
print(f"Error connecting to backend: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Create Subscribers and central queue
|
||||
|
|
@ -267,25 +287,20 @@ def main():
|
|||
description='Monitor and dump messages from multiple Pulsar queues',
|
||||
epilog="""
|
||||
Examples:
|
||||
# Monitor agent and prompt queues
|
||||
tg-dump-queues non-persistent://tg/request/agent:default \\
|
||||
non-persistent://tg/request/prompt:default
|
||||
# Monitor agent and prompt flow queues
|
||||
tg-dump-queues flow:tg:agent-request:default \\
|
||||
flow:tg:prompt-request:default
|
||||
|
||||
# Monitor with custom output file
|
||||
tg-dump-queues non-persistent://tg/request/agent:default \\
|
||||
tg-dump-queues flow:tg:agent-request:default \\
|
||||
--output debug.log
|
||||
|
||||
# Append to existing log file
|
||||
tg-dump-queues non-persistent://tg/request/agent:default \\
|
||||
tg-dump-queues flow:tg:agent-request:default \\
|
||||
--output queue.log --append
|
||||
|
||||
Common queue patterns:
|
||||
- Agent requests: non-persistent://tg/request/agent:default
|
||||
- Agent responses: non-persistent://tg/response/agent:default
|
||||
- Prompt requests: non-persistent://tg/request/prompt:default
|
||||
- Prompt responses: non-persistent://tg/response/prompt:default
|
||||
- LLM requests: non-persistent://tg/request/text-completion:default
|
||||
- LLM responses: non-persistent://tg/response/text-completion:default
|
||||
# Raw Pulsar URIs also accepted
|
||||
tg-dump-queues persistent://tg/flow/agent-request:default
|
||||
|
||||
IMPORTANT:
|
||||
This tool subscribes to queues without a schema (schema-less mode). To avoid
|
||||
|
|
@ -316,17 +331,7 @@ IMPORTANT:
|
|||
help='Append to output file instead of overwriting'
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--pulsar-host',
|
||||
default='pulsar://localhost:6650',
|
||||
help='Pulsar host URL (default: pulsar://localhost:6650)'
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--listener-name',
|
||||
default='localhost',
|
||||
help='Pulsar listener name (default: localhost)'
|
||||
)
|
||||
add_pubsub_args(parser, standalone=True)
|
||||
|
||||
parser.add_argument(
|
||||
'--subscriber',
|
||||
|
|
@ -347,10 +352,10 @@ IMPORTANT:
|
|||
asyncio.run(async_main(
|
||||
queues=queues,
|
||||
output_file=args.output,
|
||||
pulsar_host=args.pulsar_host,
|
||||
listener_name=args.listener_name,
|
||||
subscriber_name=args.subscriber,
|
||||
append_mode=args.append
|
||||
append_mode=args.append,
|
||||
**{k: v for k, v in vars(args).items()
|
||||
if k not in ('queues', 'output', 'subscriber', 'append')},
|
||||
))
|
||||
except KeyboardInterrupt:
|
||||
# Already handled in async_main
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
"""
|
||||
Initialises Pulsar with Trustgraph tenant / namespaces & policy.
|
||||
Initialises TrustGraph pub/sub infrastructure and pushes initial config.
|
||||
|
||||
For Pulsar: creates tenant, namespaces, and retention policies.
|
||||
For RabbitMQ: queues are auto-declared, so only config push is needed.
|
||||
"""
|
||||
|
||||
import requests
|
||||
|
|
@ -8,10 +11,11 @@ import argparse
|
|||
import json
|
||||
|
||||
from trustgraph.clients.config_client import ConfigClient
|
||||
from trustgraph.base.pubsub import add_pubsub_args
|
||||
|
||||
default_pulsar_admin_url = "http://pulsar:8080"
|
||||
default_pulsar_host = "pulsar://pulsar:6650"
|
||||
subscriber = "tg-init-pulsar"
|
||||
subscriber = "tg-init-pubsub"
|
||||
|
||||
|
||||
def get_clusters(url):
|
||||
|
||||
|
|
@ -65,12 +69,11 @@ def ensure_namespace(url, tenant, namespace, config):
|
|||
|
||||
print(f"Namespace {tenant}/{namespace} created.", flush=True)
|
||||
|
||||
def ensure_config(config, pulsar_host, pulsar_api_key):
|
||||
def ensure_config(config, **pubsub_config):
|
||||
|
||||
cli = ConfigClient(
|
||||
subscriber=subscriber,
|
||||
pulsar_host=pulsar_host,
|
||||
pulsar_api_key=pulsar_api_key,
|
||||
**pubsub_config,
|
||||
)
|
||||
|
||||
while True:
|
||||
|
|
@ -115,11 +118,9 @@ def ensure_config(config, pulsar_host, pulsar_api_key):
|
|||
time.sleep(2)
|
||||
print("Retrying...", flush=True)
|
||||
continue
|
||||
|
||||
def init(
|
||||
pulsar_admin_url, pulsar_host, pulsar_api_key, tenant,
|
||||
config, config_file,
|
||||
):
|
||||
|
||||
def init_pulsar(pulsar_admin_url, tenant):
|
||||
"""Pulsar-specific setup: create tenant, namespaces, retention policies."""
|
||||
|
||||
clusters = get_clusters(pulsar_admin_url)
|
||||
|
||||
|
|
@ -137,25 +138,29 @@ def init(
|
|||
}
|
||||
})
|
||||
|
||||
ensure_namespace(pulsar_admin_url, tenant, "config", {
|
||||
ensure_namespace(pulsar_admin_url, tenant, "notify", {
|
||||
"retention_policies": {
|
||||
"retentionSizeInMB": 10,
|
||||
"retentionTimeInMinutes": -1,
|
||||
"retentionSizeInMB": -1,
|
||||
"retentionTimeInMinutes": 3,
|
||||
"subscriptionExpirationTimeMinutes": 5,
|
||||
}
|
||||
})
|
||||
|
||||
if config is not None:
|
||||
|
||||
def push_config(config_json, config_file, **pubsub_config):
|
||||
"""Push initial config if provided."""
|
||||
|
||||
if config_json is not None:
|
||||
|
||||
try:
|
||||
print("Decoding config...", flush=True)
|
||||
dec = json.loads(config)
|
||||
dec = json.loads(config_json)
|
||||
print("Decoded.", flush=True)
|
||||
except Exception as e:
|
||||
print("Exception:", e, flush=True)
|
||||
raise e
|
||||
|
||||
ensure_config(dec, pulsar_host, pulsar_api_key)
|
||||
ensure_config(dec, **pubsub_config)
|
||||
|
||||
elif config_file is not None:
|
||||
|
||||
|
|
@ -167,11 +172,12 @@ def init(
|
|||
print("Exception:", e, flush=True)
|
||||
raise e
|
||||
|
||||
ensure_config(dec, pulsar_host, pulsar_api_key)
|
||||
ensure_config(dec, **pubsub_config)
|
||||
|
||||
else:
|
||||
print("No config to update.", flush=True)
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
|
|
@ -180,22 +186,11 @@ def main():
|
|||
)
|
||||
|
||||
parser.add_argument(
|
||||
'-p', '--pulsar-admin-url',
|
||||
'--pulsar-admin-url',
|
||||
default=default_pulsar_admin_url,
|
||||
help=f'Pulsar admin URL (default: {default_pulsar_admin_url})',
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--pulsar-host',
|
||||
default=default_pulsar_host,
|
||||
help=f'Pulsar host (default: {default_pulsar_host})',
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--pulsar-api-key',
|
||||
help=f'Pulsar API key',
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'-c', '--config',
|
||||
help=f'Initial configuration to load',
|
||||
|
|
@ -212,18 +207,43 @@ def main():
|
|||
help=f'Tenant (default: tg)',
|
||||
)
|
||||
|
||||
add_pubsub_args(parser)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
backend_type = args.pubsub_backend
|
||||
|
||||
# Extract pubsub config from args
|
||||
pubsub_config = {
|
||||
k: v for k, v in vars(args).items()
|
||||
if k not in ('pulsar_admin_url', 'config', 'config_file', 'tenant')
|
||||
}
|
||||
|
||||
while True:
|
||||
|
||||
try:
|
||||
|
||||
print(flush=True)
|
||||
print(
|
||||
f"Initialising with Pulsar {args.pulsar_admin_url}...",
|
||||
flush=True
|
||||
# Pulsar-specific setup (tenants, namespaces)
|
||||
if backend_type == 'pulsar':
|
||||
print(flush=True)
|
||||
print(
|
||||
f"Initialising Pulsar at {args.pulsar_admin_url}...",
|
||||
flush=True,
|
||||
)
|
||||
init_pulsar(args.pulsar_admin_url, args.tenant)
|
||||
else:
|
||||
print(flush=True)
|
||||
print(
|
||||
f"Using {backend_type} backend (no admin setup needed).",
|
||||
flush=True,
|
||||
)
|
||||
|
||||
# Push config (works with any backend)
|
||||
push_config(
|
||||
args.config, args.config_file,
|
||||
**pubsub_config,
|
||||
)
|
||||
init(**vars(args))
|
||||
|
||||
print("Initialisation complete.", flush=True)
|
||||
break
|
||||
|
||||
|
|
@ -236,4 +256,4 @@ def main():
|
|||
print("Will retry...", flush=True)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
main()
|
||||
|
|
|
|||
|
|
@ -12,7 +12,13 @@ from trustgraph.api import (
|
|||
ProvenanceEvent,
|
||||
Question,
|
||||
Analysis,
|
||||
Observation,
|
||||
Conclusion,
|
||||
Decomposition,
|
||||
Finding,
|
||||
Plan,
|
||||
StepResult,
|
||||
Synthesis,
|
||||
AgentThought,
|
||||
AgentObservation,
|
||||
AgentAnswer,
|
||||
|
|
@ -176,16 +182,18 @@ def question_explainable(
|
|||
print(item.content, end="", flush=True)
|
||||
|
||||
elif isinstance(item, ProvenanceEvent):
|
||||
# Process provenance event immediately
|
||||
# Use inline entity if available, otherwise fetch from graph
|
||||
prov_id = item.explain_id
|
||||
explain_graph = item.explain_graph or "urn:graph:retrieval"
|
||||
|
||||
entity = explain_client.fetch_entity(
|
||||
prov_id,
|
||||
graph=explain_graph,
|
||||
user=user,
|
||||
collection=collection
|
||||
)
|
||||
entity = item.entity
|
||||
if entity is None:
|
||||
entity = explain_client.fetch_entity(
|
||||
prov_id,
|
||||
graph=explain_graph,
|
||||
user=user,
|
||||
collection=collection
|
||||
)
|
||||
|
||||
if entity is None:
|
||||
if debug:
|
||||
|
|
@ -201,13 +209,42 @@ def question_explainable(
|
|||
print(f" Time: {entity.timestamp}", file=sys.stderr)
|
||||
|
||||
elif isinstance(entity, Analysis):
|
||||
print(f"\n [iteration] {prov_id}", file=sys.stderr)
|
||||
if entity.action:
|
||||
print(f" Action: {entity.action}", file=sys.stderr)
|
||||
if entity.thought:
|
||||
print(f" Thought: {entity.thought}", file=sys.stderr)
|
||||
if entity.observation:
|
||||
print(f" Observation: {entity.observation}", file=sys.stderr)
|
||||
action_label = f": {entity.action}" if entity.action else ""
|
||||
print(f"\n [analysis{action_label}] {prov_id}", file=sys.stderr)
|
||||
|
||||
elif isinstance(entity, Observation):
|
||||
print(f"\n [observation] {prov_id}", file=sys.stderr)
|
||||
if entity.document:
|
||||
print(f" Document: {entity.document}", file=sys.stderr)
|
||||
|
||||
elif isinstance(entity, Decomposition):
|
||||
print(f"\n [decompose] {prov_id}", file=sys.stderr)
|
||||
for i, goal in enumerate(entity.goals):
|
||||
print(f" Thread {i}: {goal}", file=sys.stderr)
|
||||
|
||||
elif isinstance(entity, Finding):
|
||||
print(f"\n [finding] {prov_id}", file=sys.stderr)
|
||||
if entity.goal:
|
||||
print(f" Goal: {entity.goal}", file=sys.stderr)
|
||||
if entity.document:
|
||||
print(f" Document: {entity.document}", file=sys.stderr)
|
||||
|
||||
elif isinstance(entity, Plan):
|
||||
print(f"\n [plan] {prov_id}", file=sys.stderr)
|
||||
for i, step in enumerate(entity.steps):
|
||||
print(f" Step {i}: {step}", file=sys.stderr)
|
||||
|
||||
elif isinstance(entity, StepResult):
|
||||
print(f"\n [step-result] {prov_id}", file=sys.stderr)
|
||||
if entity.step:
|
||||
print(f" Step: {entity.step}", file=sys.stderr)
|
||||
if entity.document:
|
||||
print(f" Document: {entity.document}", file=sys.stderr)
|
||||
|
||||
elif isinstance(entity, Synthesis):
|
||||
print(f"\n [synthesis] {prov_id}", file=sys.stderr)
|
||||
if entity.document:
|
||||
print(f" Document: {entity.document}", file=sys.stderr)
|
||||
|
||||
elif isinstance(entity, Conclusion):
|
||||
print(f"\n [conclusion] {prov_id}", file=sys.stderr)
|
||||
|
|
@ -233,7 +270,8 @@ def question_explainable(
|
|||
|
||||
def question(
|
||||
url, question, flow_id, user, collection,
|
||||
plan=None, state=None, group=None, verbose=False, streaming=True,
|
||||
plan=None, state=None, group=None, pattern=None,
|
||||
verbose=False, streaming=True,
|
||||
token=None, explainable=False, debug=False
|
||||
):
|
||||
# Explainable mode uses the API to capture and process provenance events
|
||||
|
|
@ -273,6 +311,8 @@ def question(
|
|||
request_params["state"] = state
|
||||
if group is not None:
|
||||
request_params["group"] = group
|
||||
if pattern is not None:
|
||||
request_params["pattern"] = pattern
|
||||
|
||||
try:
|
||||
# Call agent
|
||||
|
|
@ -396,6 +436,12 @@ def main():
|
|||
help=f'Agent plan (default: unspecified)'
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'-p', '--pattern',
|
||||
choices=['react', 'plan-then-execute', 'supervisor'],
|
||||
help='Force execution pattern (default: auto-selected by meta-router)'
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'-s', '--state',
|
||||
help=f'Agent initial state (default: unspecified)'
|
||||
|
|
@ -444,6 +490,7 @@ def main():
|
|||
plan = args.plan,
|
||||
state = args.state,
|
||||
group = args.group,
|
||||
pattern = args.pattern,
|
||||
verbose = args.verbose,
|
||||
streaming = not args.no_streaming,
|
||||
token = args.token,
|
||||
|
|
|
|||
|
|
@ -45,16 +45,18 @@ def question_explainable(
|
|||
print(item.content, end="", flush=True)
|
||||
|
||||
elif isinstance(item, ProvenanceEvent):
|
||||
# Process provenance event immediately
|
||||
# Use inline entity if available, otherwise fetch from graph
|
||||
prov_id = item.explain_id
|
||||
explain_graph = item.explain_graph or "urn:graph:retrieval"
|
||||
|
||||
entity = explain_client.fetch_entity(
|
||||
prov_id,
|
||||
graph=explain_graph,
|
||||
user=user,
|
||||
collection=collection
|
||||
)
|
||||
entity = item.entity
|
||||
if entity is None:
|
||||
entity = explain_client.fetch_entity(
|
||||
prov_id,
|
||||
graph=explain_graph,
|
||||
user=user,
|
||||
collection=collection
|
||||
)
|
||||
|
||||
if entity is None:
|
||||
if debug:
|
||||
|
|
|
|||
|
|
@ -667,16 +667,18 @@ def _question_explainable_api(
|
|||
print(item.content, end="", flush=True)
|
||||
|
||||
elif isinstance(item, ProvenanceEvent):
|
||||
# Process provenance event immediately
|
||||
# Use inline entity if available, otherwise fetch from graph
|
||||
prov_id = item.explain_id
|
||||
explain_graph = item.explain_graph or "urn:graph:retrieval"
|
||||
|
||||
entity = explain_client.fetch_entity(
|
||||
prov_id,
|
||||
graph=explain_graph,
|
||||
user=user,
|
||||
collection=collection
|
||||
)
|
||||
entity = item.entity
|
||||
if entity is None:
|
||||
entity = explain_client.fetch_entity(
|
||||
prov_id,
|
||||
graph=explain_graph,
|
||||
user=user,
|
||||
collection=collection
|
||||
)
|
||||
|
||||
if entity is None:
|
||||
if debug:
|
||||
|
|
|
|||
239
trustgraph-cli/trustgraph/cli/invoke_sparql_query.py
Normal file
239
trustgraph-cli/trustgraph/cli/invoke_sparql_query.py
Normal file
|
|
@ -0,0 +1,239 @@
|
|||
"""
|
||||
Execute a SPARQL query against the TrustGraph knowledge graph.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import json
|
||||
import sys
|
||||
from trustgraph.api import Api
|
||||
|
||||
default_url = os.getenv("TRUSTGRAPH_URL", 'http://localhost:8088/')
|
||||
default_user = 'trustgraph'
|
||||
default_collection = 'default'
|
||||
|
||||
|
||||
def _term_cell(val):
|
||||
"""Extract display string from a wire-format term."""
|
||||
if val is None:
|
||||
return ""
|
||||
t = val.get("t", "")
|
||||
if t == "i":
|
||||
return val.get("i", "")
|
||||
elif t == "l":
|
||||
return val.get("v", "")
|
||||
return val.get("v", val.get("i", ""))
|
||||
|
||||
|
||||
def _term_str(val):
|
||||
"""Convert a wire-format term to a Turtle-style display string."""
|
||||
if val is None:
|
||||
return "?"
|
||||
t = val.get("t", "")
|
||||
if t == "i":
|
||||
return f"<{val.get('i', '')}>"
|
||||
elif t == "l":
|
||||
v = val.get("v", "")
|
||||
dt = val.get("d", "")
|
||||
lang = val.get("l", "")
|
||||
if lang:
|
||||
return f'"{v}"@{lang}'
|
||||
elif dt:
|
||||
return f'"{v}"^^<{dt}>'
|
||||
return f'"{v}"'
|
||||
return str(val)
|
||||
|
||||
|
||||
def sparql_query(url, token, flow_id, query, user, collection, limit,
|
||||
batch_size, output_format):
|
||||
|
||||
socket = Api(url=url, token=token).socket()
|
||||
flow = socket.flow(flow_id)
|
||||
|
||||
variables = None
|
||||
all_rows = []
|
||||
|
||||
try:
|
||||
|
||||
for response in flow.sparql_query_stream(
|
||||
query=query,
|
||||
user=user,
|
||||
collection=collection,
|
||||
limit=limit,
|
||||
batch_size=batch_size,
|
||||
):
|
||||
query_type = response.get("query-type", "select")
|
||||
|
||||
# ASK queries - just print and return
|
||||
if query_type == "ask":
|
||||
print("true" if response.get("ask-result") else "false")
|
||||
return
|
||||
|
||||
# CONSTRUCT/DESCRIBE - print triples
|
||||
if query_type in ("construct", "describe"):
|
||||
triples = response.get("triples", [])
|
||||
if not triples:
|
||||
print("No triples.")
|
||||
elif output_format == "json":
|
||||
print(json.dumps(triples, indent=2))
|
||||
else:
|
||||
for t in triples:
|
||||
s = _term_str(t.get("s"))
|
||||
p = _term_str(t.get("p"))
|
||||
o = _term_str(t.get("o"))
|
||||
print(f"{s} {p} {o} .")
|
||||
return
|
||||
|
||||
# SELECT - accumulate bindings across batches
|
||||
if variables is None:
|
||||
variables = response.get("variables", [])
|
||||
|
||||
bindings = response.get("bindings", [])
|
||||
for binding in bindings:
|
||||
values = binding.get("values", [])
|
||||
all_rows.append([_term_cell(v) for v in values])
|
||||
|
||||
# Output SELECT results
|
||||
if variables is None:
|
||||
print("No results.")
|
||||
return
|
||||
|
||||
if not all_rows:
|
||||
print("No results.")
|
||||
return
|
||||
|
||||
if output_format == "json":
|
||||
rows = []
|
||||
for row in all_rows:
|
||||
rows.append({
|
||||
var: cell for var, cell in zip(variables, row)
|
||||
})
|
||||
print(json.dumps(rows, indent=2))
|
||||
else:
|
||||
# Table format
|
||||
col_widths = [len(v) for v in variables]
|
||||
for row in all_rows:
|
||||
for i, cell in enumerate(row):
|
||||
if i < len(col_widths):
|
||||
col_widths[i] = max(col_widths[i], len(cell))
|
||||
|
||||
header = " | ".join(
|
||||
v.ljust(col_widths[i]) for i, v in enumerate(variables)
|
||||
)
|
||||
separator = "-+-".join("-" * w for w in col_widths)
|
||||
print(header)
|
||||
print(separator)
|
||||
for row in all_rows:
|
||||
line = " | ".join(
|
||||
cell.ljust(col_widths[i]) if i < len(col_widths) else cell
|
||||
for i, cell in enumerate(row)
|
||||
)
|
||||
print(line)
|
||||
|
||||
finally:
|
||||
socket.close()
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
prog='tg-invoke-sparql-query',
|
||||
description=__doc__,
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'-u', '--url',
|
||||
default=default_url,
|
||||
help=f'API URL (default: {default_url})',
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'-t', '--token',
|
||||
default=os.getenv("TRUSTGRAPH_TOKEN"),
|
||||
help='API bearer token (default: TRUSTGRAPH_TOKEN env var)',
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'-f', '--flow-id',
|
||||
default="default",
|
||||
help='Flow ID (default: default)',
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'-q', '--query',
|
||||
help='SPARQL query string',
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'-i', '--input',
|
||||
help='Read SPARQL query from file (use - for stdin)',
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'-U', '--user',
|
||||
default=default_user,
|
||||
help=f'User ID (default: {default_user})',
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'-C', '--collection',
|
||||
default=default_collection,
|
||||
help=f'Collection ID (default: {default_collection})',
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'-l', '--limit',
|
||||
type=int,
|
||||
default=10000,
|
||||
help='Result limit (default: 10000)',
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'-b', '--batch-size',
|
||||
type=int,
|
||||
default=20,
|
||||
help='Streaming batch size (default: 20)',
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--format',
|
||||
choices=['table', 'json'],
|
||||
default='table',
|
||||
help='Output format (default: table)',
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Get query from argument or file
|
||||
query = args.query
|
||||
if not query and args.input:
|
||||
if args.input == '-':
|
||||
query = sys.stdin.read()
|
||||
else:
|
||||
with open(args.input) as f:
|
||||
query = f.read()
|
||||
|
||||
if not query:
|
||||
parser.error("Either -q/--query or -i/--input is required")
|
||||
|
||||
try:
|
||||
|
||||
sparql_query(
|
||||
url=args.url,
|
||||
token=args.token,
|
||||
flow_id=args.flow_id,
|
||||
query=query,
|
||||
user=args.user,
|
||||
collection=args.collection,
|
||||
limit=args.limit,
|
||||
batch_size=args.batch_size,
|
||||
output_format=args.format,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Exception: {e}", flush=True, file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
328
trustgraph-cli/trustgraph/cli/monitor_prompts.py
Normal file
328
trustgraph-cli/trustgraph/cli/monitor_prompts.py
Normal file
|
|
@ -0,0 +1,328 @@
|
|||
"""
|
||||
Monitor prompt request/response queues and log activity with timing.
|
||||
|
||||
Subscribes to prompt request and response queues, correlates
|
||||
them by message ID, and logs a summary of each request/response with
|
||||
elapsed time. Streaming responses are accumulated and shown once at
|
||||
completion.
|
||||
|
||||
Examples:
|
||||
tg-monitor-prompts
|
||||
tg-monitor-prompts --flow default --max-lines 5
|
||||
tg-monitor-prompts --queue-type prompt-rag
|
||||
"""
|
||||
|
||||
import json
|
||||
import asyncio
|
||||
import sys
|
||||
import argparse
|
||||
from datetime import datetime
|
||||
from collections import OrderedDict
|
||||
|
||||
from trustgraph.base.pubsub import get_pubsub, add_pubsub_args
|
||||
|
||||
|
||||
default_flow = "default"
|
||||
default_queue_type = "prompt-rag"
|
||||
default_max_lines = 3
|
||||
default_max_width = 80
|
||||
|
||||
|
||||
def truncate_text(text, max_lines, max_width):
|
||||
"""Truncate text to max_lines lines, each at most max_width chars."""
|
||||
if not text:
|
||||
return "(empty)"
|
||||
|
||||
lines = text.splitlines()
|
||||
result = []
|
||||
for line in lines[:max_lines]:
|
||||
if len(line) > max_width:
|
||||
result.append(line[:max_width - 3] + "...")
|
||||
else:
|
||||
result.append(line)
|
||||
|
||||
if len(lines) > max_lines:
|
||||
result.append(f" ... ({len(lines) - max_lines} more lines)")
|
||||
|
||||
return "\n".join(result)
|
||||
|
||||
|
||||
def summarise_value(value, max_width):
|
||||
"""Summarise a term value — show type and size for large values."""
|
||||
# Try to parse JSON
|
||||
try:
|
||||
parsed = json.loads(value)
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
parsed = value
|
||||
|
||||
if isinstance(parsed, list):
|
||||
return f"[{len(parsed)} items]"
|
||||
elif isinstance(parsed, dict):
|
||||
return f"{{{len(parsed)} keys}}"
|
||||
elif isinstance(parsed, str):
|
||||
if len(parsed) > max_width:
|
||||
return parsed[:max_width - 3] + "..."
|
||||
return parsed
|
||||
else:
|
||||
s = str(parsed)
|
||||
if len(s) > max_width:
|
||||
return s[:max_width - 3] + "..."
|
||||
return s
|
||||
|
||||
|
||||
def format_terms(terms, max_lines, max_width):
|
||||
"""Format prompt terms for display — concise summary."""
|
||||
if not terms:
|
||||
return ""
|
||||
|
||||
parts = []
|
||||
for key, value in terms.items():
|
||||
summary = summarise_value(value, max_width - len(key) - 4)
|
||||
parts.append(f" {key}: {summary}")
|
||||
|
||||
return "\n".join(parts)
|
||||
|
||||
|
||||
def parse_raw_message(msg):
|
||||
"""Parse a raw message into (correlation_id, body_dict)."""
|
||||
try:
|
||||
props = msg.properties()
|
||||
corr_id = props.get("id", "")
|
||||
except Exception:
|
||||
corr_id = ""
|
||||
|
||||
try:
|
||||
value = msg.value()
|
||||
if isinstance(value, dict):
|
||||
body = value
|
||||
elif isinstance(value, bytes):
|
||||
body = json.loads(value.decode("utf-8"))
|
||||
elif isinstance(value, str):
|
||||
body = json.loads(value)
|
||||
else:
|
||||
body = {}
|
||||
except Exception:
|
||||
body = {}
|
||||
|
||||
return corr_id, body
|
||||
|
||||
|
||||
async def monitor(flow, queue_type, max_lines, max_width, **config):
|
||||
|
||||
request_queue = f"request:tg:{queue_type}:{flow}"
|
||||
response_queue = f"response:tg:{queue_type}:{flow}"
|
||||
|
||||
print(f"Monitoring prompt queues:")
|
||||
print(f" Request: {request_queue}")
|
||||
print(f" Response: {response_queue}")
|
||||
print(f"Press Ctrl+C to stop\n")
|
||||
|
||||
backend = get_pubsub(**config)
|
||||
|
||||
req_consumer = backend.create_consumer(
|
||||
topic=request_queue,
|
||||
subscription="prompt-monitor-req",
|
||||
schema=None,
|
||||
initial_position='latest',
|
||||
)
|
||||
|
||||
resp_consumer = backend.create_consumer(
|
||||
topic=response_queue,
|
||||
subscription="prompt-monitor-resp",
|
||||
schema=None,
|
||||
initial_position='latest',
|
||||
)
|
||||
|
||||
# Track in-flight requests: corr_id -> (timestamp, template_id)
|
||||
in_flight = OrderedDict()
|
||||
|
||||
# Accumulate streaming responses: corr_id -> list of text chunks
|
||||
streaming_chunks = {}
|
||||
|
||||
print("Listening...\n")
|
||||
|
||||
try:
|
||||
while True:
|
||||
got_message = False
|
||||
|
||||
# Poll request queue
|
||||
try:
|
||||
msg = req_consumer.receive(timeout_millis=100)
|
||||
got_message = True
|
||||
timestamp = datetime.now()
|
||||
corr_id, body = parse_raw_message(msg)
|
||||
time_str = timestamp.strftime("%H:%M:%S.%f")[:-3]
|
||||
|
||||
template_id = body.get("id", "(unknown)")
|
||||
terms = body.get("terms", {})
|
||||
streaming = body.get("streaming", False)
|
||||
|
||||
in_flight[corr_id] = (timestamp, template_id)
|
||||
|
||||
# Limit size
|
||||
while len(in_flight) > 1000:
|
||||
in_flight.popitem(last=False)
|
||||
|
||||
stream_flag = " [streaming]" if streaming else ""
|
||||
id_display = corr_id[:8] if corr_id else "--------"
|
||||
print(f"[{time_str}] REQ {id_display} "
|
||||
f"template={template_id}{stream_flag}")
|
||||
|
||||
if terms:
|
||||
print(format_terms(terms, max_lines, max_width))
|
||||
|
||||
req_consumer.acknowledge(msg)
|
||||
except TimeoutError:
|
||||
pass
|
||||
|
||||
# Poll response queue
|
||||
try:
|
||||
msg = resp_consumer.receive(timeout_millis=100)
|
||||
got_message = True
|
||||
timestamp = datetime.now()
|
||||
corr_id, body = parse_raw_message(msg)
|
||||
time_str = timestamp.strftime("%H:%M:%S.%f")[:-3]
|
||||
id_display = corr_id[:8] if corr_id else "--------"
|
||||
|
||||
error = body.get("error")
|
||||
text = body.get("text", "")
|
||||
obj = body.get("object", "")
|
||||
eos = body.get("end_of_stream", False)
|
||||
|
||||
if error:
|
||||
# Error — show immediately
|
||||
elapsed_str = ""
|
||||
if corr_id in in_flight:
|
||||
req_timestamp, _ = in_flight.pop(corr_id)
|
||||
elapsed = (timestamp - req_timestamp).total_seconds()
|
||||
elapsed_str = f" ({elapsed:.3f}s)"
|
||||
streaming_chunks.pop(corr_id, None)
|
||||
|
||||
err_msg = error
|
||||
if isinstance(error, dict):
|
||||
err_msg = error.get("message", str(error))
|
||||
print(f"[{time_str}] ERR {id_display} "
|
||||
f"{err_msg}{elapsed_str}")
|
||||
|
||||
elif eos:
|
||||
# End of stream — show accumulated text + timing
|
||||
elapsed_str = ""
|
||||
if corr_id in in_flight:
|
||||
req_timestamp, _ = in_flight.pop(corr_id)
|
||||
elapsed = (timestamp - req_timestamp).total_seconds()
|
||||
elapsed_str = f" ({elapsed:.3f}s)"
|
||||
|
||||
accumulated = streaming_chunks.pop(corr_id, [])
|
||||
if text:
|
||||
accumulated.append(text)
|
||||
|
||||
full_text = "".join(accumulated)
|
||||
if full_text:
|
||||
truncated = truncate_text(
|
||||
full_text, max_lines, max_width
|
||||
)
|
||||
print(f"[{time_str}] RESP {id_display}"
|
||||
f"{elapsed_str}")
|
||||
print(f" {truncated}")
|
||||
else:
|
||||
print(f"[{time_str}] RESP {id_display}"
|
||||
f"{elapsed_str} (empty)")
|
||||
|
||||
elif text or obj:
|
||||
# Streaming chunk or non-streaming response
|
||||
if corr_id in streaming_chunks or (
|
||||
corr_id in in_flight
|
||||
):
|
||||
# Accumulate streaming chunk
|
||||
if corr_id not in streaming_chunks:
|
||||
streaming_chunks[corr_id] = []
|
||||
streaming_chunks[corr_id].append(text or obj)
|
||||
else:
|
||||
# Non-streaming single response
|
||||
elapsed_str = ""
|
||||
if corr_id in in_flight:
|
||||
req_timestamp, _ = in_flight.pop(corr_id)
|
||||
elapsed = (
|
||||
timestamp - req_timestamp
|
||||
).total_seconds()
|
||||
elapsed_str = f" ({elapsed:.3f}s)"
|
||||
|
||||
content = text or obj
|
||||
label = "" if text else " (object)"
|
||||
truncated = truncate_text(
|
||||
content, max_lines, max_width
|
||||
)
|
||||
print(f"[{time_str}] RESP {id_display}"
|
||||
f"{label}{elapsed_str}")
|
||||
print(f" {truncated}")
|
||||
|
||||
resp_consumer.acknowledge(msg)
|
||||
except TimeoutError:
|
||||
pass
|
||||
|
||||
if not got_message:
|
||||
await asyncio.sleep(0.05)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\nStopping...")
|
||||
finally:
|
||||
req_consumer.close()
|
||||
resp_consumer.close()
|
||||
backend.close()
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
prog="tg-monitor-prompts",
|
||||
description=__doc__,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-f", "--flow",
|
||||
default=default_flow,
|
||||
help=f"Flow ID (default: {default_flow})",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-q", "--queue-type",
|
||||
default=default_queue_type,
|
||||
help=f"Queue type: prompt or prompt-rag (default: {default_queue_type})",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-l", "--max-lines",
|
||||
type=int,
|
||||
default=default_max_lines,
|
||||
help=f"Max lines of text per term/response (default: {default_max_lines})",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-w", "--max-width",
|
||||
type=int,
|
||||
default=default_max_width,
|
||||
help=f"Max width per line (default: {default_max_width})",
|
||||
)
|
||||
|
||||
add_pubsub_args(parser, standalone=True)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
try:
|
||||
asyncio.run(monitor(
|
||||
flow=args.flow,
|
||||
queue_type=args.queue_type,
|
||||
max_lines=args.max_lines,
|
||||
max_width=args.max_width,
|
||||
**{k: v for k, v in vars(args).items()
|
||||
if k not in ('flow', 'queue_type', 'max_lines', 'max_width')},
|
||||
))
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
except Exception as e:
|
||||
print(f"Fatal error: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -26,7 +26,12 @@ from trustgraph.api import (
|
|||
Focus,
|
||||
Synthesis,
|
||||
Analysis,
|
||||
Observation,
|
||||
Conclusion,
|
||||
Decomposition,
|
||||
Finding,
|
||||
Plan,
|
||||
StepResult,
|
||||
)
|
||||
|
||||
default_url = os.getenv("TRUSTGRAPH_URL", 'http://localhost:8088/')
|
||||
|
|
@ -297,6 +302,23 @@ def print_docrag_text(trace, explain_client, api, user):
|
|||
print("No synthesis data found")
|
||||
|
||||
|
||||
def _print_document_content(explain_client, api, user, document_uri, label="Answer"):
|
||||
"""Fetch and print document content, or fall back to URI."""
|
||||
if not document_uri:
|
||||
return
|
||||
content = ""
|
||||
if api:
|
||||
content = explain_client.fetch_document_content(
|
||||
document_uri, api, user
|
||||
)
|
||||
if content:
|
||||
print(f"{label}:")
|
||||
for line in content.split("\n"):
|
||||
print(f" {line}")
|
||||
else:
|
||||
print(f"Document: {document_uri}")
|
||||
|
||||
|
||||
def print_agent_text(trace, explain_client, api, user):
|
||||
"""Print Agent trace in text format."""
|
||||
question = trace.get("question")
|
||||
|
|
@ -310,82 +332,150 @@ def print_agent_text(trace, explain_client, api, user):
|
|||
print(f"Time: {question.timestamp}")
|
||||
print()
|
||||
|
||||
# Analysis steps
|
||||
print("--- Analysis ---")
|
||||
iterations = trace.get("iterations", [])
|
||||
if iterations:
|
||||
for i, analysis in enumerate(iterations, 1):
|
||||
print(f"Analysis {i}:")
|
||||
print(f" Thought: {analysis.thought or 'N/A'}")
|
||||
print(f" Action: {analysis.action or 'N/A'}")
|
||||
# Walk the steps list which contains all entity types
|
||||
steps = trace.get("steps", [])
|
||||
|
||||
for step in steps:
|
||||
|
||||
if analysis.arguments:
|
||||
# Try to pretty-print JSON arguments
|
||||
if isinstance(step, Decomposition):
|
||||
print("--- Decomposition ---")
|
||||
print(f"Decomposed into {len(step.goals)} research threads:")
|
||||
for i, goal in enumerate(step.goals):
|
||||
print(f" {i}: {goal}")
|
||||
print()
|
||||
|
||||
elif isinstance(step, Finding):
|
||||
print("--- Finding ---")
|
||||
print(f"Goal: {step.goal}")
|
||||
_print_document_content(
|
||||
explain_client, api, user, step.document, "Result",
|
||||
)
|
||||
print()
|
||||
|
||||
elif isinstance(step, Plan):
|
||||
print("--- Plan ---")
|
||||
print(f"Plan with {len(step.steps)} steps:")
|
||||
for i, s in enumerate(step.steps):
|
||||
print(f" {i}: {s}")
|
||||
print()
|
||||
|
||||
elif isinstance(step, StepResult):
|
||||
print("--- Step Result ---")
|
||||
print(f"Step: {step.step}")
|
||||
_print_document_content(
|
||||
explain_client, api, user, step.document, "Result",
|
||||
)
|
||||
print()
|
||||
|
||||
elif isinstance(step, Analysis):
|
||||
print("--- Analysis ---")
|
||||
print(f" Action: {step.action or 'N/A'}")
|
||||
|
||||
if step.arguments:
|
||||
try:
|
||||
args_obj = json.loads(analysis.arguments)
|
||||
args_obj = json.loads(step.arguments)
|
||||
args_str = json.dumps(args_obj, indent=4)
|
||||
print(f" Arguments:")
|
||||
for line in args_str.split('\n'):
|
||||
print(f" {line}")
|
||||
except Exception:
|
||||
print(f" Arguments: {analysis.arguments}")
|
||||
else:
|
||||
print(f" Arguments: N/A")
|
||||
|
||||
obs = analysis.observation or 'N/A'
|
||||
if obs and len(obs) > 200:
|
||||
obs = obs[:200] + "... [truncated]"
|
||||
print(f" Observation: {obs}")
|
||||
print(f" Arguments: {step.arguments}")
|
||||
print()
|
||||
else:
|
||||
print("No analysis steps recorded")
|
||||
print()
|
||||
|
||||
# Conclusion
|
||||
print("--- Conclusion ---")
|
||||
conclusion = trace.get("conclusion")
|
||||
if conclusion:
|
||||
content = ""
|
||||
if conclusion.document and api:
|
||||
content = explain_client.fetch_document_content(
|
||||
conclusion.document, api, user
|
||||
elif isinstance(step, Observation):
|
||||
print("--- Observation ---")
|
||||
_print_document_content(
|
||||
explain_client, api, user, step.document, "Content",
|
||||
)
|
||||
if content:
|
||||
print("Answer:")
|
||||
for line in content.split("\n"):
|
||||
print(f" {line}")
|
||||
elif conclusion.document:
|
||||
print(f"Document: {conclusion.document}")
|
||||
else:
|
||||
print("No conclusion recorded")
|
||||
else:
|
||||
print("No conclusion recorded")
|
||||
print()
|
||||
|
||||
elif isinstance(step, Synthesis):
|
||||
print("--- Synthesis ---")
|
||||
_print_document_content(
|
||||
explain_client, api, user, step.document, "Answer",
|
||||
)
|
||||
print()
|
||||
|
||||
elif isinstance(step, Conclusion):
|
||||
print("--- Conclusion ---")
|
||||
_print_document_content(
|
||||
explain_client, api, user, step.document, "Answer",
|
||||
)
|
||||
print()
|
||||
|
||||
if not steps:
|
||||
print("No trace steps recorded")
|
||||
print()
|
||||
|
||||
|
||||
def trace_to_dict(trace, trace_type):
|
||||
"""Convert trace entities to JSON-serializable dict."""
|
||||
if trace_type == "agent":
|
||||
question = trace.get("question")
|
||||
|
||||
def _step_to_dict(step):
|
||||
if isinstance(step, Decomposition):
|
||||
return {
|
||||
"type": "decomposition",
|
||||
"id": step.uri,
|
||||
"goals": step.goals,
|
||||
}
|
||||
elif isinstance(step, Finding):
|
||||
return {
|
||||
"type": "finding",
|
||||
"id": step.uri,
|
||||
"goal": step.goal,
|
||||
"document": step.document,
|
||||
}
|
||||
elif isinstance(step, Plan):
|
||||
return {
|
||||
"type": "plan",
|
||||
"id": step.uri,
|
||||
"steps": step.steps,
|
||||
}
|
||||
elif isinstance(step, StepResult):
|
||||
return {
|
||||
"type": "step-result",
|
||||
"id": step.uri,
|
||||
"step": step.step,
|
||||
"document": step.document,
|
||||
}
|
||||
elif isinstance(step, Observation):
|
||||
return {
|
||||
"type": "observation",
|
||||
"id": step.uri,
|
||||
"document": step.document,
|
||||
}
|
||||
elif isinstance(step, Analysis):
|
||||
return {
|
||||
"type": "analysis",
|
||||
"id": step.uri,
|
||||
"action": step.action,
|
||||
"arguments": step.arguments,
|
||||
"thought": step.thought,
|
||||
}
|
||||
elif isinstance(step, Synthesis):
|
||||
return {
|
||||
"type": "synthesis",
|
||||
"id": step.uri,
|
||||
"document": step.document,
|
||||
}
|
||||
elif isinstance(step, Conclusion):
|
||||
return {
|
||||
"type": "conclusion",
|
||||
"id": step.uri,
|
||||
"document": step.document,
|
||||
}
|
||||
return {"type": step.entity_type, "id": step.uri}
|
||||
|
||||
steps = trace.get("steps", [])
|
||||
|
||||
return {
|
||||
"type": "agent",
|
||||
"session_id": question.uri if question else None,
|
||||
"question": question.query if question else None,
|
||||
"time": question.timestamp if question else None,
|
||||
"iterations": [
|
||||
{
|
||||
"id": a.uri,
|
||||
"thought": a.thought,
|
||||
"action": a.action,
|
||||
"arguments": a.arguments,
|
||||
"observation": a.observation,
|
||||
}
|
||||
for a in trace.get("iterations", [])
|
||||
],
|
||||
"conclusion": {
|
||||
"id": trace["conclusion"].uri,
|
||||
"document": trace["conclusion"].document,
|
||||
} if trace.get("conclusion") else None,
|
||||
"steps": [_step_to_dict(s) for s in steps],
|
||||
}
|
||||
elif trace_type == "docrag":
|
||||
question = trace.get("question")
|
||||
|
|
|
|||
|
|
@ -398,16 +398,11 @@ def main():
|
|||
print("=" * 60)
|
||||
print(tr.t("cli.verify_system_status.title"))
|
||||
print("=" * 60)
|
||||
# print(f"Global timeout: {args.global_timeout}s")
|
||||
# print(f"Check timeout: {args.check_timeout}s")
|
||||
# print(f"Retry delay: {args.retry_delay}s")
|
||||
# print("=" * 60)
|
||||
print()
|
||||
|
||||
# Phase 1: Infrastructure
|
||||
print(tr.t("cli.verify_system_status.phase_1"))
|
||||
print("-" * 60)
|
||||
|
||||
if not checker.run_check(
|
||||
tr.t("cli.verify_system_status.check_name.pulsar"),
|
||||
check_pulsar,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue