Merge branch 'master' into type-hints/base-classes

This commit is contained in:
cybermaggedon 2026-04-16 09:55:49 +01:00 committed by GitHub
commit 11ca2bec7f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
714 changed files with 243185 additions and 1975 deletions

View file

@ -15,6 +15,7 @@ dependencies = [
"requests",
"python-logging-loki",
"pika",
"pyyaml",
]
classifiers = [
"Programming Language :: Python :: 3",
@ -24,8 +25,14 @@ classifiers = [
[project.urls]
Homepage = "https://github.com/trustgraph-ai/trustgraph"
[project.scripts]
processor-group = "trustgraph.base.processor_group:run"
[tool.setuptools.packages.find]
include = ["trustgraph*"]
[tool.setuptools.package-data]
"trustgraph.i18n.packs" = ["*.json"]
[tool.setuptools.dynamic]
version = {attr = "trustgraph.base_version.__version__"}
version = {attr = "trustgraph.base_version.__version__"}

View file

@ -107,6 +107,7 @@ from .types import (
AgentObservation,
AgentAnswer,
RAGChunk,
TextCompletionResult,
ProvenanceEvent,
)
@ -185,6 +186,7 @@ __all__ = [
"AgentObservation",
"AgentAnswer",
"RAGChunk",
"TextCompletionResult",
"ProvenanceEvent",
# Exceptions

View file

@ -14,6 +14,8 @@ import aiohttp
import json
from typing import Optional, Dict, Any, List
from . types import TextCompletionResult
from . exceptions import ProtocolException, ApplicationException
@ -434,12 +436,11 @@ class AsyncFlowInstance:
return await self.request("agent", request_data)
async def text_completion(self, system: str, prompt: str, **kwargs: Any) -> str:
async def text_completion(self, system: str, prompt: str, **kwargs: Any) -> TextCompletionResult:
"""
Generate text completion (non-streaming).
Generates a text response from an LLM given a system prompt and user prompt.
Returns the complete response text.
Note: This method does not support streaming. For streaming text generation,
use AsyncSocketFlowInstance.text_completion() instead.
@ -450,19 +451,19 @@ class AsyncFlowInstance:
**kwargs: Additional service-specific parameters
Returns:
str: Complete generated text response
TextCompletionResult: Result with text, in_token, out_token, model
Example:
```python
async_flow = await api.async_flow()
flow = async_flow.id("default")
# Generate text
response = await flow.text_completion(
result = await flow.text_completion(
system="You are a helpful assistant.",
prompt="Explain quantum computing in simple terms."
)
print(response)
print(result.text)
print(f"Tokens: {result.in_token} in, {result.out_token} out")
```
"""
request_data = {
@ -473,7 +474,12 @@ class AsyncFlowInstance:
request_data.update(kwargs)
result = await self.request("text-completion", request_data)
return result.get("response", "")
return TextCompletionResult(
text=result.get("response", ""),
in_token=result.get("in_token"),
out_token=result.get("out_token"),
model=result.get("model"),
)
async def graph_rag(self, query: str, user: str, collection: str,
max_subgraph_size: int = 1000, max_subgraph_count: int = 5,

View file

@ -4,7 +4,7 @@ import asyncio
import websockets
from typing import Optional, Dict, Any, AsyncIterator, Union
from . types import AgentThought, AgentObservation, AgentAnswer, RAGChunk
from . types import AgentThought, AgentObservation, AgentAnswer, RAGChunk, TextCompletionResult
from . exceptions import ProtocolException, ApplicationException
@ -178,30 +178,32 @@ class AsyncSocketClient:
def _parse_chunk(self, resp: Dict[str, Any]):
"""Parse response chunk into appropriate type. Returns None for non-content messages."""
chunk_type = resp.get("chunk_type")
message_type = resp.get("message_type")
# Handle new GraphRAG message format with message_type
if message_type == "provenance":
return None
if chunk_type == "thought":
if message_type == "thought":
return AgentThought(
content=resp.get("content", ""),
end_of_message=resp.get("end_of_message", False)
)
elif chunk_type == "observation":
elif message_type == "observation":
return AgentObservation(
content=resp.get("content", ""),
end_of_message=resp.get("end_of_message", False)
)
elif chunk_type == "answer" or chunk_type == "final-answer":
elif message_type == "answer" or message_type == "final-answer":
return AgentAnswer(
content=resp.get("content", ""),
end_of_message=resp.get("end_of_message", False),
end_of_dialog=resp.get("end_of_dialog", False)
end_of_dialog=resp.get("end_of_dialog", False),
in_token=resp.get("in_token"),
out_token=resp.get("out_token"),
model=resp.get("model"),
)
elif chunk_type == "action":
elif message_type == "action":
return AgentThought(
content=resp.get("content", ""),
end_of_message=resp.get("end_of_message", False)
@ -211,7 +213,10 @@ class AsyncSocketClient:
return RAGChunk(
content=content,
end_of_stream=resp.get("end_of_stream", False),
error=None
error=None,
in_token=resp.get("in_token"),
out_token=resp.get("out_token"),
model=resp.get("model"),
)
async def aclose(self):
@ -269,7 +274,11 @@ class AsyncSocketFlowInstance:
return await self.client._send_request("agent", self.flow_id, request)
async def text_completion(self, system: str, prompt: str, streaming: bool = False, **kwargs):
"""Text completion with optional streaming"""
"""Text completion with optional streaming.
Non-streaming: returns a TextCompletionResult with text and token counts.
Streaming: returns an async iterator of RAGChunk (with token counts on the final chunk).
"""
request = {
"system": system,
"prompt": prompt,
@ -281,13 +290,18 @@ class AsyncSocketFlowInstance:
return self._text_completion_streaming(request)
else:
result = await self.client._send_request("text-completion", self.flow_id, request)
return result.get("response", "")
return TextCompletionResult(
text=result.get("response", ""),
in_token=result.get("in_token"),
out_token=result.get("out_token"),
model=result.get("model"),
)
async def _text_completion_streaming(self, request):
"""Helper for streaming text completion"""
"""Helper for streaming text completion. Yields RAGChunk objects."""
async for chunk in self.client._send_request_streaming("text-completion", self.flow_id, request):
if hasattr(chunk, 'content'):
yield chunk.content
if isinstance(chunk, RAGChunk):
yield chunk
async def graph_rag(self, query: str, user: str, collection: str,
max_subgraph_size: int = 1000, max_subgraph_count: int = 5,

View file

@ -11,7 +11,7 @@ import base64
from .. knowledge import hash, Uri, Literal, QuotedTriple
from .. schema import IRI, LITERAL, TRIPLE
from . types import Triple
from . types import Triple, TextCompletionResult
from . exceptions import ProtocolException
@ -360,16 +360,17 @@ class FlowInstance:
prompt: User prompt/question
Returns:
str: Generated response text
TextCompletionResult: Result with text, in_token, out_token, model
Example:
```python
flow = api.flow().id("default")
response = flow.text_completion(
result = flow.text_completion(
system="You are a helpful assistant",
prompt="What is quantum computing?"
)
print(response)
print(result.text)
print(f"Tokens: {result.in_token} in, {result.out_token} out")
```
"""
@ -379,10 +380,17 @@ class FlowInstance:
"prompt": prompt
}
return self.request(
result = self.request(
"service/text-completion",
input
)["response"]
)
return TextCompletionResult(
text=result.get("response", ""),
in_token=result.get("in_token"),
out_token=result.get("out_token"),
model=result.get("model"),
)
def agent(self, question, user="trustgraph", state=None, group=None, history=None):
"""
@ -498,10 +506,17 @@ class FlowInstance:
"edge-limit": edge_limit,
}
return self.request(
result = self.request(
"service/graph-rag",
input
)["response"]
)
return TextCompletionResult(
text=result.get("response", ""),
in_token=result.get("in_token"),
out_token=result.get("out_token"),
model=result.get("model"),
)
def document_rag(
self, query, user="trustgraph", collection="default",
@ -543,10 +558,17 @@ class FlowInstance:
"doc-limit": doc_limit,
}
return self.request(
result = self.request(
"service/document-rag",
input
)["response"]
)
return TextCompletionResult(
text=result.get("response", ""),
in_token=result.get("in_token"),
out_token=result.get("out_token"),
model=result.get("model"),
)
def embeddings(self, texts):
"""

View file

@ -14,7 +14,7 @@ import websockets
from typing import Optional, Dict, Any, Iterator, Union, List
from threading import Lock
from . types import AgentThought, AgentObservation, AgentAnswer, RAGChunk, StreamingChunk, ProvenanceEvent
from . types import AgentThought, AgentObservation, AgentAnswer, RAGChunk, StreamingChunk, ProvenanceEvent, TextCompletionResult
from . exceptions import ProtocolException, raise_from_error_dict
@ -360,41 +360,36 @@ class SocketClient:
def _parse_chunk(self, resp: Dict[str, Any], include_provenance: bool = False) -> Optional[StreamingChunk]:
"""Parse response chunk into appropriate type. Returns None for non-content messages."""
chunk_type = resp.get("chunk_type")
message_type = resp.get("message_type")
# Handle GraphRAG/DocRAG message format with message_type
if message_type == "explain":
if include_provenance:
return self._build_provenance_event(resp)
return None
# Handle Agent message format with chunk_type="explain"
if chunk_type == "explain":
if include_provenance:
return self._build_provenance_event(resp)
return None
if chunk_type == "thought":
if message_type == "thought":
return AgentThought(
content=resp.get("content", ""),
end_of_message=resp.get("end_of_message", False),
message_id=resp.get("message_id", ""),
)
elif chunk_type == "observation":
elif message_type == "observation":
return AgentObservation(
content=resp.get("content", ""),
end_of_message=resp.get("end_of_message", False),
message_id=resp.get("message_id", ""),
)
elif chunk_type == "answer" or chunk_type == "final-answer":
elif message_type == "answer" or message_type == "final-answer":
return AgentAnswer(
content=resp.get("content", ""),
end_of_message=resp.get("end_of_message", False),
end_of_dialog=resp.get("end_of_dialog", False),
message_id=resp.get("message_id", ""),
in_token=resp.get("in_token"),
out_token=resp.get("out_token"),
model=resp.get("model"),
)
elif chunk_type == "action":
elif message_type == "action":
return AgentThought(
content=resp.get("content", ""),
end_of_message=resp.get("end_of_message", False)
@ -404,7 +399,10 @@ class SocketClient:
return RAGChunk(
content=content,
end_of_stream=resp.get("end_of_stream", False),
error=None
error=None,
in_token=resp.get("in_token"),
out_token=resp.get("out_token"),
model=resp.get("model"),
)
def _build_provenance_event(self, resp: Dict[str, Any]) -> ProvenanceEvent:
@ -543,8 +541,12 @@ class SocketFlowInstance:
streaming=True, include_provenance=True
)
def text_completion(self, system: str, prompt: str, streaming: bool = False, **kwargs) -> Union[str, Iterator[str]]:
"""Execute text completion with optional streaming."""
def text_completion(self, system: str, prompt: str, streaming: bool = False, **kwargs) -> Union[TextCompletionResult, Iterator[RAGChunk]]:
"""Execute text completion with optional streaming.
Non-streaming: returns a TextCompletionResult with text and token counts.
Streaming: returns an iterator of RAGChunk (with token counts on the final chunk).
"""
request = {
"system": system,
"prompt": prompt,
@ -557,12 +559,17 @@ class SocketFlowInstance:
if streaming:
return self._text_completion_generator(result)
else:
return result.get("response", "")
return TextCompletionResult(
text=result.get("response", ""),
in_token=result.get("in_token"),
out_token=result.get("out_token"),
model=result.get("model"),
)
def _text_completion_generator(self, result: Iterator[StreamingChunk]) -> Iterator[str]:
def _text_completion_generator(self, result: Iterator[StreamingChunk]) -> Iterator[RAGChunk]:
for chunk in result:
if hasattr(chunk, 'content'):
yield chunk.content
if isinstance(chunk, RAGChunk):
yield chunk
def graph_rag(
self,
@ -577,8 +584,12 @@ class SocketFlowInstance:
edge_limit: int = 25,
streaming: bool = False,
**kwargs: Any
) -> Union[str, Iterator[str]]:
"""Execute graph-based RAG query with optional streaming."""
) -> Union[TextCompletionResult, Iterator[RAGChunk]]:
"""Execute graph-based RAG query with optional streaming.
Non-streaming: returns a TextCompletionResult with text and token counts.
Streaming: returns an iterator of RAGChunk (with token counts on the final chunk).
"""
request = {
"query": query,
"user": user,
@ -598,7 +609,12 @@ class SocketFlowInstance:
if streaming:
return self._rag_generator(result)
else:
return result.get("response", "")
return TextCompletionResult(
text=result.get("response", ""),
in_token=result.get("in_token"),
out_token=result.get("out_token"),
model=result.get("model"),
)
def graph_rag_explain(
self,
@ -642,8 +658,12 @@ class SocketFlowInstance:
doc_limit: int = 10,
streaming: bool = False,
**kwargs: Any
) -> Union[str, Iterator[str]]:
"""Execute document-based RAG query with optional streaming."""
) -> Union[TextCompletionResult, Iterator[RAGChunk]]:
"""Execute document-based RAG query with optional streaming.
Non-streaming: returns a TextCompletionResult with text and token counts.
Streaming: returns an iterator of RAGChunk (with token counts on the final chunk).
"""
request = {
"query": query,
"user": user,
@ -658,7 +678,12 @@ class SocketFlowInstance:
if streaming:
return self._rag_generator(result)
else:
return result.get("response", "")
return TextCompletionResult(
text=result.get("response", ""),
in_token=result.get("in_token"),
out_token=result.get("out_token"),
model=result.get("model"),
)
def document_rag_explain(
self,
@ -684,10 +709,10 @@ class SocketFlowInstance:
streaming=True, include_provenance=True
)
def _rag_generator(self, result: Iterator[StreamingChunk]) -> Iterator[str]:
def _rag_generator(self, result: Iterator[StreamingChunk]) -> Iterator[RAGChunk]:
for chunk in result:
if hasattr(chunk, 'content'):
yield chunk.content
if isinstance(chunk, RAGChunk):
yield chunk
def prompt(
self,
@ -695,8 +720,12 @@ class SocketFlowInstance:
variables: Dict[str, str],
streaming: bool = False,
**kwargs: Any
) -> Union[str, Iterator[str]]:
"""Execute a prompt template with optional streaming."""
) -> Union[TextCompletionResult, Iterator[RAGChunk]]:
"""Execute a prompt template with optional streaming.
Non-streaming: returns a TextCompletionResult with text and token counts.
Streaming: returns an iterator of RAGChunk (with token counts on the final chunk).
"""
request = {
"id": id,
"variables": variables,
@ -709,7 +738,12 @@ class SocketFlowInstance:
if streaming:
return self._rag_generator(result)
else:
return result.get("response", "")
return TextCompletionResult(
text=result.get("text", result.get("response", "")),
in_token=result.get("in_token"),
out_token=result.get("out_token"),
model=result.get("model"),
)
def graph_embeddings_query(
self,

View file

@ -149,10 +149,10 @@ class AgentThought(StreamingChunk):
Attributes:
content: Agent's thought text
end_of_message: True if this completes the current thought
chunk_type: Always "thought"
message_type: Always "thought"
message_id: Provenance URI of the entity being built
"""
chunk_type: str = "thought"
message_type: str = "thought"
message_id: str = ""
@dataclasses.dataclass
@ -166,10 +166,10 @@ class AgentObservation(StreamingChunk):
Attributes:
content: Observation text describing tool results
end_of_message: True if this completes the current observation
chunk_type: Always "observation"
message_type: Always "observation"
message_id: Provenance URI of the entity being built
"""
chunk_type: str = "observation"
message_type: str = "observation"
message_id: str = ""
@dataclasses.dataclass
@ -184,11 +184,14 @@ class AgentAnswer(StreamingChunk):
content: Answer text
end_of_message: True if this completes the current answer segment
end_of_dialog: True if this completes the entire agent interaction
chunk_type: Always "final-answer"
message_type: Always "final-answer"
"""
chunk_type: str = "final-answer"
message_type: str = "final-answer"
end_of_dialog: bool = False
message_id: str = ""
in_token: Optional[int] = None
out_token: Optional[int] = None
model: Optional[str] = None
@dataclasses.dataclass
class RAGChunk(StreamingChunk):
@ -202,11 +205,37 @@ class RAGChunk(StreamingChunk):
content: Generated text content
end_of_stream: True if this is the final chunk of the stream
error: Optional error information if an error occurred
chunk_type: Always "rag"
in_token: Input token count (populated on the final chunk, 0 otherwise)
out_token: Output token count (populated on the final chunk, 0 otherwise)
model: Model identifier (populated on the final chunk, empty otherwise)
message_type: Always "rag"
"""
chunk_type: str = "rag"
message_type: str = "rag"
end_of_stream: bool = False
error: Optional[Dict[str, str]] = None
in_token: Optional[int] = None
out_token: Optional[int] = None
model: Optional[str] = None
@dataclasses.dataclass
class TextCompletionResult:
"""
Result from a text completion request.
Returned by text_completion() in both streaming and non-streaming modes.
In streaming mode, text is None (chunks are delivered via the iterator).
In non-streaming mode, text contains the complete response.
Attributes:
text: Complete response text (None in streaming mode)
in_token: Input token count (None if not available)
out_token: Output token count (None if not available)
model: Model identifier (None if not available)
"""
text: Optional[str]
in_token: Optional[int] = None
out_token: Optional[int] = None
model: Optional[str] = None
@dataclasses.dataclass
class ProvenanceEvent:

View file

@ -18,8 +18,10 @@ from . librarian_client import LibrarianClient
from . chunking_service import ChunkingService
from . embeddings_service import EmbeddingsService
from . embeddings_client import EmbeddingsClientSpec
from . text_completion_client import TextCompletionClientSpec
from . prompt_client import PromptClientSpec
from . text_completion_client import (
TextCompletionClientSpec, TextCompletionClient, TextCompletionResult,
)
from . prompt_client import PromptClientSpec, PromptClient, PromptResult
from . triples_store_service import TriplesStoreService
from . graph_embeddings_store_service import GraphEmbeddingsStoreService
from . document_embeddings_store_service import DocumentEmbeddingsStoreService

View file

@ -30,19 +30,19 @@ class AgentClient(RequestResponse):
raise RuntimeError(resp.error.message)
# Handle thought chunks
if resp.chunk_type == 'thought':
if resp.message_type == 'thought':
if think:
await think(resp.content, resp.end_of_message)
return False # Continue receiving
# Handle observation chunks
if resp.chunk_type == 'observation':
if resp.message_type == 'observation':
if observe:
await observe(resp.content, resp.end_of_message)
return False # Continue receiving
# Handle answer chunks
if resp.chunk_type == 'answer':
if resp.message_type == 'answer':
if resp.content:
accumulated_answer.append(resp.content)
if answer_callback:

View file

@ -58,6 +58,18 @@ class BackendProducer(Protocol):
class BackendConsumer(Protocol):
"""Protocol for backend-specific consumer."""
def ensure_connected(self) -> None:
"""
Eagerly establish the underlying connection and bind the queue.
Backends that lazily connect on first receive() must implement this
so that callers can guarantee the consumer is fully bound and
therefore able to receive responses before any related request is
published. Backends that connect at construction time may make this
a no-op.
"""
...
def receive(self, timeout_millis: int = 2000) -> Message:
"""
Receive a message from the topic.

View file

@ -88,14 +88,14 @@ class ChunkingService(FlowProcessor):
chunk_overlap = default_chunk_overlap
try:
cs = flow.parameters.get("chunk-size")
cs = flow("chunk-size")
if cs is not None:
chunk_size = int(cs)
except Exception as e:
logger.warning(f"Could not parse chunk-size parameter: {e}")
try:
co = flow.parameters.get("chunk-overlap")
co = flow("chunk-overlap")
if co is not None:
chunk_overlap = int(co)
except Exception as e:

View file

@ -2,6 +2,13 @@
import asyncio
class Flow:
"""
Runtime representation of a deployed flow process.
This class maintains internal processor states and orchestrates
lifecycles (start, stop) for inputs (consumers) and parameters
that drive data flowing across linked nodes.
"""
def __init__(self, id, flow, processor, defn):
self.id = id

View file

@ -45,6 +45,12 @@ class LlmChunk:
__slots__ = ["text", "in_token", "out_token", "model", "is_final"]
class LlmService(FlowProcessor):
"""
Extensible service processing requests to Large Language Models (LLMs).
This class handles the core logic of dispatching text completion or chat requests
to integrated underlying LLM providers (e.g. OpenAI, vertex ai).
"""
def __init__(self, **params):

View file

@ -8,6 +8,7 @@ ensuring consistent log formats, levels, and command-line arguments.
Supports dual output to console and Loki for centralized log aggregation.
"""
import contextvars
import logging
import logging.handlers
from argparse import ArgumentParser
@ -16,6 +17,44 @@ from typing import Any
import os
# The current processor id for this task context. Read by
# _ProcessorIdFilter to stamp every LogRecord with its owning
# processor, and read by logging_loki's emitter via record.tags
# to label log lines in Loki. ContextVar so asyncio subtasks
# inherit their parent supervisor's processor id automatically.
current_processor_id = contextvars.ContextVar(
"current_processor_id", default="unknown"
)
def set_processor_id(pid):
"""Set the processor id for the current task context.
All subsequent log records emitted from this task and any
asyncio tasks spawned from it will be tagged with this id
in the console format and in Loki labels.
"""
current_processor_id.set(pid)
class _ProcessorIdFilter(logging.Filter):
"""Stamps every LogRecord with processor_id from the contextvar.
Attaches two fields to each record:
record.processor_id used by the console format string
record.tags merged into Loki labels by logging_loki's
emitter (it reads record.tags and combines
with the handler's static tags)
"""
def filter(self, record):
pid = current_processor_id.get()
record.processor_id = pid
existing = getattr(record, "tags", None) or {}
record.tags = {**existing, "processor": pid}
return True
def add_logging_args(parser: ArgumentParser) -> None:
"""
Add standard logging arguments to an argument parser.
@ -89,12 +128,15 @@ def setup_logging(args: dict[str, Any]) -> None:
loki_url = args.get('loki_url', 'http://loki:3100/loki/api/v1/push')
loki_username = args.get('loki_username')
loki_password = args.get('loki_password')
processor_id = args.get('id') # Processor identity (e.g., "config-svc", "text-completion")
try:
from logging_loki import LokiHandler
# Create Loki handler with optional authentication and processor label
# Create Loki handler with optional authentication. The
# processor label is NOT baked in here — it's stamped onto
# each record by _ProcessorIdFilter reading the task-local
# contextvar, and logging_loki's emitter reads record.tags
# to build per-record Loki labels.
loki_handler_kwargs = {
'url': loki_url,
'version': "1",
@ -103,10 +145,6 @@ def setup_logging(args: dict[str, Any]) -> None:
if loki_username and loki_password:
loki_handler_kwargs['auth'] = (loki_username, loki_password)
# Add processor label if available (for consistency with Prometheus metrics)
if processor_id:
loki_handler_kwargs['tags'] = {'processor': processor_id}
loki_handler = LokiHandler(**loki_handler_kwargs)
# Wrap in QueueHandler for non-blocking operation
@ -135,23 +173,44 @@ def setup_logging(args: dict[str, Any]) -> None:
print(f"WARNING: Failed to setup Loki logging: {e}")
print("Continuing with console-only logging")
# Get processor ID for log formatting (use 'unknown' if not available)
processor_id = args.get('id', 'unknown')
# Configure logging with all handlers
# Use processor ID as the primary identifier in logs
# Configure logging with all handlers. The processor id comes
# from _ProcessorIdFilter (via contextvar) and is injected into
# each record as record.processor_id. The format string reads
# that attribute on every emit.
logging.basicConfig(
level=getattr(logging, log_level.upper()),
format=f'%(asctime)s - {processor_id} - %(levelname)s - %(message)s',
format='%(asctime)s - %(processor_id)s - %(levelname)s - %(message)s',
handlers=handlers,
force=True # Force reconfiguration if already configured
)
# Prevent recursive logging from Loki's HTTP client
if loki_enabled and queue_listener:
# Disable urllib3 logging to prevent infinite loop
logging.getLogger('urllib3').setLevel(logging.WARNING)
logging.getLogger('urllib3.connectionpool').setLevel(logging.WARNING)
# Attach the processor-id filter to every handler so all records
# passing through any sink get stamped (console, queue→loki,
# future handlers). Filters on handlers run regardless of which
# logger originated the record, so logs from pika, cassandra,
# processor code, etc. all pass through it.
processor_filter = _ProcessorIdFilter()
for h in handlers:
h.addFilter(processor_filter)
# Seed the contextvar from --id if one was supplied. In group
# mode --id isn't present; the processor_group supervisor sets
# it per task. In standalone mode AsyncProcessor.launch provides
# it via argparse default.
if args.get('id'):
set_processor_id(args['id'])
# Silence noisy third-party library loggers. These emit INFO-level
# chatter (connection churn, channel open/close, driver warnings) that
# drowns the useful signal and can't be attributed to a specific
# processor anyway. WARNING and above still propagate.
for noisy in (
'pika',
'cassandra',
'urllib3',
'urllib3.connectionpool',
):
logging.getLogger(noisy).setLevel(logging.WARNING)
logger = logging.getLogger(__name__)
logger.info(f"Logging configured with level: {log_level}")

View file

@ -6,6 +6,12 @@ from prometheus_client import start_http_server, Info, Enum, Histogram
from prometheus_client import Counter
class ConsumerMetrics:
"""
Metrics tracking and reporting for flow consumers.
This class manages prometheus metrics specifically related to consumers
within the flow, including state, requests, processing time, and queues.
"""
def __init__(self, processor: str, flow: str, name: str) -> None:

View file

@ -7,9 +7,9 @@ from . spec import Spec
class Parameter:
def __init__(self, value: Any) -> None:
self.value = value
async def start() -> None:
async def start(self) -> None:
pass
async def stop() -> None:
async def stop(self) -> None:
pass
class ParameterSpec(Spec):

View file

@ -0,0 +1,204 @@
# Multi-processor group runner. Runs multiple AsyncProcessor descendants
# as concurrent tasks inside a single process, sharing one event loop,
# one Prometheus HTTP server, and one pub/sub backend pool.
#
# Intended for dev and resource-constrained deployments. Scale deployments
# should continue to use per-processor endpoints.
#
# Group config is a YAML or JSON file with shape:
#
# processors:
# - class: trustgraph.extract.kg.definitions.extract.Processor
# params:
# id: kg-extract-definitions
# triples_batch_size: 1000
# - class: trustgraph.chunking.recursive.Processor
# params:
# id: chunker-recursive
#
# Each entry's params are passed directly to the class constructor alongside
# the shared taskgroup. Defaults live inside each processor class.
import argparse
import asyncio
import importlib
import json
import logging
import time
from prometheus_client import start_http_server
from . logging import add_logging_args, setup_logging, set_processor_id
logger = logging.getLogger(__name__)
def _load_config(path):
with open(path) as f:
text = f.read()
if path.endswith((".yaml", ".yml")):
import yaml
return yaml.safe_load(text)
return json.loads(text)
def _resolve_class(dotted):
module_path, _, class_name = dotted.rpartition(".")
if not module_path:
raise ValueError(
f"Processor class must be a dotted path, got {dotted!r}"
)
module = importlib.import_module(module_path)
return getattr(module, class_name)
RESTART_DELAY_SECONDS = 4
async def _supervise(entry):
"""Run one processor with its own nested TaskGroup, restarting on any
failure. Each processor is isolated from its siblings a crash here
does not propagate to the outer group."""
pid = entry["params"]["id"]
class_path = entry["class"]
# Stamp the contextvar for this supervisor task. Every log
# record emitted from this task — and from any inner TaskGroup
# child created by the processor — inherits this id via
# contextvar propagation. Siblings in the outer group set
# their own id in their own task context and do not interfere.
set_processor_id(pid)
while True:
try:
async with asyncio.TaskGroup() as inner_tg:
cls = _resolve_class(class_path)
params = dict(entry.get("params", {}))
params["taskgroup"] = inner_tg
logger.info(f"Starting {class_path} as {pid}")
p = cls(**params)
await p.start()
inner_tg.create_task(p.run())
# Clean exit — processor's run() returned without raising.
# Treat as a transient shutdown and restart, matching the
# behaviour of per-container `restart: on-failure`.
logger.warning(
f"Processor {pid} exited cleanly, will restart"
)
except asyncio.CancelledError:
logger.info(f"Processor {pid} cancelled")
raise
except BaseExceptionGroup as eg:
for e in eg.exceptions:
logger.error(
f"Processor {pid} failure: {type(e).__name__}: {e}",
exc_info=e,
)
except Exception as e:
logger.error(
f"Processor {pid} failure: {type(e).__name__}: {e}",
exc_info=True,
)
logger.info(
f"Restarting {pid} in {RESTART_DELAY_SECONDS}s..."
)
await asyncio.sleep(RESTART_DELAY_SECONDS)
async def run_group(config):
entries = config.get("processors", [])
if not entries:
raise RuntimeError("Group config has no processors")
seen_ids = set()
for entry in entries:
pid = entry.get("params", {}).get("id")
if pid is None:
raise RuntimeError(
f"Entry {entry.get('class')!r} missing params.id — "
f"required for metrics labelling"
)
if pid in seen_ids:
raise RuntimeError(f"Duplicate processor id {pid!r} in group")
seen_ids.add(pid)
async with asyncio.TaskGroup() as outer_tg:
for entry in entries:
outer_tg.create_task(_supervise(entry))
def run():
parser = argparse.ArgumentParser(
prog="processor-group",
description="Run multiple processors as tasks in one process",
)
parser.add_argument(
"-c", "--config",
required=True,
help="Path to group config file (JSON or YAML)",
)
parser.add_argument(
"--metrics",
action=argparse.BooleanOptionalAction,
default=True,
help="Metrics enabled (default: true)",
)
parser.add_argument(
"-P", "--metrics-port",
type=int,
default=8000,
help="Prometheus metrics port (default: 8000)",
)
add_logging_args(parser)
args = vars(parser.parse_args())
setup_logging(args)
config = _load_config(args["config"])
if args["metrics"]:
start_http_server(args["metrics_port"])
while True:
logger.info("Starting group...")
try:
asyncio.run(run_group(config))
except KeyboardInterrupt:
logger.info("Keyboard interrupt.")
return
except ExceptionGroup as e:
logger.error("Exception group:")
for se in e.exceptions:
logger.error(f" Type: {type(se)}")
logger.error(f" Exception: {se}", exc_info=se)
except Exception as e:
logger.error(f"Type: {type(e)}")
logger.error(f"Exception: {e}", exc_info=True)
logger.warning("Will retry...")
time.sleep(4)
logger.info("Retrying...")

View file

@ -1,10 +1,22 @@
import json
import asyncio
from dataclasses import dataclass
from typing import Optional, Any
from . request_response_spec import RequestResponse, RequestResponseSpec
from .. schema import PromptRequest, PromptResponse
@dataclass
class PromptResult:
response_type: str # "text", "json", or "jsonl"
text: Optional[str] = None # populated for "text"
object: Any = None # populated for "json"
objects: Optional[list] = None # populated for "jsonl"
in_token: Optional[int] = None
out_token: Optional[int] = None
model: Optional[str] = None
class PromptClient(RequestResponse):
async def prompt(self, id, variables, timeout=600, streaming=False, chunk_callback=None):
@ -26,17 +38,40 @@ class PromptClient(RequestResponse):
if resp.error:
raise RuntimeError(resp.error.message)
if resp.text: return resp.text
if resp.text:
return PromptResult(
response_type="text",
text=resp.text,
in_token=resp.in_token,
out_token=resp.out_token,
model=resp.model,
)
return json.loads(resp.object)
parsed = json.loads(resp.object)
if isinstance(parsed, list):
return PromptResult(
response_type="jsonl",
objects=parsed,
in_token=resp.in_token,
out_token=resp.out_token,
model=resp.model,
)
return PromptResult(
response_type="json",
object=parsed,
in_token=resp.in_token,
out_token=resp.out_token,
model=resp.model,
)
else:
last_text = ""
last_object = None
last_resp = None
async def forward_chunks(resp):
nonlocal last_text, last_object
nonlocal last_resp
if resp.error:
raise RuntimeError(resp.error.message)
@ -44,14 +79,13 @@ class PromptClient(RequestResponse):
end_stream = getattr(resp, 'end_of_stream', False)
if resp.text is not None:
last_text = resp.text
if chunk_callback:
if asyncio.iscoroutinefunction(chunk_callback):
await chunk_callback(resp.text, end_stream)
else:
chunk_callback(resp.text, end_stream)
elif resp.object:
last_object = resp.object
last_resp = resp
return end_stream
@ -70,10 +104,36 @@ class PromptClient(RequestResponse):
timeout=timeout
)
if last_text:
return last_text
if last_resp is None:
return PromptResult(response_type="text")
return json.loads(last_object) if last_object else None
if last_resp.object:
parsed = json.loads(last_resp.object)
if isinstance(parsed, list):
return PromptResult(
response_type="jsonl",
objects=parsed,
in_token=last_resp.in_token,
out_token=last_resp.out_token,
model=last_resp.model,
)
return PromptResult(
response_type="json",
object=parsed,
in_token=last_resp.in_token,
out_token=last_resp.out_token,
model=last_resp.model,
)
return PromptResult(
response_type="text",
text=last_resp.text,
in_token=last_resp.in_token,
out_token=last_resp.out_token,
model=last_resp.model,
)
async def extract_definitions(self, text, timeout=600):
return await self.prompt(
@ -152,4 +212,3 @@ class PromptClientSpec(RequestResponseSpec):
response_schema = PromptResponse,
impl = PromptClient,
)

View file

@ -72,6 +72,16 @@ class PulsarBackendConsumer:
self._consumer = pulsar_consumer
self._schema_cls = schema_cls
def ensure_connected(self) -> None:
"""No-op for Pulsar.
PulsarBackend.create_consumer() calls client.subscribe() which is
synchronous and returns a fully-subscribed consumer, so the
consumer is already ready by the time this object is constructed.
Defined for parity with the BackendConsumer protocol used by
Subscriber.start()'s readiness barrier."""
pass
def receive(self, timeout_millis: int = 2000) -> Message:
"""Receive a message. Raises TimeoutError if no message available."""
try:

View file

@ -214,16 +214,43 @@ class RabbitMQBackendConsumer:
and self._channel.is_open
)
def ensure_connected(self) -> None:
"""Eagerly declare and bind the queue.
Without this, the queue is only declared lazily on the first
receive() call. For request/response with ephemeral per-subscriber
response queues that is a race: a request published before the
response queue is bound will have its reply silently dropped by
the broker. Subscriber.start() calls this so callers get a hard
readiness barrier."""
if not self._is_alive():
self._connect()
def receive(self, timeout_millis: int = 2000) -> Message:
"""Receive a message. Raises TimeoutError if none available."""
"""Receive a message. Raises TimeoutError if none available.
Loop ordering matters: check _incoming at the TOP of each
iteration, not as the loop condition. process_data_events
may dispatch a message via the _on_message callback during
the pump; we must re-check _incoming on the next iteration
before giving up on the deadline. The previous control
flow (`while deadline: check; pump`) could lose a wakeup if
the pump consumed the remainder of the window the
`while` check would fail before `_incoming` was re-read,
leaving a just-dispatched message stranded until the next
receive() call one full poll cycle later.
"""
if not self._is_alive():
self._connect()
timeout_seconds = timeout_millis / 1000.0
deadline = time.monotonic() + timeout_seconds
while time.monotonic() < deadline:
# Check if a message was already delivered
while True:
# Check if a message has been dispatched to our queue.
# This catches both (a) messages dispatched before this
# receive() was called and (b) messages dispatched
# during the previous iteration's process_data_events.
try:
method, properties, body = self._incoming.get_nowait()
return RabbitMQMessage(
@ -232,14 +259,16 @@ class RabbitMQBackendConsumer:
except queue.Empty:
pass
# Drive pika's I/O — delivers messages and processes heartbeats
remaining = deadline - time.monotonic()
if remaining > 0:
self._connection.process_data_events(
time_limit=min(0.1, remaining),
)
if remaining <= 0:
raise TimeoutError("No message received within timeout")
raise TimeoutError("No message received within timeout")
# Drive pika's I/O. Any messages delivered during this
# call land in _incoming via _on_message; the next
# iteration of this loop catches them at the top.
self._connection.process_data_events(
time_limit=min(0.1, remaining),
)
def acknowledge(self, message: Message) -> None:
if isinstance(message, RabbitMQMessage) and message._method:

View file

@ -41,14 +41,55 @@ class Subscriber:
self.consumer = None
self.executor = None
# Readiness barrier — completed by run() once the underlying
# backend consumer is fully connected and bound. start() awaits
# this so callers know any subsequently published request will
# have a queue ready to receive its response. Without this,
# ephemeral per-subscriber response queues (RabbitMQ auto-delete
# exclusive queues) would race the request and lose the reply.
# A Future is used (rather than an Event) so that a first-attempt
# connection failure can be propagated to start() as an exception.
self._ready = None # created in start() so we have a running loop
def __del__(self):
self.running = False
async def start(self):
self._ready = asyncio.get_event_loop().create_future()
self.task = asyncio.create_task(self.run())
# Block until run() signals readiness OR exits. The future
# carries the outcome of the first connect attempt: a value on
# success, an exception on first-attempt failure. If run() exits
# without ever signalling (e.g. cancelled, or a code path bug),
# we surface that as a clear RuntimeError rather than hanging
# forever waiting on the future.
ready_wait = asyncio.ensure_future(
asyncio.shield(self._ready)
)
try:
await asyncio.wait(
{self.task, ready_wait},
return_when=asyncio.FIRST_COMPLETED,
)
finally:
ready_wait.cancel()
if self._ready.done():
# Re-raise first-attempt connect failure if any.
self._ready.result()
return
# run() exited before _ready was settled. Propagate its exception
# if it had one, otherwise raise a generic readiness error.
if self.task.done() and self.task.exception() is not None:
raise self.task.exception()
raise RuntimeError(
"Subscriber.run() exited before signalling readiness"
)
async def stop(self):
"""Initiate graceful shutdown with draining"""
self.running = False
@ -66,6 +107,7 @@ class Subscriber:
async def run(self):
"""Enhanced run method with integrated draining logic"""
first_attempt = True
while self.running or self.draining:
if self.metrics:
@ -87,10 +129,27 @@ class Subscriber:
),
)
# Eagerly bind the queue. For backends that connect
# lazily on first receive (RabbitMQ), this is what
# closes the request/response setup race — without
# it the response queue is not bound until later and
# any reply published in the meantime is dropped.
await loop.run_in_executor(
self.executor,
lambda: self.consumer.ensure_connected(),
)
if self.metrics:
self.metrics.state("running")
logger.info("Subscriber running...")
# Signal start() that the consumer is ready. This must
# happen AFTER ensure_connected() above so callers can
# safely publish requests immediately after start() returns.
if first_attempt and not self._ready.done():
self._ready.set_result(None)
first_attempt = False
drain_end_time = None
while self.running or self.draining:
@ -162,6 +221,16 @@ class Subscriber:
except Exception as e:
logger.error(f"Subscriber exception: {e}", exc_info=True)
# First-attempt connection failure: propagate to start()
# so the caller can decide what to do (retry, give up).
# Subsequent failures use the existing retry-with-backoff
# path so a long-lived subscriber survives broker blips.
if first_attempt and not self._ready.done():
self._ready.set_exception(e)
first_attempt = False
# Falls through into finally for cleanup, then the
# outer return below ends run() so start() unblocks.
finally:
# Negative acknowledge any pending messages
for msg in self.pending_acks.values():
@ -193,6 +262,11 @@ class Subscriber:
if not self.running and not self.draining:
return
# If start() has already returned with an exception there is
# nothing more to do — exit run() rather than busy-retry.
if self._ready.done() and self._ready.exception() is not None:
return
# Sleep before retry
await asyncio.sleep(1)

View file

@ -1,47 +1,71 @@
from dataclasses import dataclass
from typing import Optional
from . request_response_spec import RequestResponse, RequestResponseSpec
from .. schema import TextCompletionRequest, TextCompletionResponse
@dataclass
class TextCompletionResult:
text: Optional[str]
in_token: Optional[int] = None
out_token: Optional[int] = None
model: Optional[str] = None
class TextCompletionClient(RequestResponse):
async def text_completion(self, system, prompt, streaming=False, timeout=600):
# If not streaming, use original behavior
if not streaming:
resp = await self.request(
TextCompletionRequest(
system = system, prompt = prompt, streaming = False
),
timeout=timeout
)
if resp.error:
raise RuntimeError(resp.error.message)
async def text_completion(self, system, prompt, timeout=600):
return resp.response
# For streaming: collect all chunks and return complete response
full_response = ""
async def collect_chunks(resp):
nonlocal full_response
if resp.error:
raise RuntimeError(resp.error.message)
if resp.response:
full_response += resp.response
# Return True when end_of_stream is reached
return getattr(resp, 'end_of_stream', False)
await self.request(
resp = await self.request(
TextCompletionRequest(
system = system, prompt = prompt, streaming = True
system = system, prompt = prompt, streaming = False
),
recipient=collect_chunks,
timeout=timeout
)
return full_response
if resp.error:
raise RuntimeError(resp.error.message)
return TextCompletionResult(
text = resp.response,
in_token = resp.in_token,
out_token = resp.out_token,
model = resp.model,
)
async def text_completion_stream(
self, system, prompt, handler, timeout=600,
):
"""
Streaming text completion. `handler` is an async callable invoked
once per chunk with the chunk's TextCompletionResponse. Returns a
TextCompletionResult with text=None and token counts / model taken
from the end_of_stream message.
"""
async def on_chunk(resp):
if resp.error:
raise RuntimeError(resp.error.message)
await handler(resp)
return getattr(resp, "end_of_stream", False)
final = await self.request(
TextCompletionRequest(
system = system, prompt = prompt, streaming = True
),
recipient=on_chunk,
timeout=timeout,
)
return TextCompletionResult(
text = None,
in_token = final.in_token,
out_token = final.out_token,
model = final.model,
)
class TextCompletionClientSpec(RequestResponseSpec):
def __init__(
@ -54,4 +78,3 @@ class TextCompletionClientSpec(RequestResponseSpec):
response_schema = TextCompletionResponse,
impl = TextCompletionClient,
)

View file

@ -5,6 +5,13 @@ from . request_response_spec import RequestResponse, RequestResponseSpec
from .. schema import ToolRequest, ToolResponse
class ToolClient(RequestResponse):
"""
Client for invoking tools over the flow messaging fabric.
This class provides an interface to abstract away the messaging mechanics
and provides a direct awaitable mechanism for invoking tools and
getting their responses.
"""
async def invoke(self, name, parameters={}, timeout=600):

View file

@ -18,6 +18,12 @@ logger = logging.getLogger(__name__)
default_ident = "triples-write"
class TriplesStoreService(FlowProcessor):
"""
Component for maintaining the triples store.
This service acts as a processor in the flow that receives knowledge triples
and writes them persistently into an overarching graph database or equivalent backend.
"""
def __init__(self, **params):

View file

@ -58,23 +58,23 @@ class AgentClient(BaseClient):
def inspect(x):
# Handle errors
if x.chunk_type == 'error' or x.error:
if x.message_type == 'error' or x.error:
if error_callback:
error_callback(x.content or (x.error.message if x.error else ""))
# Continue to check end_of_dialog
# Handle thought chunks
elif x.chunk_type == 'thought':
elif x.message_type == 'thought':
if think:
think(x.content, x.end_of_message)
# Handle observation chunks
elif x.chunk_type == 'observation':
elif x.message_type == 'observation':
if observe:
observe(x.content, x.end_of_message)
# Handle answer chunks
elif x.chunk_type == 'answer':
elif x.message_type == 'answer':
if x.content:
accumulated_answer.append(x.content)
if answer_callback:

View file

@ -0,0 +1,156 @@
"""Minimal i18n support for TrustGraph.
This module intentionally stays lightweight:
- No runtime translation calls
- Translations are pre-generated and shipped as language packs
Consumers (CLI/API/Workbench) select a language code (e.g. "es") and
use `Translator.t(key, **kwargs)` to format localized strings.
"""
from __future__ import annotations
import json
from dataclasses import dataclass
from functools import lru_cache
from typing import Any, Dict, Mapping, Optional
import importlib.resources as importlib_resources
SUPPORTED_LANGUAGES: Mapping[str, str] = {
"en": "English",
"es": "Spanish",
"sw": "Swahili",
"pt": "Portuguese",
"tr": "Turkish",
"hi": "Hindi",
"he": "Hebrew",
"ar": "Arabic",
"zh-cn": "Chinese (simplified)",
"ru": "Russian",
}
_LANGUAGE_ALIASES: Mapping[str, str] = {
"zh": "zh-cn",
"zh-hans": "zh-cn",
"zh-hans-cn": "zh-cn",
"zh-cn": "zh-cn",
"zh_cn": "zh-cn",
}
def normalize_language(value: Optional[str]) -> str:
"""Normalize language inputs to our supported codes.
Accepts:
- Simple codes: "es"
- Region tags: "es-ES", "en-US"
- Accept-Language style: "es-ES,es;q=0.9,en;q=0.8"
Falls back to "en" when unknown.
"""
if not value:
return "en"
# Accept-Language: take first entry
token = value.split(",", 1)[0].strip()
if not token:
return "en"
token = token.replace("_", "-").lower()
# Exact alias mapping
if token in _LANGUAGE_ALIASES:
token = _LANGUAGE_ALIASES[token]
# Collapse common regional tags
if token.startswith("en-"):
token = "en"
elif token.startswith("es-"):
token = "es"
elif token.startswith("pt-"):
token = "pt"
elif token.startswith("tr-"):
token = "tr"
elif token.startswith("hi-"):
token = "hi"
elif token.startswith("he-"):
token = "he"
elif token.startswith("ar-"):
token = "ar"
elif token.startswith("sw-"):
token = "sw"
elif token.startswith("ru-"):
token = "ru"
elif token.startswith("zh-"):
token = "zh-cn"
# Otherwise use primary subtag
primary = token.split("-", 1)[0]
if primary in SUPPORTED_LANGUAGES:
return primary
if token in SUPPORTED_LANGUAGES:
return token
return "en"
# Returns a mutable object - caller must not mutate!
@lru_cache(maxsize=32)
def get_language_pack(language: str) -> Dict[str, str]:
"""Load the language pack for `language` from package resources."""
lang = normalize_language(language)
if lang not in SUPPORTED_LANGUAGES:
lang = "en"
try:
with importlib_resources.open_text(
"trustgraph.i18n.packs", f"{lang}.json", encoding="utf-8"
) as f:
data = json.load(f)
except FileNotFoundError:
data = {}
if not isinstance(data, dict):
return {}
# Ensure values are strings
out: Dict[str, str] = {}
for k, v in data.items():
if isinstance(k, str) and isinstance(v, str):
out[k] = v
return out
@dataclass(frozen=True)
class Translator:
language: str
def t(self, key: str, **kwargs: Any) -> str:
"""Translate `key` using the current language pack.
Falls back to English pack, then the key itself.
Supports `.format(**kwargs)` placeholder substitution.
"""
lang = normalize_language(self.language)
pack = get_language_pack(lang)
fallback = get_language_pack("en")
template = pack.get(key) or fallback.get(key) or key
if not kwargs:
return template
try:
return template.format(**kwargs)
except Exception:
# If formatting fails, return the untranslated template
return template
def get_translator(language: Optional[str]) -> Translator:
return Translator(language=normalize_language(language))

View file

@ -0,0 +1 @@
# Language packs live next to this module as JSON files.

View file

@ -0,0 +1,54 @@
{
"cli.verify_system_status.title": "التحقق من حالة نظام TrustGraph.",
"cli.verify_system_status.phase_1": "المرحلة الأولى: البنية التحتية.",
"cli.verify_system_status.phase_2": "المرحلة الثانية: الخدمات الأساسية.",
"cli.verify_system_status.phase_3": "المرحلة الثالثة: خدمات البيانات.",
"cli.verify_system_status.phase_4": "المرحلة الرابعة: واجهة المستخدم.",
"cli.verify_system_status.summary": "ملخص.",
"cli.verify_system_status.checking": "التحقق من {name}...",
"cli.verify_system_status.checking_attempt": "التحقق من {name}... (المحاولة {attempt}).",
"cli.verify_system_status.failed_timeout": "{name}: فشل (انتهت المهلة بعد {attempt} محاولة).",
"cli.verify_system_status.pulsar_not_responding": "Pulsar لا تستجيب - قد تفشل عمليات التحقق الأخرى.",
"cli.verify_system_status.checks_passed": "عمليات التحقق التي نجحت: {passed}/{total}.",
"cli.verify_system_status.checks_failed": "عمليات التحقق التي فشلت: {failed}/{total}.",
"cli.verify_system_status.total_time": "إجمالي الوقت: {elapsed}.",
"cli.verify_system_status.system_healthy": "النظام يعمل بشكل صحيح!",
"cli.verify_system_status.system_failing": "النظام لديه {failed} عملية تحقق فاشلة.",
"cli.verify_system_status.check_name.pulsar": "Pulsar.",
"cli.verify_system_status.check_name.api_gateway": "بوابة واجهة برمجة التطبيقات (API Gateway).",
"cli.verify_system_status.check_name.processors": "المعالجات (Processors).",
"cli.verify_system_status.check_name.flow_blueprints": "مخططات التدفق (Flow Blueprints).",
"cli.verify_system_status.check_name.flows": "التدفقات (Flows).",
"cli.verify_system_status.check_name.prompts": "المطالبات (Prompts).",
"cli.verify_system_status.check_name.library": "المكتبة (Library).",
"cli.verify_system_status.check_name.workbench_ui": "واجهة المستخدم الخاصة بـ Workbench.",
"cli.verify_system_status.pulsar.healthy": "Pulsar تعمل بشكل صحيح ({clusters} مجموعة).",
"cli.verify_system_status.pulsar.status": "Pulsar أرجعت الحالة {status_code}.",
"cli.verify_system_status.pulsar.timeout": "Pulsar: انتهاء المهلة.",
"cli.verify_system_status.pulsar.cannot_connect": "لا يمكن الاتصال بـ Pulsar.",
"cli.verify_system_status.pulsar.error": "Pulsar: خطأ: {error}.",
"cli.verify_system_status.api_gateway.responding": "بوابة واجهة برمجة التطبيقات (API Gateway) تستجيب.",
"cli.verify_system_status.api_gateway.status": "بوابة واجهة برمجة التطبيقات (API Gateway) أرجعت الحالة {status_code}.",
"cli.verify_system_status.api_gateway.timeout": "بوابة واجهة برمجة التطبيقات (API Gateway): انتهاء المهلة.",
"cli.verify_system_status.api_gateway.cannot_connect": "لا يمكن الاتصال بـ بوابة واجهة برمجة التطبيقات (API Gateway).",
"cli.verify_system_status.api_gateway.error": "بوابة واجهة برمجة التطبيقات (API Gateway): خطأ: {error}.",
"cli.verify_system_status.processors.found": "تم العثور على {count} معالج (≥ {min}).",
"cli.verify_system_status.processors.only": "فقط {count} معالج قيد التشغيل (مطلوب {min}).",
"cli.verify_system_status.processors.metrics_status": "المقاييس أرجعت الحالة {status_code}.",
"cli.verify_system_status.processors.error": "خطأ في فحص المعالج: {error}.",
"cli.verify_system_status.flow_blueprints.found": "تم العثور على {count} مخطط تدفق.",
"cli.verify_system_status.flow_blueprints.none": "لم يتم العثور على أي مخططات تدفق.",
"cli.verify_system_status.flow_blueprints.error": "خطأ في فحص مخططات التدفق: {error}.",
"cli.verify_system_status.flows.responding": "مدير التدفق يستجيب ({count} تدفق).",
"cli.verify_system_status.flows.error": "خطأ في فحص مدير التدفق: {error}.",
"cli.verify_system_status.prompts.found": "تم العثور على {count} مطالبة.",
"cli.verify_system_status.prompts.none": "لم يتم العثور على أي مطالبات.",
"cli.verify_system_status.prompts.error": "خطأ في فحص المطالبات: {error}.",
"cli.verify_system_status.library.responding": "المكتبة تستجيب ({count} مستند).",
"cli.verify_system_status.library.error": "خطأ في فحص المكتبة: {error}.",
"cli.verify_system_status.ui.responding": "واجهة المستخدم الخاصة بـ Workbench تستجيب.",
"cli.verify_system_status.ui.status": "واجهة المستخدم أرجعت الحالة {status_code}.",
"cli.verify_system_status.ui.timeout": "واجهة المستخدم: انتهاء المهلة.",
"cli.verify_system_status.ui.cannot_connect": "لا يمكن الاتصال بواجهة المستخدم.",
"cli.verify_system_status.ui.error": "واجهة المستخدم: خطأ: {error}."
}

View file

@ -0,0 +1,54 @@
{
"cli.verify_system_status.title": "TrustGraph System Status Verification",
"cli.verify_system_status.phase_1": "Phase 1: Infrastructure",
"cli.verify_system_status.phase_2": "Phase 2: Core Services",
"cli.verify_system_status.phase_3": "Phase 3: Data Services",
"cli.verify_system_status.phase_4": "Phase 4: User Interface",
"cli.verify_system_status.summary": "Summary",
"cli.verify_system_status.checking": "Checking {name}...",
"cli.verify_system_status.checking_attempt": "Checking {name}... (attempt {attempt})",
"cli.verify_system_status.failed_timeout": "{name}: Failed (timeout after {attempt} attempts)",
"cli.verify_system_status.pulsar_not_responding": "Pulsar is not responding - other checks may fail",
"cli.verify_system_status.checks_passed": "Checks passed: {passed}/{total}",
"cli.verify_system_status.checks_failed": "Checks failed: {failed}/{total}",
"cli.verify_system_status.total_time": "Total time: {elapsed}",
"cli.verify_system_status.system_healthy": "System is healthy!",
"cli.verify_system_status.system_failing": "System has {failed} failing check(s)",
"cli.verify_system_status.check_name.pulsar": "Pulsar",
"cli.verify_system_status.check_name.api_gateway": "API Gateway",
"cli.verify_system_status.check_name.processors": "Processors",
"cli.verify_system_status.check_name.flow_blueprints": "Flow Blueprints",
"cli.verify_system_status.check_name.flows": "Flows",
"cli.verify_system_status.check_name.prompts": "Prompts",
"cli.verify_system_status.check_name.library": "Library",
"cli.verify_system_status.check_name.workbench_ui": "Workbench UI",
"cli.verify_system_status.pulsar.healthy": "Pulsar healthy ({clusters} cluster(s))",
"cli.verify_system_status.pulsar.status": "Pulsar returned status {status_code}",
"cli.verify_system_status.pulsar.timeout": "Pulsar connection timeout",
"cli.verify_system_status.pulsar.cannot_connect": "Cannot connect to Pulsar",
"cli.verify_system_status.pulsar.error": "Pulsar error: {error}",
"cli.verify_system_status.api_gateway.responding": "API Gateway is responding",
"cli.verify_system_status.api_gateway.status": "API Gateway returned status {status_code}",
"cli.verify_system_status.api_gateway.timeout": "API Gateway connection timeout",
"cli.verify_system_status.api_gateway.cannot_connect": "Cannot connect to API Gateway",
"cli.verify_system_status.api_gateway.error": "API Gateway error: {error}",
"cli.verify_system_status.processors.found": "Found {count} processors (≥ {min})",
"cli.verify_system_status.processors.only": "Only {count} processors running (need {min})",
"cli.verify_system_status.processors.metrics_status": "Metrics returned status {status_code}",
"cli.verify_system_status.processors.error": "Processor check error: {error}",
"cli.verify_system_status.flow_blueprints.found": "Found {count} flow blueprint(s)",
"cli.verify_system_status.flow_blueprints.none": "No flow blueprints found",
"cli.verify_system_status.flow_blueprints.error": "Flow blueprints check error: {error}",
"cli.verify_system_status.flows.responding": "Flow manager responding ({count} flow(s))",
"cli.verify_system_status.flows.error": "Flow manager check error: {error}",
"cli.verify_system_status.prompts.found": "Found {count} prompt(s)",
"cli.verify_system_status.prompts.none": "No prompts found",
"cli.verify_system_status.prompts.error": "Prompts check error: {error}",
"cli.verify_system_status.library.responding": "Library responding ({count} document(s))",
"cli.verify_system_status.library.error": "Library check error: {error}",
"cli.verify_system_status.ui.responding": "Workbench UI is responding",
"cli.verify_system_status.ui.status": "UI returned status {status_code}",
"cli.verify_system_status.ui.timeout": "UI connection timeout",
"cli.verify_system_status.ui.cannot_connect": "Cannot connect to UI",
"cli.verify_system_status.ui.error": "UI error: {error}"
}

View file

@ -0,0 +1,54 @@
{
"cli.verify_system_status.title": "Verificación del estado del sistema TrustGraph",
"cli.verify_system_status.phase_1": "Fase 1: Infraestructura",
"cli.verify_system_status.phase_2": "Fase 2: Servicios principales",
"cli.verify_system_status.phase_3": "Fase 3: Servicios de datos",
"cli.verify_system_status.phase_4": "Fase 4: Interfaz de usuario",
"cli.verify_system_status.summary": "Resumen",
"cli.verify_system_status.checking": "Verificando {name}...",
"cli.verify_system_status.checking_attempt": "Verificando {name}... (intento {attempt})",
"cli.verify_system_status.failed_timeout": "{name}: Fallido (tiempo de espera después de {attempt} intentos)",
"cli.verify_system_status.pulsar_not_responding": "Pulsar no está respondiendo; otras verificaciones pueden fallar",
"cli.verify_system_status.checks_passed": "Verificaciones superadas: {passed}/{total}",
"cli.verify_system_status.checks_failed": "Verificaciones fallidas: {failed}/{total}",
"cli.verify_system_status.total_time": "Tiempo total: {elapsed}",
"cli.verify_system_status.system_healthy": "¡El sistema es saludable!",
"cli.verify_system_status.system_failing": "El sistema tiene {failed} verificación(es) fallida(s)",
"cli.verify_system_status.check_name.pulsar": "Pulsar",
"cli.verify_system_status.check_name.api_gateway": "API Gateway",
"cli.verify_system_status.check_name.processors": "Procesadores",
"cli.verify_system_status.check_name.flow_blueprints": "Plantillas de flujo",
"cli.verify_system_status.check_name.flows": "Flujos",
"cli.verify_system_status.check_name.prompts": "Indicaciones",
"cli.verify_system_status.check_name.library": "Biblioteca",
"cli.verify_system_status.check_name.workbench_ui": "Interfaz de usuario del entorno de trabajo",
"cli.verify_system_status.pulsar.healthy": "Pulsar saludable ({clusters} clúster(es))",
"cli.verify_system_status.pulsar.status": "Pulsar devolvió el estado {status_code}",
"cli.verify_system_status.pulsar.timeout": "Tiempo de espera de la conexión a Pulsar",
"cli.verify_system_status.pulsar.cannot_connect": "No se puede conectar a Pulsar",
"cli.verify_system_status.pulsar.error": "Error de Pulsar: {error}",
"cli.verify_system_status.api_gateway.responding": "API Gateway está respondiendo",
"cli.verify_system_status.api_gateway.status": "API Gateway devolvió el estado {status_code}",
"cli.verify_system_status.api_gateway.timeout": "Tiempo de espera de la conexión a API Gateway",
"cli.verify_system_status.api_gateway.cannot_connect": "No se puede conectar a API Gateway",
"cli.verify_system_status.api_gateway.error": "Error de API Gateway: {error}",
"cli.verify_system_status.processors.found": "Se encontraron {count} procesadores (≥ {min})",
"cli.verify_system_status.processors.only": "Solo {count} procesadores en ejecución (se necesitan {min})",
"cli.verify_system_status.processors.metrics_status": "Métricas devolvieron el estado {status_code}",
"cli.verify_system_status.processors.error": "Error de verificación del procesador: {error}",
"cli.verify_system_status.flow_blueprints.found": "Se encontraron {count} plantilla(s) de flujo",
"cli.verify_system_status.flow_blueprints.none": "No se encontraron plantillas de flujo",
"cli.verify_system_status.flow_blueprints.error": "Error de verificación de la plantilla de flujo: {error}",
"cli.verify_system_status.flows.responding": "El administrador de flujos está respondiendo ({count} flujo(s))",
"cli.verify_system_status.flows.error": "Error de verificación del administrador de flujos: {error}",
"cli.verify_system_status.prompts.found": "Se encontraron {count} indicación(es)",
"cli.verify_system_status.prompts.none": "No se encontraron indicaciones",
"cli.verify_system_status.prompts.error": "Error de verificación de la indicación: {error}",
"cli.verify_system_status.library.responding": "La biblioteca está respondiendo ({count} documento(s))",
"cli.verify_system_status.library.error": "Error de verificación de la biblioteca: {error}",
"cli.verify_system_status.ui.responding": "La interfaz de usuario del entorno de trabajo está respondiendo",
"cli.verify_system_status.ui.status": "La interfaz de usuario devolvió el estado {status_code}",
"cli.verify_system_status.ui.timeout": "Tiempo de espera de la conexión a la interfaz de usuario",
"cli.verify_system_status.ui.cannot_connect": "No se puede conectar a la interfaz de usuario",
"cli.verify_system_status.ui.error": "Error de la interfaz de usuario: {error}"
}

View file

@ -0,0 +1,54 @@
{
"cli.verify_system_status.title": "אימות סטטוס מערכת TrustGraph",
"cli.verify_system_status.phase_1": "שלב 1: תשתית",
"cli.verify_system_status.phase_2": "שלב 2: שירותים מרכזיים",
"cli.verify_system_status.phase_3": "שלב 3: שירותי נתונים",
"cli.verify_system_status.phase_4": "שלב 4: ממשק משתמש",
"cli.verify_system_status.summary": "סיכום",
"cli.verify_system_status.checking": "בדיקת {name}...",
"cli.verify_system_status.checking_attempt": "בדיקת {name}... (ניסיון {attempt})",
"cli.verify_system_status.failed_timeout": "{name}: נכשל (תפוגה לאחר {attempt} ניסיונות)",
"cli.verify_system_status.pulsar_not_responding": "Pulsar אינו מגיב - בדיקות אחרות עשויות להיכשל",
"cli.verify_system_status.checks_passed": "בדיקות שעברו: {passed}/{total}",
"cli.verify_system_status.checks_failed": "בדיקות שנכשלו: {failed}/{total}",
"cli.verify_system_status.total_time": "זמן כולל: {elapsed}",
"cli.verify_system_status.system_healthy": "המערכת תקינה!",
"cli.verify_system_status.system_failing": "למערכת יש {failed} בדיקה/ות שנכשלו",
"cli.verify_system_status.check_name.pulsar": "Pulsar",
"cli.verify_system_status.check_name.api_gateway": "API Gateway",
"cli.verify_system_status.check_name.processors": "מעבדים",
"cli.verify_system_status.check_name.flow_blueprints": "תבניות זרימה",
"cli.verify_system_status.check_name.flows": "זרימות",
"cli.verify_system_status.check_name.prompts": "הנחיות",
"cli.verify_system_status.check_name.library": "ספרייה",
"cli.verify_system_status.check_name.workbench_ui": "ממשק משתמש (Workbench UI)",
"cli.verify_system_status.pulsar.healthy": "Pulsar תקין ({clusters} אשכולות)",
"cli.verify_system_status.pulsar.status": "Pulsar החזיר סטטוס {status_code}",
"cli.verify_system_status.pulsar.timeout": "תפוגת זמן חיבור ל-Pulsar",
"cli.verify_system_status.pulsar.cannot_connect": "לא ניתן להתחבר ל-Pulsar",
"cli.verify_system_status.pulsar.error": "שגיאה ב-Pulsar: {error}",
"cli.verify_system_status.api_gateway.responding": "API Gateway מגיב",
"cli.verify_system_status.api_gateway.status": "API Gateway החזיר סטטוס {status_code}",
"cli.verify_system_status.api_gateway.timeout": "תפוגת זמן חיבור ל-API Gateway",
"cli.verify_system_status.api_gateway.cannot_connect": "לא ניתן להתחבר ל-API Gateway",
"cli.verify_system_status.api_gateway.error": "שגיאה ב-API Gateway: {error}",
"cli.verify_system_status.processors.found": "נמצאו {count} מעבדים (≥ {min})",
"cli.verify_system_status.processors.only": "פועלים רק {count} מעבדים (נדרשים {min})",
"cli.verify_system_status.processors.metrics_status": "Metrics החזירו סטטוס {status_code}",
"cli.verify_system_status.processors.error": "שגיאת בדיקת מעבד: {error}",
"cli.verify_system_status.flow_blueprints.found": "נמצאו {count} תבניות זרימה",
"cli.verify_system_status.flow_blueprints.none": "לא נמצאו תבניות זרימה",
"cli.verify_system_status.flow_blueprints.error": "שגיאת בדיקת תבניות זרימה: {error}",
"cli.verify_system_status.flows.responding": "מנהל הזרימה מגיב ({count} זרימות)",
"cli.verify_system_status.flows.error": "שגיאת בדיקת מנהל הזרימה: {error}",
"cli.verify_system_status.prompts.found": "נמצאו {count} הנחיות",
"cli.verify_system_status.prompts.none": "לא נמצאו הנחיות",
"cli.verify_system_status.prompts.error": "שגיאת בדיקת הנחיות: {error}",
"cli.verify_system_status.library.responding": "הספרייה מגיבה ({count} מסמכים)",
"cli.verify_system_status.library.error": "שגיאת בדיקת ספרייה: {error}",
"cli.verify_system_status.ui.responding": "ממשק המשתמש (Workbench UI) מגיב",
"cli.verify_system_status.ui.status": "ממשק המשתמש החזיר סטטוס {status_code}",
"cli.verify_system_status.ui.timeout": "תפוגת זמן חיבור לממשק המשתמש",
"cli.verify_system_status.ui.cannot_connect": "לא ניתן להתחבר לממשק המשתמש",
"cli.verify_system_status.ui.error": "שגיאה בממשק המשתמש: {error}"
}

View file

@ -0,0 +1,54 @@
{
"cli.verify_system_status.title": "ट्रस्टग्राफ सिस्टम स्टेटस वेरिफिकेशन",
"cli.verify_system_status.phase_1": "चरण 1: इंफ्रास्ट्रक्चर",
"cli.verify_system_status.phase_2": "चरण 2: कोर सर्विसेज",
"cli.verify_system_status.phase_3": "चरण 3: डेटा सर्विसेज",
"cli.verify_system_status.phase_4": "चरण 4: यूजर इंटरफेस",
"cli.verify_system_status.summary": "सारांश",
"cli.verify_system_status.checking": "{name} की जाँच की जा रही है...",
"cli.verify_system_status.checking_attempt": "{name} की जाँच की जा रही है... (प्रयास {attempt})",
"cli.verify_system_status.failed_timeout": "{name}: विफल ({attempt} प्रयासों के बाद टाइमआउट)",
"cli.verify_system_status.pulsar_not_responding": "पल्सर प्रतिक्रिया नहीं दे रहा है - अन्य जाँचें विफल हो सकती हैं",
"cli.verify_system_status.checks_passed": "पास हुई जाँचें: {passed}/{total}",
"cli.verify_system_status.checks_failed": "विफल हुई जाँचें: {failed}/{total}",
"cli.verify_system_status.total_time": "कुल समय: {elapsed}",
"cli.verify_system_status.system_healthy": "सिस्टम स्वस्थ है!",
"cli.verify_system_status.system_failing": "सिस्टम में {failed} विफल जाँच(एँ) हैं",
"cli.verify_system_status.check_name.pulsar": "पल्सर",
"cli.verify_system_status.check_name.api_gateway": "एपीआई गेटवे",
"cli.verify_system_status.check_name.processors": "प्रोसेसर",
"cli.verify_system_status.check_name.flow_blueprints": "फ्लो ब्लूप्रिंट्स",
"cli.verify_system_status.check_name.flows": "फ्लोस",
"cli.verify_system_status.check_name.prompts": "प्रॉम्प्ट्स",
"cli.verify_system_status.check_name.library": "लाइब्रेरी",
"cli.verify_system_status.check_name.workbench_ui": "वर्कबेंच यूआई",
"cli.verify_system_status.pulsar.healthy": "पल्सर स्वस्थ ({clusters} क्लस्टर(्स))",
"cli.verify_system_status.pulsar.status": "पल्सर ने स्टेटस {status_code} लौटाया",
"cli.verify_system_status.pulsar.timeout": "पल्सर कनेक्शन टाइमआउट",
"cli.verify_system_status.pulsar.cannot_connect": "पल्सर से कनेक्ट नहीं किया जा सका",
"cli.verify_system_status.pulsar.error": "पल्सर त्रुटि: {error}",
"cli.verify_system_status.api_gateway.responding": "एपीआई गेटवे प्रतिक्रिया दे रहा है",
"cli.verify_system_status.api_gateway.status": "एपीआई गेटवे ने स्टेटस {status_code} लौटाया",
"cli.verify_system_status.api_gateway.timeout": "एपीआई गेटवे कनेक्शन टाइमआउट",
"cli.verify_system_status.api_gateway.cannot_connect": "एपीआई गेटवे से कनेक्ट नहीं किया जा सका",
"cli.verify_system_status.api_gateway.error": "एपीआई गेटवे त्रुटि: {error}",
"cli.verify_system_status.processors.found": "{count} प्रोसेसर पाए गए (≥ {min})",
"cli.verify_system_status.processors.only": "केवल {count} प्रोसेसर चल रहे हैं (आवश्यक {min})",
"cli.verify_system_status.processors.metrics_status": "मेट्रिक्स ने स्टेटस {status_code} लौटाया",
"cli.verify_system_status.processors.error": "प्रोसेसर जाँच त्रुटि: {error}",
"cli.verify_system_status.flow_blueprints.found": "{count} फ्लो ब्लूप्रिंट पाए गए",
"cli.verify_system_status.flow_blueprints.none": "कोई फ्लो ब्लूप्रिंट नहीं मिला",
"cli.verify_system_status.flow_blueprints.error": "फ्लो ब्लूप्रिंट जाँच त्रुटि: {error}",
"cli.verify_system_status.flows.responding": "फ्लो मैनेजर प्रतिक्रिया दे रहा है ({count} फ्लो(्स))",
"cli.verify_system_status.flows.error": "फ्लो मैनेजर जाँच त्रुटि: {error}",
"cli.verify_system_status.prompts.found": "{count} प्रॉम्प्ट पाए गए",
"cli.verify_system_status.prompts.none": "कोई प्रॉम्प्ट नहीं मिला",
"cli.verify_system_status.prompts.error": "प्रॉम्प्ट जाँच त्रुटि: {error}",
"cli.verify_system_status.library.responding": "लाइब्रेरी प्रतिक्रिया दे रही है ({count} दस्तावेज़(्स))",
"cli.verify_system_status.library.error": "लाइब्रेरी जाँच त्रुटि: {error}",
"cli.verify_system_status.ui.responding": "वर्कबेंच यूआई प्रतिक्रिया दे रहा है",
"cli.verify_system_status.ui.status": "यूआई ने स्टेटस {status_code} लौटाया",
"cli.verify_system_status.ui.timeout": "यूआई कनेक्शन टाइमआउट",
"cli.verify_system_status.ui.cannot_connect": "यूआई से कनेक्ट नहीं किया जा सका",
"cli.verify_system_status.ui.error": "यूआई त्रुटि: {error}"
}

View file

@ -0,0 +1,54 @@
{
"cli.verify_system_status.title": "Verificação do Status do Sistema TrustGraph",
"cli.verify_system_status.phase_1": "Fase 1: Infraestrutura",
"cli.verify_system_status.phase_2": "Fase 2: Serviços Essenciais",
"cli.verify_system_status.phase_3": "Fase 3: Serviços de Dados",
"cli.verify_system_status.phase_4": "Fase 4: Interface do Usuário",
"cli.verify_system_status.summary": "Resumo",
"cli.verify_system_status.checking": "Verificando {name}...",
"cli.verify_system_status.checking_attempt": "Verificando {name}... (tentativa {attempt})",
"cli.verify_system_status.failed_timeout": "{name}: Falha (tempo limite após {attempt} tentativas)",
"cli.verify_system_status.pulsar_not_responding": "O Pulsar não está respondendo - outras verificações podem falhar",
"cli.verify_system_status.checks_passed": "Verificações aprovadas: {passed}/{total}",
"cli.verify_system_status.checks_failed": "Verificações falhadas: {failed}/{total}",
"cli.verify_system_status.total_time": "Tempo total: {elapsed}",
"cli.verify_system_status.system_healthy": "O sistema está saudável!",
"cli.verify_system_status.system_failing": "O sistema possui {failed} verificação(ões) com falha",
"cli.verify_system_status.check_name.pulsar": "Pulsar",
"cli.verify_system_status.check_name.api_gateway": "API Gateway",
"cli.verify_system_status.check_name.processors": "Processadores",
"cli.verify_system_status.check_name.flow_blueprints": "Flow Blueprints",
"cli.verify_system_status.check_name.flows": "Flows",
"cli.verify_system_status.check_name.prompts": "Prompts",
"cli.verify_system_status.check_name.library": "Biblioteca",
"cli.verify_system_status.check_name.workbench_ui": "Workbench UI",
"cli.verify_system_status.pulsar.healthy": "Pulsar saudável ({clusters} cluster(s))",
"cli.verify_system_status.pulsar.status": "O Pulsar retornou o status {status_code}",
"cli.verify_system_status.pulsar.timeout": "Tempo limite de conexão do Pulsar",
"cli.verify_system_status.pulsar.cannot_connect": "Não foi possível conectar ao Pulsar",
"cli.verify_system_status.pulsar.error": "Erro do Pulsar: {error}",
"cli.verify_system_status.api_gateway.responding": "O API Gateway está respondendo",
"cli.verify_system_status.api_gateway.status": "O API Gateway retornou o status {status_code}",
"cli.verify_system_status.api_gateway.timeout": "Tempo limite de conexão do API Gateway",
"cli.verify_system_status.api_gateway.cannot_connect": "Não foi possível conectar ao API Gateway",
"cli.verify_system_status.api_gateway.error": "Erro do API Gateway: {error}",
"cli.verify_system_status.processors.found": "Encontrados {count} processadores (≥ {min})",
"cli.verify_system_status.processors.only": "Apenas {count} processadores em execução (necessários {min})",
"cli.verify_system_status.processors.metrics_status": "As métricas retornaram o status {status_code}",
"cli.verify_system_status.processors.error": "Erro de verificação do processador: {error}",
"cli.verify_system_status.flow_blueprints.found": "Encontrados {count} flow blueprint(s)",
"cli.verify_system_status.flow_blueprints.none": "Nenhum flow blueprint encontrado",
"cli.verify_system_status.flow_blueprints.error": "Erro de verificação do flow blueprint: {error}",
"cli.verify_system_status.flows.responding": "O gerenciador de fluxo está respondendo ({count} flow(s))",
"cli.verify_system_status.flows.error": "Erro de verificação do gerenciador de fluxo: {error}",
"cli.verify_system_status.prompts.found": "Encontrados {count} prompt(s)",
"cli.verify_system_status.prompts.none": "Nenhum prompt encontrado",
"cli.verify_system_status.prompts.error": "Erro de verificação do prompt: {error}",
"cli.verify_system_status.library.responding": "A biblioteca está respondendo ({count} document(s))",
"cli.verify_system_status.library.error": "Erro de verificação da biblioteca: {error}",
"cli.verify_system_status.ui.responding": "O Workbench UI está respondendo",
"cli.verify_system_status.ui.status": "A UI retornou o status {status_code}",
"cli.verify_system_status.ui.timeout": "Tempo limite de conexão da UI",
"cli.verify_system_status.ui.cannot_connect": "Não foi possível conectar à UI",
"cli.verify_system_status.ui.error": "Erro da UI: {error}"
}

View file

@ -0,0 +1,54 @@
{
"cli.verify_system_status.title": "Проверка состояния системы TrustGraph",
"cli.verify_system_status.phase_1": "Фаза 1: Инфраструктура",
"cli.verify_system_status.phase_2": "Фаза 2: Основные сервисы",
"cli.verify_system_status.phase_3": "Фаза 3: Сервисы данных",
"cli.verify_system_status.phase_4": "Фаза 4: Пользовательский интерфейс",
"cli.verify_system_status.summary": "Краткое описание",
"cli.verify_system_status.checking": "Проверка {name}...",
"cli.verify_system_status.checking_attempt": "Проверка {name}... (попытка {attempt})",
"cli.verify_system_status.failed_timeout": "{name}: Не удалось (тайм-аут после {attempt} попыток)",
"cli.verify_system_status.pulsar_not_responding": "Pulsar не отвечает - другие проверки могут не пройти",
"cli.verify_system_status.checks_passed": "Проверено: {passed}/{total}",
"cli.verify_system_status.checks_failed": "Не удалось: {failed}/{total}",
"cli.verify_system_status.total_time": "Общее время: {elapsed}",
"cli.verify_system_status.system_healthy": "Система в рабочем состоянии!",
"cli.verify_system_status.system_failing": "В системе {failed} неисправностей.",
"cli.verify_system_status.check_name.pulsar": "Pulsar",
"cli.verify_system_status.check_name.api_gateway": "API Gateway",
"cli.verify_system_status.check_name.processors": "Процессоры",
"cli.verify_system_status.check_name.flow_blueprints": "Шаблоны потоков",
"cli.verify_system_status.check_name.flows": "Потоки",
"cli.verify_system_status.check_name.prompts": "Подсказки",
"cli.verify_system_status.check_name.library": "Библиотека",
"cli.verify_system_status.check_name.workbench_ui": "Пользовательский интерфейс Workbench",
"cli.verify_system_status.pulsar.healthy": "Pulsar работает ({clusters} кластер(ов))",
"cli.verify_system_status.pulsar.status": "Pulsar вернул статус {status_code}",
"cli.verify_system_status.pulsar.timeout": "Время ожидания соединения с Pulsar истекло",
"cli.verify_system_status.pulsar.cannot_connect": "Не удалось подключиться к Pulsar",
"cli.verify_system_status.pulsar.error": "Ошибка Pulsar: {error}",
"cli.verify_system_status.api_gateway.responding": "API Gateway отвечает",
"cli.verify_system_status.api_gateway.status": "API Gateway вернул статус {status_code}",
"cli.verify_system_status.api_gateway.timeout": "Время ожидания соединения с API Gateway истекло",
"cli.verify_system_status.api_gateway.cannot_connect": "Не удалось подключиться к API Gateway",
"cli.verify_system_status.api_gateway.error": "Ошибка API Gateway: {error}",
"cli.verify_system_status.processors.found": "Обнаружено {count} процессоров (≥ {min})",
"cli.verify_system_status.processors.only": "Работает только {count} процессоров (требуется {min})",
"cli.verify_system_status.processors.metrics_status": "Метрики вернули статус {status_code}",
"cli.verify_system_status.processors.error": "Ошибка проверки процессора: {error}",
"cli.verify_system_status.flow_blueprints.found": "Обнаружено {count} шаблонов потоков",
"cli.verify_system_status.flow_blueprints.none": "Шаблоны потоков не найдены",
"cli.verify_system_status.flow_blueprints.error": "Ошибка проверки шаблонов потоков: {error}",
"cli.verify_system_status.flows.responding": "Менеджер потоков отвечает ({count} поток(ов))",
"cli.verify_system_status.flows.error": "Ошибка проверки менеджера потоков: {error}",
"cli.verify_system_status.prompts.found": "Обнаружено {count} подсказок",
"cli.verify_system_status.prompts.none": "Подсказки не найдены",
"cli.verify_system_status.prompts.error": "Ошибка проверки подсказок: {error}",
"cli.verify_system_status.library.responding": "Библиотека отвечает ({count} документ(ов))",
"cli.verify_system_status.library.error": "Ошибка проверки библиотеки: {error}",
"cli.verify_system_status.ui.responding": "Пользовательский интерфейс Workbench отвечает",
"cli.verify_system_status.ui.status": "Пользовательский интерфейс вернул статус {status_code}",
"cli.verify_system_status.ui.timeout": "Время ожидания соединения с пользовательским интерфейсом истекло",
"cli.verify_system_status.ui.cannot_connect": "Не удалось подключиться к пользовательскому интерфейсу",
"cli.verify_system_status.ui.error": "Ошибка пользовательского интерфейса: {error}"
}

View file

@ -0,0 +1,54 @@
{
"cli.verify_system_status.title": "Uthibitisho wa Hali ya Mfumo wa TrustGraph",
"cli.verify_system_status.phase_1": "Awamu ya 1: Miundombinu",
"cli.verify_system_status.phase_2": "Awamu ya 2: Huduma za Msingi",
"cli.verify_system_status.phase_3": "Awamu ya 3: Huduma za Data",
"cli.verify_system_status.phase_4": "Awamu ya 4: Kiolesura cha Mtumiaji",
"cli.verify_system_status.summary": "Muhtasari",
"cli.verify_system_status.checking": "Kuchunguza {name}...",
"cli.verify_system_status.checking_attempt": "Kuchunguza {name}... (jaribio la {attempt})",
"cli.verify_system_status.failed_timeout": "{name}: Imeshindwa (iliyepuka baada ya majaribio {attempt})",
"cli.verify_system_status.pulsar_not_responding": "Pulsar haijibu - vipimo vingine vinaweza kushindwa",
"cli.verify_system_status.checks_passed": "Vipimo vilivyofaulu: {passed}/{total}",
"cli.verify_system_status.checks_failed": "Vipimo vilivyoshindwa: {failed}/{total}",
"cli.verify_system_status.total_time": "Muda jumla: {elapsed}",
"cli.verify_system_status.system_healthy": "Mfumo una afya!",
"cli.verify_system_status.system_failing": "Mfumo una {failed} kipimo(s) kilicho(s) kifeli(s)",
"cli.verify_system_status.check_name.pulsar": "Pulsar",
"cli.verify_system_status.check_name.api_gateway": "Milango ya API",
"cli.verify_system_status.check_name.processors": "Wasindikaji",
"cli.verify_system_status.check_name.flow_blueprints": "Mipango ya Mtiririko",
"cli.verify_system_status.check_name.flows": "Mitiririko",
"cli.verify_system_status.check_name.prompts": "Maagizo",
"cli.verify_system_status.check_name.library": "Maktaba",
"cli.verify_system_status.check_name.workbench_ui": "Kiolesura cha Kifaa cha Kazi",
"cli.verify_system_status.pulsar.healthy": "Pulsar ina afya (vikundi {clusters})",
"cli.verify_system_status.pulsar.status": "Pulsar ilirudisha hali {status_code}",
"cli.verify_system_status.pulsar.timeout": "Muda wa kuunganisha na Pulsar umepita",
"cli.verify_system_status.pulsar.cannot_connect": "Haiwezekani kuunganisha na Pulsar",
"cli.verify_system_status.pulsar.error": "Kosa la Pulsar: {error}",
"cli.verify_system_status.api_gateway.responding": "Milango ya API inajibu",
"cli.verify_system_status.api_gateway.status": "Milango ya API ilirudisha hali {status_code}",
"cli.verify_system_status.api_gateway.timeout": "Muda wa kuunganisha na Milango ya API umepita",
"cli.verify_system_status.api_gateway.cannot_connect": "Haiwezekani kuunganisha na Milango ya API",
"cli.verify_system_status.api_gateway.error": "Kosa la Milango ya API: {error}",
"cli.verify_system_status.processors.found": "Imebainika wasindikaji {count} (≥ {min})",
"cli.verify_system_status.processors.only": "Tu wasindikaji {count} wanaendesha (wanahitaji {min})",
"cli.verify_system_status.processors.metrics_status": "Vipimo vilirudisha hali {status_code}",
"cli.verify_system_status.processors.error": "Kosa la ukaguzi wa wasindikaji: {error}",
"cli.verify_system_status.flow_blueprints.found": "Imebainika mipango ya mtiririko {count}",
"cli.verify_system_status.flow_blueprints.none": "Hakuna mipango ya mtiririko iliyobainika",
"cli.verify_system_status.flow_blueprints.error": "Kosa la ukaguzi wa mipango ya mtiririko: {error}",
"cli.verify_system_status.flows.responding": "Kidhibiti cha mtiririko kinajibu (mitiririko {count})",
"cli.verify_system_status.flows.error": "Kosa la ukaguzi wa kidhibiti cha mtiririko: {error}",
"cli.verify_system_status.prompts.found": "Imebainika maagizo {count}",
"cli.verify_system_status.prompts.none": "Hakuna maagizo yaliyobainika",
"cli.verify_system_status.prompts.error": "Kosa la ukaguzi wa maagizo: {error}",
"cli.verify_system_status.library.responding": "Maktaba inajibu (nyaraka {count})",
"cli.verify_system_status.library.error": "Kosa la ukaguzi wa maktaba: {error}",
"cli.verify_system_status.ui.responding": "Kiolesura cha Kifaa cha Kazi kinajibu",
"cli.verify_system_status.ui.status": "Kiolesura kilirudisha hali {status_code}",
"cli.verify_system_status.ui.timeout": "Muda wa kuunganisha na kiolesura umepita",
"cli.verify_system_status.ui.cannot_connect": "Haiwezekani kuunganisha na kiolesura",
"cli.verify_system_status.ui.error": "Kosa la kiolesura: {error}"
}

View file

@ -0,0 +1,54 @@
{
"cli.verify_system_status.title": "TrustGraph Sistem Durumu Doğrulama",
"cli.verify_system_status.phase_1": "Aşama 1: Altyapı",
"cli.verify_system_status.phase_2": "Aşama 2: Temel Hizmetler",
"cli.verify_system_status.phase_3": "Aşama 3: Veri Hizmetleri",
"cli.verify_system_status.phase_4": "Aşama 4: Kullanıcı Arayüzü",
"cli.verify_system_status.summary": "Özet",
"cli.verify_system_status.checking": "{name}'ı kontrol ediliyor...",
"cli.verify_system_status.checking_attempt": "{name}'ı kontrol ediliyor... ({attempt} deneme)",
"cli.verify_system_status.failed_timeout": "{name}: Başarısız ({attempt} denemeden sonra zaman aşımı)",
"cli.verify_system_status.pulsar_not_responding": "Pulsar yanıt vermiyor - diğer kontroller başarısız olabilir",
"cli.verify_system_status.checks_passed": "Başarılı kontroller: {passed}/{total}",
"cli.verify_system_status.checks_failed": "Başarısız kontroller: {failed}/{total}",
"cli.verify_system_status.total_time": "Toplam süre: {elapsed}",
"cli.verify_system_status.system_healthy": "Sistem sağlıklı!",
"cli.verify_system_status.system_failing": "Sistemde {failed} adet başarısız kontrol var",
"cli.verify_system_status.check_name.pulsar": "Pulsar",
"cli.verify_system_status.check_name.api_gateway": "API Ağ Geçidi",
"cli.verify_system_status.check_name.processors": "İşlemciler",
"cli.verify_system_status.check_name.flow_blueprints": "Akış Şemaları",
"cli.verify_system_status.check_name.flows": "Akışlar",
"cli.verify_system_status.check_name.prompts": "İpuçları",
"cli.verify_system_status.check_name.library": "Kütüphane",
"cli.verify_system_status.check_name.workbench_ui": "Çalışma Alanı Arayüzü",
"cli.verify_system_status.pulsar.healthy": "Pulsar sağlıklı ({clusters} küme)",
"cli.verify_system_status.pulsar.status": "Pulsar, {status_code} durumunu döndürdü",
"cli.verify_system_status.pulsar.timeout": "Pulsar bağlantı zaman aşımı",
"cli.verify_system_status.pulsar.cannot_connect": "Pulsar'a bağlanılamıyor",
"cli.verify_system_status.pulsar.error": "Pulsar hatası: {error}",
"cli.verify_system_status.api_gateway.responding": "API Ağ Geçidi yanıt veriyor",
"cli.verify_system_status.api_gateway.status": "API Ağ Geçidi, {status_code} durumunu döndürdü",
"cli.verify_system_status.api_gateway.timeout": "API Ağ Geçidi bağlantı zaman aşımı",
"cli.verify_system_status.api_gateway.cannot_connect": "API Ağ Geçidi'ne bağlanılamıyor",
"cli.verify_system_status.api_gateway.error": "API Ağ Geçidi hatası: {error}",
"cli.verify_system_status.processors.found": "{count} adet işlemci bulundu (≥ {min})",
"cli.verify_system_status.processors.only": "Sadece {count} adet işlemci çalışıyor (gerekli {min})",
"cli.verify_system_status.processors.metrics_status": "Ölçümler, {status_code} durumunu döndürdü",
"cli.verify_system_status.processors.error": "İşlemci kontrol hatası: {error}",
"cli.verify_system_status.flow_blueprints.found": "{count} adet akış şeması bulundu",
"cli.verify_system_status.flow_blueprints.none": "Akış şeması bulunamadı",
"cli.verify_system_status.flow_blueprints.error": "Akış şeması kontrol hatası: {error}",
"cli.verify_system_status.flows.responding": "Akış yöneticisi yanıt veriyor ({count} akış)",
"cli.verify_system_status.flows.error": "Akış yöneticisi kontrol hatası: {error}",
"cli.verify_system_status.prompts.found": "{count} adet ipucu bulundu",
"cli.verify_system_status.prompts.none": "İpucu bulunamadı",
"cli.verify_system_status.prompts.error": "İpuçları kontrol hatası: {error}",
"cli.verify_system_status.library.responding": "Kütüphane yanıt veriyor ({count} belge)",
"cli.verify_system_status.library.error": "Kütüphane kontrol hatası: {error}",
"cli.verify_system_status.ui.responding": "Çalışma alanı Arayüzü yanıt veriyor",
"cli.verify_system_status.ui.status": "Arayüz, {status_code} durumunu döndürdü",
"cli.verify_system_status.ui.timeout": "Arayüz bağlantı zaman aşımı",
"cli.verify_system_status.ui.cannot_connect": "Arayüze bağlanılamıyor",
"cli.verify_system_status.ui.error": "Arayüz hatası: {error}"
}

View file

@ -0,0 +1,54 @@
{
"cli.verify_system_status.title": "TrustGraph 系统状态验证",
"cli.verify_system_status.phase_1": "第一阶段:基础设施",
"cli.verify_system_status.phase_2": "第二阶段:核心服务",
"cli.verify_system_status.phase_3": "第三阶段:数据服务",
"cli.verify_system_status.phase_4": "第四阶段:用户界面",
"cli.verify_system_status.summary": "总结",
"cli.verify_system_status.checking": "正在检查 {name}...",
"cli.verify_system_status.checking_attempt": "正在检查 {name}... (第 {attempt} 次尝试)",
"cli.verify_system_status.failed_timeout": "{name}: 失败 (在 {attempt} 次尝试后超时)",
"cli.verify_system_status.pulsar_not_responding": "Pulsar 未响应 - 其他检查可能失败",
"cli.verify_system_status.checks_passed": "通过检查: {passed}/{total}",
"cli.verify_system_status.checks_failed": "失败检查: {failed}/{total}",
"cli.verify_system_status.total_time": "总时间: {elapsed}",
"cli.verify_system_status.system_healthy": "系统状态良好!",
"cli.verify_system_status.system_failing": "系统有 {failed} 个失败的检查",
"cli.verify_system_status.check_name.pulsar": "Pulsar",
"cli.verify_system_status.check_name.api_gateway": "API 网关",
"cli.verify_system_status.check_name.processors": "处理程序",
"cli.verify_system_status.check_name.flow_blueprints": "流蓝图",
"cli.verify_system_status.check_name.flows": "流",
"cli.verify_system_status.check_name.prompts": "提示",
"cli.verify_system_status.check_name.library": "库",
"cli.verify_system_status.check_name.workbench_ui": "Workbench UI",
"cli.verify_system_status.pulsar.healthy": "Pulsar 状态良好 ({clusters} 个集群)",
"cli.verify_system_status.pulsar.status": "Pulsar 返回状态 {status_code}",
"cli.verify_system_status.pulsar.timeout": "Pulsar 连接超时",
"cli.verify_system_status.pulsar.cannot_connect": "无法连接到 Pulsar",
"cli.verify_system_status.pulsar.error": "Pulsar 错误: {error}",
"cli.verify_system_status.api_gateway.responding": "API 网关正在响应",
"cli.verify_system_status.api_gateway.status": "API 网关返回状态 {status_code}",
"cli.verify_system_status.api_gateway.timeout": "API 网关连接超时",
"cli.verify_system_status.api_gateway.cannot_connect": "无法连接到 API 网关",
"cli.verify_system_status.api_gateway.error": "API 网关错误: {error}",
"cli.verify_system_status.processors.found": "发现 {count} 个处理程序 (≥ {min})",
"cli.verify_system_status.processors.only": "只有 {count} 个处理程序正在运行 (需要 {min} 个)",
"cli.verify_system_status.processors.metrics_status": "Metrics 返回状态 {status_code}",
"cli.verify_system_status.processors.error": "处理程序检查错误: {error}",
"cli.verify_system_status.flow_blueprints.found": "发现 {count} 个流蓝图",
"cli.verify_system_status.flow_blueprints.none": "未发现任何流蓝图",
"cli.verify_system_status.flow_blueprints.error": "流蓝图检查错误: {error}",
"cli.verify_system_status.flows.responding": "流管理器正在响应 ({count} 个流)",
"cli.verify_system_status.flows.error": "流管理器检查错误: {error}",
"cli.verify_system_status.prompts.found": "发现 {count} 个提示",
"cli.verify_system_status.prompts.none": "未发现任何提示",
"cli.verify_system_status.prompts.error": "提示检查错误: {error}",
"cli.verify_system_status.library.responding": "库正在响应 ({count} 个文档)",
"cli.verify_system_status.library.error": "库检查错误: {error}",
"cli.verify_system_status.ui.responding": "Workbench UI 正在响应",
"cli.verify_system_status.ui.status": "UI 返回状态 {status_code}",
"cli.verify_system_status.ui.timeout": "UI 连接超时",
"cli.verify_system_status.ui.cannot_connect": "无法连接到 UI",
"cli.verify_system_status.ui.error": "UI 错误: {error}"
}

View file

@ -60,8 +60,8 @@ class AgentResponseTranslator(MessageTranslator):
def encode(self, obj: AgentResponse) -> Dict[str, Any]:
result = {}
if obj.chunk_type:
result["chunk_type"] = obj.chunk_type
if obj.message_type:
result["message_type"] = obj.message_type
if obj.content:
result["content"] = obj.content
result["end_of_message"] = getattr(obj, "end_of_message", False)
@ -90,6 +90,13 @@ class AgentResponseTranslator(MessageTranslator):
if hasattr(obj, 'error') and obj.error and obj.error.message:
result["error"] = {"message": obj.error.message, "code": obj.error.code}
if obj.in_token is not None:
result["in_token"] = obj.in_token
if obj.out_token is not None:
result["out_token"] = obj.out_token
if obj.model is not None:
result["model"] = obj.model
return result
def encode_with_completion(self, obj: AgentResponse) -> Tuple[Dict[str, Any], bool]:

View file

@ -151,7 +151,7 @@ class DocumentEmbeddingsTranslator(SendTranslator):
chunks = [
ChunkEmbeddings(
chunk_id=chunk["chunk_id"],
vectors=chunk["vectors"]
vector=chunk["vector"]
)
for chunk in data.get("chunks", [])
]

View file

@ -39,7 +39,7 @@ class KnowledgeRequestTranslator(MessageTranslator):
entities=[
EntityEmbeddings(
entity=self.value_translator.decode(ent["entity"]),
vectors=ent["vectors"],
vector=ent["vector"],
)
for ent in data["graph-embeddings"]["entities"]
]

View file

@ -53,6 +53,13 @@ class PromptResponseTranslator(MessageTranslator):
# Always include end_of_stream flag for streaming support
result["end_of_stream"] = getattr(obj, "end_of_stream", False)
if obj.in_token is not None:
result["in_token"] = obj.in_token
if obj.out_token is not None:
result["out_token"] = obj.out_token
if obj.model is not None:
result["model"] = obj.model
return result
def encode_with_completion(self, obj: PromptResponse) -> Tuple[Dict[str, Any], bool]:

View file

@ -74,6 +74,13 @@ class DocumentRagResponseTranslator(MessageTranslator):
if hasattr(obj, 'error') and obj.error and obj.error.message:
result["error"] = {"message": obj.error.message, "type": obj.error.type}
if obj.in_token is not None:
result["in_token"] = obj.in_token
if obj.out_token is not None:
result["out_token"] = obj.out_token
if obj.model is not None:
result["model"] = obj.model
return result
def encode_with_completion(self, obj: DocumentRagResponse) -> Tuple[Dict[str, Any], bool]:
@ -163,6 +170,13 @@ class GraphRagResponseTranslator(MessageTranslator):
if hasattr(obj, 'error') and obj.error and obj.error.message:
result["error"] = {"message": obj.error.message, "type": obj.error.type}
if obj.in_token is not None:
result["in_token"] = obj.in_token
if obj.out_token is not None:
result["out_token"] = obj.out_token
if obj.model is not None:
result["model"] = obj.model
return result
def encode_with_completion(self, obj: GraphRagResponse) -> Tuple[Dict[str, Any], bool]:

View file

@ -29,11 +29,11 @@ class TextCompletionResponseTranslator(MessageTranslator):
def encode(self, obj: TextCompletionResponse) -> Dict[str, Any]:
result = {"response": obj.response}
if obj.in_token:
if obj.in_token is not None:
result["in_token"] = obj.in_token
if obj.out_token:
if obj.out_token is not None:
result["out_token"] = obj.out_token
if obj.model:
if obj.model is not None:
result["model"] = obj.model
# Always include end_of_stream flag for streaming support

View file

@ -59,6 +59,7 @@ from . uris import (
agent_plan_uri,
agent_step_result_uri,
agent_synthesis_uri,
agent_pattern_decision_uri,
# Document RAG provenance URIs
docrag_question_uri,
docrag_grounding_uri,
@ -102,6 +103,11 @@ from . namespaces import (
# Agent provenance predicates
TG_THOUGHT, TG_ACTION, TG_ARGUMENTS, TG_OBSERVATION,
TG_SUBAGENT_GOAL, TG_PLAN_STEP,
TG_TOOL_CANDIDATE, TG_TERMINATION_REASON,
TG_STEP_NUMBER, TG_PATTERN_DECISION, TG_PATTERN, TG_TASK_TYPE,
TG_LLM_DURATION_MS, TG_TOOL_DURATION_MS, TG_TOOL_ERROR,
TG_IN_TOKEN, TG_OUT_TOKEN,
TG_ERROR_TYPE,
# Orchestrator entity types
TG_DECOMPOSITION, TG_FINDING, TG_PLAN_TYPE, TG_STEP_RESULT,
# Document reference predicate
@ -141,6 +147,7 @@ from . agent import (
agent_plan_triples,
agent_step_result_triples,
agent_synthesis_triples,
agent_pattern_decision_triples,
)
# Vocabulary bootstrap
@ -182,6 +189,7 @@ __all__ = [
"agent_plan_uri",
"agent_step_result_uri",
"agent_synthesis_uri",
"agent_pattern_decision_uri",
# Document RAG provenance URIs
"docrag_question_uri",
"docrag_grounding_uri",
@ -218,6 +226,11 @@ __all__ = [
# Agent provenance predicates
"TG_THOUGHT", "TG_ACTION", "TG_ARGUMENTS", "TG_OBSERVATION",
"TG_SUBAGENT_GOAL", "TG_PLAN_STEP",
"TG_TOOL_CANDIDATE", "TG_TERMINATION_REASON",
"TG_STEP_NUMBER", "TG_PATTERN_DECISION", "TG_PATTERN", "TG_TASK_TYPE",
"TG_LLM_DURATION_MS", "TG_TOOL_DURATION_MS", "TG_TOOL_ERROR",
"TG_IN_TOKEN", "TG_OUT_TOKEN",
"TG_ERROR_TYPE",
# Orchestrator entity types
"TG_DECOMPOSITION", "TG_FINDING", "TG_PLAN_TYPE", "TG_STEP_RESULT",
# Document reference predicate
@ -249,6 +262,7 @@ __all__ = [
"agent_plan_triples",
"agent_step_result_triples",
"agent_synthesis_triples",
"agent_pattern_decision_triples",
# Utility
"set_graph",
# Vocabulary

View file

@ -29,6 +29,11 @@ from . namespaces import (
TG_AGENT_QUESTION,
TG_DECOMPOSITION, TG_FINDING, TG_PLAN_TYPE, TG_STEP_RESULT,
TG_SYNTHESIS, TG_SUBAGENT_GOAL, TG_PLAN_STEP,
TG_TOOL_CANDIDATE, TG_TERMINATION_REASON,
TG_STEP_NUMBER, TG_PATTERN_DECISION, TG_PATTERN, TG_TASK_TYPE,
TG_LLM_DURATION_MS, TG_TOOL_DURATION_MS, TG_TOOL_ERROR,
TG_ERROR_TYPE,
TG_IN_TOKEN, TG_OUT_TOKEN, TG_LLM_MODEL,
)
@ -47,6 +52,17 @@ def _triple(s: str, p: str, o_term: Term) -> Triple:
return Triple(s=_iri(s), p=_iri(p), o=o_term)
def _append_token_triples(triples, uri, in_token=None, out_token=None,
model=None):
"""Append in_token/out_token/model triples when values are present."""
if in_token is not None:
triples.append(_triple(uri, TG_IN_TOKEN, _literal(str(in_token))))
if out_token is not None:
triples.append(_triple(uri, TG_OUT_TOKEN, _literal(str(out_token))))
if model is not None:
triples.append(_triple(uri, TG_LLM_MODEL, _literal(model)))
def agent_session_triples(
session_uri: str,
query: str,
@ -90,6 +106,43 @@ def agent_session_triples(
return triples
def agent_pattern_decision_triples(
uri: str,
session_uri: str,
pattern: str,
task_type: str = "",
) -> List[Triple]:
"""
Build triples for a meta-router pattern decision.
Creates:
- Entity declaration with tg:PatternDecision type
- wasDerivedFrom link to session
- Pattern and task type predicates
Args:
uri: URI of this decision (from agent_pattern_decision_uri)
session_uri: URI of the parent session
pattern: Selected execution pattern (e.g. "react", "plan-then-execute")
task_type: Identified task type (e.g. "general", "research")
Returns:
List of Triple objects
"""
triples = [
_triple(uri, RDF_TYPE, _iri(PROV_ENTITY)),
_triple(uri, RDF_TYPE, _iri(TG_PATTERN_DECISION)),
_triple(uri, RDFS_LABEL, _literal(f"Pattern: {pattern}")),
_triple(uri, TG_PATTERN, _literal(pattern)),
_triple(uri, PROV_WAS_DERIVED_FROM, _iri(session_uri)),
]
if task_type:
triples.append(_triple(uri, TG_TASK_TYPE, _literal(task_type)))
return triples
def agent_iteration_triples(
iteration_uri: str,
question_uri: Optional[str] = None,
@ -98,6 +151,12 @@ def agent_iteration_triples(
arguments: Dict[str, Any] = None,
thought_uri: Optional[str] = None,
thought_document_id: Optional[str] = None,
tool_candidates: Optional[List[str]] = None,
step_number: Optional[int] = None,
llm_duration_ms: Optional[int] = None,
in_token: Optional[int] = None,
out_token: Optional[int] = None,
model: Optional[str] = None,
) -> List[Triple]:
"""
Build triples for one agent iteration (Analysis+ToolUse).
@ -106,6 +165,7 @@ def agent_iteration_triples(
- Entity declaration with tg:Analysis and tg:ToolUse types
- wasDerivedFrom link to question (if first iteration) or previous
- Action and arguments metadata
- Tool candidates (names of tools visible to the LLM)
- Thought sub-entity (tg:Reflection, tg:Thought) with librarian document
Args:
@ -116,6 +176,7 @@ def agent_iteration_triples(
arguments: Arguments passed to the tool (will be JSON-encoded)
thought_uri: URI for the thought sub-entity
thought_document_id: Document URI for thought in librarian
tool_candidates: List of tool names available to the LLM
Returns:
List of Triple objects
@ -132,6 +193,23 @@ def agent_iteration_triples(
_triple(iteration_uri, TG_ARGUMENTS, _literal(json.dumps(arguments))),
]
if tool_candidates:
for name in tool_candidates:
triples.append(
_triple(iteration_uri, TG_TOOL_CANDIDATE, _literal(name))
)
if step_number is not None:
triples.append(
_triple(iteration_uri, TG_STEP_NUMBER, _literal(str(step_number)))
)
if llm_duration_ms is not None:
triples.append(
_triple(iteration_uri, TG_LLM_DURATION_MS,
_literal(str(llm_duration_ms)))
)
if question_uri:
triples.append(
_triple(iteration_uri, PROV_WAS_DERIVED_FROM, _iri(question_uri))
@ -155,6 +233,8 @@ def agent_iteration_triples(
_triple(thought_uri, TG_DOCUMENT, _iri(thought_document_id))
)
_append_token_triples(triples, iteration_uri, in_token, out_token, model)
return triples
@ -162,6 +242,8 @@ def agent_observation_triples(
observation_uri: str,
iteration_uri: str,
document_id: Optional[str] = None,
tool_duration_ms: Optional[int] = None,
tool_error: Optional[str] = None,
) -> List[Triple]:
"""
Build triples for an agent observation (standalone entity).
@ -170,11 +252,15 @@ def agent_observation_triples(
- Entity declaration with prov:Entity and tg:Observation types
- wasDerivedFrom link to the iteration (Analysis+ToolUse)
- Document reference to librarian (if provided)
- Tool execution duration (if provided)
- Tool error message (if the tool failed)
Args:
observation_uri: URI of the observation entity
iteration_uri: URI of the iteration this observation derives from
document_id: Librarian document ID for the observation content
tool_duration_ms: Tool execution time in milliseconds
tool_error: Error message if the tool failed
Returns:
List of Triple objects
@ -191,6 +277,20 @@ def agent_observation_triples(
_triple(observation_uri, TG_DOCUMENT, _iri(document_id))
)
if tool_duration_ms is not None:
triples.append(
_triple(observation_uri, TG_TOOL_DURATION_MS,
_literal(str(tool_duration_ms)))
)
if tool_error:
triples.append(
_triple(observation_uri, TG_TOOL_ERROR, _literal(tool_error))
)
triples.append(
_triple(observation_uri, RDF_TYPE, _iri(TG_ERROR_TYPE))
)
return triples
@ -199,6 +299,10 @@ def agent_final_triples(
question_uri: Optional[str] = None,
previous_uri: Optional[str] = None,
document_id: Optional[str] = None,
termination_reason: Optional[str] = None,
in_token: Optional[int] = None,
out_token: Optional[int] = None,
model: Optional[str] = None,
) -> List[Triple]:
"""
Build triples for an agent final answer (Conclusion).
@ -208,12 +312,15 @@ def agent_final_triples(
- wasGeneratedBy link to question (if no iterations)
- wasDerivedFrom link to last iteration (if iterations exist)
- Document reference to librarian
- Termination reason (why the agent loop stopped)
Args:
final_uri: URI of the final answer (from agent_final_uri)
question_uri: URI of the question activity (if no iterations)
previous_uri: URI of the last iteration (if iterations exist)
document_id: Librarian document ID for the answer content
termination_reason: Why the loop stopped, e.g. "final-answer",
"max-iterations", "error"
Returns:
List of Triple objects
@ -237,6 +344,14 @@ def agent_final_triples(
if document_id:
triples.append(_triple(final_uri, TG_DOCUMENT, _iri(document_id)))
if termination_reason:
triples.append(
_triple(final_uri, TG_TERMINATION_REASON,
_literal(termination_reason))
)
_append_token_triples(triples, final_uri, in_token, out_token, model)
return triples
@ -244,6 +359,9 @@ def agent_decomposition_triples(
uri: str,
session_uri: str,
goals: List[str],
in_token: Optional[int] = None,
out_token: Optional[int] = None,
model: Optional[str] = None,
) -> List[Triple]:
"""Build triples for a supervisor decomposition step."""
triples = [
@ -255,6 +373,7 @@ def agent_decomposition_triples(
]
for goal in goals:
triples.append(_triple(uri, TG_SUBAGENT_GOAL, _literal(goal)))
_append_token_triples(triples, uri, in_token, out_token, model)
return triples
@ -282,6 +401,9 @@ def agent_plan_triples(
uri: str,
session_uri: str,
steps: List[str],
in_token: Optional[int] = None,
out_token: Optional[int] = None,
model: Optional[str] = None,
) -> List[Triple]:
"""Build triples for a plan-then-execute plan."""
triples = [
@ -293,6 +415,7 @@ def agent_plan_triples(
]
for step in steps:
triples.append(_triple(uri, TG_PLAN_STEP, _literal(step)))
_append_token_triples(triples, uri, in_token, out_token, model)
return triples
@ -301,6 +424,9 @@ def agent_step_result_triples(
plan_uri: str,
goal: str,
document_id: Optional[str] = None,
in_token: Optional[int] = None,
out_token: Optional[int] = None,
model: Optional[str] = None,
) -> List[Triple]:
"""Build triples for a plan step result."""
triples = [
@ -313,6 +439,7 @@ def agent_step_result_triples(
]
if document_id:
triples.append(_triple(uri, TG_DOCUMENT, _iri(document_id)))
_append_token_triples(triples, uri, in_token, out_token, model)
return triples
@ -320,6 +447,10 @@ def agent_synthesis_triples(
uri: str,
previous_uris,
document_id: Optional[str] = None,
termination_reason: Optional[str] = None,
in_token: Optional[int] = None,
out_token: Optional[int] = None,
model: Optional[str] = None,
) -> List[Triple]:
"""Build triples for a synthesis answer.
@ -327,6 +458,8 @@ def agent_synthesis_triples(
uri: URI of the synthesis entity
previous_uris: Single URI string or list of URIs to derive from
document_id: Librarian document ID for the answer content
termination_reason: Why the agent loop stopped
in_token/out_token/model: Token usage for the synthesis LLM call
"""
triples = [
_triple(uri, RDF_TYPE, _iri(PROV_ENTITY)),
@ -342,4 +475,12 @@ def agent_synthesis_triples(
if document_id:
triples.append(_triple(uri, TG_DOCUMENT, _iri(document_id)))
if termination_reason:
triples.append(
_triple(uri, TG_TERMINATION_REASON, _literal(termination_reason))
)
_append_token_triples(triples, uri, in_token, out_token, model)
return triples

View file

@ -119,6 +119,18 @@ TG_ARGUMENTS = TG + "arguments"
TG_OBSERVATION = TG + "observation" # Links iteration to observation sub-entity
TG_SUBAGENT_GOAL = TG + "subagentGoal" # Goal string on Decomposition/Finding
TG_PLAN_STEP = TG + "planStep" # Step goal string on Plan/StepResult
TG_TOOL_CANDIDATE = TG + "toolCandidate" # Tool name on Analysis events
TG_TERMINATION_REASON = TG + "terminationReason" # Why the agent loop stopped
TG_STEP_NUMBER = TG + "stepNumber" # Explicit step counter on iteration events
TG_PATTERN_DECISION = TG + "PatternDecision" # Meta-router routing decision entity type
TG_PATTERN = TG + "pattern" # Selected execution pattern
TG_TASK_TYPE = TG + "taskType" # Identified task type
TG_LLM_DURATION_MS = TG + "llmDurationMs" # LLM call duration in milliseconds
TG_TOOL_DURATION_MS = TG + "toolDurationMs" # Tool execution duration in milliseconds
TG_TOOL_ERROR = TG + "toolError" # Error message from a failed tool execution
TG_ERROR_TYPE = TG + "Error" # Mixin type for failure events
TG_IN_TOKEN = TG + "inToken" # Input token count for an LLM call
TG_OUT_TOKEN = TG + "outToken" # Output token count for an LLM call
# Named graph URIs for RDF datasets
# These separate different types of data while keeping them in the same collection

View file

@ -34,6 +34,8 @@ from . namespaces import (
TG_ANSWER_TYPE,
# Question subtypes
TG_GRAPH_RAG_QUESTION, TG_DOC_RAG_QUESTION,
# Token usage
TG_IN_TOKEN, TG_OUT_TOKEN,
)
from . uris import activity_uri, agent_uri, subgraph_uri, edge_selection_uri
@ -74,6 +76,17 @@ def _triple(s: str, p: str, o_term: Term) -> Triple:
return Triple(s=_iri(s), p=_iri(p), o=o_term)
def _append_token_triples(triples, uri, in_token=None, out_token=None,
model=None):
"""Append in_token/out_token/model triples when values are present."""
if in_token is not None:
triples.append(_triple(uri, TG_IN_TOKEN, _literal(str(in_token))))
if out_token is not None:
triples.append(_triple(uri, TG_OUT_TOKEN, _literal(str(out_token))))
if model is not None:
triples.append(_triple(uri, TG_LLM_MODEL, _literal(model)))
def document_triples(
doc_uri: str,
title: Optional[str] = None,
@ -396,6 +409,9 @@ def grounding_triples(
grounding_uri: str,
question_uri: str,
concepts: List[str],
in_token: Optional[int] = None,
out_token: Optional[int] = None,
model: Optional[str] = None,
) -> List[Triple]:
"""
Build triples for a grounding entity (concept decomposition of query).
@ -423,6 +439,8 @@ def grounding_triples(
for concept in concepts:
triples.append(_triple(grounding_uri, TG_CONCEPT, _literal(concept)))
_append_token_triples(triples, grounding_uri, in_token, out_token, model)
return triples
@ -485,6 +503,9 @@ def focus_triples(
exploration_uri: str,
selected_edges_with_reasoning: List[dict],
session_id: str = "",
in_token: Optional[int] = None,
out_token: Optional[int] = None,
model: Optional[str] = None,
) -> List[Triple]:
"""
Build triples for a focus entity (selected edges with reasoning).
@ -543,6 +564,8 @@ def focus_triples(
_triple(edge_sel_uri, TG_REASONING, _literal(reasoning))
)
_append_token_triples(triples, focus_uri, in_token, out_token, model)
return triples
@ -550,6 +573,9 @@ def synthesis_triples(
synthesis_uri: str,
focus_uri: str,
document_id: Optional[str] = None,
in_token: Optional[int] = None,
out_token: Optional[int] = None,
model: Optional[str] = None,
) -> List[Triple]:
"""
Build triples for a synthesis entity (final answer).
@ -578,6 +604,8 @@ def synthesis_triples(
if document_id:
triples.append(_triple(synthesis_uri, TG_DOCUMENT, _iri(document_id)))
_append_token_triples(triples, synthesis_uri, in_token, out_token, model)
return triples
@ -674,6 +702,9 @@ def docrag_synthesis_triples(
synthesis_uri: str,
exploration_uri: str,
document_id: Optional[str] = None,
in_token: Optional[int] = None,
out_token: Optional[int] = None,
model: Optional[str] = None,
) -> List[Triple]:
"""
Build triples for a document RAG synthesis entity (final answer).
@ -702,4 +733,6 @@ def docrag_synthesis_triples(
if document_id:
triples.append(_triple(synthesis_uri, TG_DOCUMENT, _iri(document_id)))
_append_token_triples(triples, synthesis_uri, in_token, out_token, model)
return triples

View file

@ -259,6 +259,11 @@ def agent_synthesis_uri(session_id: str) -> str:
return f"urn:trustgraph:agent:{session_id}/synthesis"
def agent_pattern_decision_uri(session_id: str) -> str:
"""Generate URI for a meta-router pattern decision."""
return f"urn:trustgraph:agent:{session_id}/pattern-decision"
# Document RAG provenance URIs
# These URIs use the urn:trustgraph:docrag: namespace to distinguish
# document RAG provenance from graph RAG provenance

View file

@ -51,8 +51,8 @@ class AgentRequest:
@dataclass
class AgentResponse:
# Streaming-first design
chunk_type: str = "" # "thought", "action", "observation", "answer", "explain", "error"
content: str = "" # The actual content (interpretation depends on chunk_type)
message_type: str = "" # "thought", "action", "observation", "answer", "explain", "error"
content: str = "" # The actual content (interpretation depends on message_type)
end_of_message: bool = False # Current chunk type (thought/action/etc.) is complete
end_of_dialog: bool = False # Entire agent dialog is complete
@ -66,5 +66,10 @@ class AgentResponse:
error: Error | None = None
# Token usage (populated on end_of_dialog message)
in_token: int | None = None
out_token: int | None = None
model: str | None = None
############################################################################

View file

@ -17,9 +17,9 @@ class TextCompletionRequest:
class TextCompletionResponse:
error: Error | None = None
response: str = ""
in_token: int = 0
out_token: int = 0
model: str = ""
in_token: int | None = None
out_token: int | None = None
model: str | None = None
end_of_stream: bool = False # Indicates final message in stream
############################################################################

View file

@ -41,4 +41,9 @@ class PromptResponse:
# Indicates final message in stream
end_of_stream: bool = False
# Token usage from the underlying text completion
in_token: int | None = None
out_token: int | None = None
model: str | None = None
############################################################################

View file

@ -29,6 +29,9 @@ class GraphRagResponse:
explain_triples: list[Triple] = field(default_factory=list) # Provenance triples for this step
message_type: str = "" # "chunk" or "explain"
end_of_session: bool = False # Entire session complete
in_token: int | None = None
out_token: int | None = None
model: str | None = None
############################################################################
@ -52,3 +55,6 @@ class DocumentRagResponse:
explain_triples: list[Triple] = field(default_factory=list) # Provenance triples for this step
message_type: str = "" # "chunk" or "explain"
end_of_session: bool = False # Entire session complete
in_token: int | None = None
out_token: int | None = None
model: str | None = None