Update tech spec

This commit is contained in:
Cyber MacGeddon 2025-12-03 16:52:50 +00:00
parent 9256464e12
commit 2e4afc220a

View file

@ -68,125 +68,276 @@ The November 2024 streaming enhancement to the Gateway API provided 60x latency
### Architecture ### Architecture
The refactored Python API will support two transport layers: The refactored Python API uses a **modular interface approach** with separate objects for different communication patterns:
1. **REST Transport** (existing, enhanced) 1. **REST Interface** (existing, unchanged)
- `api.flow()`, `api.library()`, `api.knowledge()`, `api.collection()`, `api.config()`
- Synchronous request/response - Synchronous request/response
- Simple connection model - Simple connection model
- No streaming support
- Default for backward compatibility - Default for backward compatibility
2. **WebSocket Transport** (new) 2. **WebSocket Interface** (new)
- `api.socket()`
- Persistent connection - Persistent connection
- Multiplexed requests - Multiplexed requests
- Streaming support - Streaming support
- Bulk operations - Same method signatures as REST where functionality overlaps
- Opt-in via constructor parameter
3. **Bulk Operations Interface** (new)
- `api.bulk()`
- WebSocket-based for efficiency
- Iterator-based import/export
- Handles large datasets
```python ```python
# REST transport (default, backward compatible) # REST interface (existing, unchanged)
api = Api(url="http://localhost:8088/") api = Api(url="http://localhost:8088/")
flow = api.flow().id("default")
response = flow.agent(question="...", user="...")
# WebSocket transport (new, opt-in) # WebSocket interface (new)
api = Api(url="ws://localhost:8088/", transport="websocket") socket = api.socket()
socket_flow = socket.flow("default")
response = socket_flow.agent(question="...", user="...")
for chunk in socket_flow.agent(question="...", user="...", streaming=True):
print(chunk)
# Bulk operations interface (new)
bulk = api.bulk()
bulk.import_triples(flow="default", triples=triple_generator())
``` ```
**Key Design Principles**:
- **Same URL for all interfaces**: `Api(url="http://localhost:8088/")` works for all
- **Identical signatures**: Where functionality overlaps, method signatures are identical
- **Progressive enhancement**: Choose interface based on needs (REST for simple, WebSocket for streaming, Bulk for large datasets)
- **Explicit intent**: `api.socket()` clearly signals WebSocket usage
- **Backward compatible**: Existing code unchanged
### Components ### Components
#### 1. Transport Layer Abstraction #### 1. Core API Class (Modified)
Module: `trustgraph-base/trustgraph/api/transport/` Module: `trustgraph-base/trustgraph/api/api.py`
``` **Enhanced API Class**:
transport/
├── __init__.py
├── base.py # Abstract transport interface
├── rest.py # REST transport (existing behavior)
└── websocket.py # WebSocket transport (new)
```
**Base Transport Interface**:
```python ```python
from abc import ABC, abstractmethod class Api:
from typing import Iterator, Any, Dict def __init__(self, url: str, timeout: int = 60, token: Optional[str] = None):
self.url = url
self.timeout = timeout
self.token = token
self._socket_client = None
self._bulk_client = None
class Transport(ABC): # Existing methods (unchanged)
@abstractmethod def flow(self) -> Flow:
def request(self, endpoint: str, data: Dict[str, Any]) -> Dict[str, Any]: """REST-based flow interface"""
"""Synchronous request/response"""
pass pass
@abstractmethod def library(self) -> Library:
def streaming_request( """REST-based library interface"""
self,
endpoint: str,
data: Dict[str, Any]
) -> Iterator[Dict[str, Any]]:
"""Streaming request/response (yields chunks)"""
pass pass
@abstractmethod def knowledge(self) -> Knowledge:
def bulk_import( """REST-based knowledge interface"""
self,
endpoint: str,
items: Iterator[Dict[str, Any]]
) -> None:
"""Bulk import from iterator"""
pass pass
@abstractmethod def collection(self) -> Collection:
def bulk_export( """REST-based collection interface"""
self,
endpoint: str
) -> Iterator[Dict[str, Any]]:
"""Bulk export as iterator"""
pass pass
@abstractmethod def config(self) -> Config:
"""REST-based config interface"""
pass
# New methods
def socket(self) -> SocketClient:
"""WebSocket-based interface for streaming operations"""
if self._socket_client is None:
self._socket_client = SocketClient(self.url, self.timeout, self.token)
return self._socket_client
def bulk(self) -> BulkClient:
"""Bulk operations interface for import/export"""
if self._bulk_client is None:
self._bulk_client = BulkClient(self.url, self.timeout, self.token)
return self._bulk_client
def metrics(self) -> Metrics:
"""Metrics interface"""
return Metrics(self.url, self.timeout, self.token)
def close(self) -> None: def close(self) -> None:
"""Close connection and cleanup""" """Close all connections"""
if self._socket_client:
self._socket_client.close()
if self._bulk_client:
self._bulk_client.close()
def __enter__(self):
return self
def __exit__(self, *args):
self.close()
```
#### 2. WebSocket Client
Module: `trustgraph-base/trustgraph/api/socket_client.py` (new)
**SocketClient Class**:
```python
class SocketClient:
def __init__(self, url: str, timeout: int, token: Optional[str]):
self.url = self._convert_to_ws_url(url)
self.timeout = timeout
self.token = token
self._connection = None
self._request_counter = 0
def flow(self, flow_id: str) -> SocketFlowInstance:
"""Get flow instance for WebSocket operations"""
return SocketFlowInstance(self, flow_id)
def _connect(self) -> WebSocket:
"""Establish WebSocket connection"""
# Lazy connection establishment
pass
def _send_request(
self,
service: str,
flow: Optional[str],
request: Dict[str, Any],
streaming: bool = False
) -> Union[Dict[str, Any], Iterator[Dict[str, Any]]]:
"""Send request and handle response/streaming"""
pass
def close(self) -> None:
"""Close WebSocket connection"""
pass
class SocketFlowInstance:
"""WebSocket flow instance with same interface as REST FlowInstance"""
def __init__(self, client: SocketClient, flow_id: str):
self.client = client
self.flow_id = flow_id
# Same method signatures as FlowInstance
def agent(
self,
question: str,
user: str,
state: Optional[Dict[str, Any]] = None,
group: Optional[str] = None,
history: Optional[List[Dict[str, Any]]] = None,
streaming: bool = False, # Additional parameter for WebSocket
**kwargs
) -> Union[Dict[str, Any], Iterator[Dict[str, Any]]]:
"""Agent with optional streaming"""
pass
def text_completion(
self,
system: str,
prompt: str,
streaming: bool = False,
**kwargs
) -> Union[str, Iterator[str]]:
"""Text completion with optional streaming"""
pass
# ... similar for graph_rag, document_rag, prompt, etc.
```
**Key Features**:
- Lazy connection (only connects when first request sent)
- Request multiplexing (up to 15 concurrent)
- Automatic reconnection on disconnect
- Streaming response parsing
- Thread-safe operation
#### 3. Bulk Operations Client
Module: `trustgraph-base/trustgraph/api/bulk_client.py` (new)
**BulkClient Class**:
```python
class BulkClient:
def __init__(self, url: str, timeout: int, token: Optional[str]):
self.url = self._convert_to_ws_url(url)
self.timeout = timeout
self.token = token
def import_triples(
self,
flow: str,
triples: Iterator[Triple],
**kwargs
) -> None:
"""Bulk import triples via WebSocket"""
pass
def export_triples(
self,
flow: str,
**kwargs
) -> Iterator[Triple]:
"""Bulk export triples via WebSocket"""
pass
def import_graph_embeddings(
self,
flow: str,
embeddings: Iterator[Dict[str, Any]],
**kwargs
) -> None:
"""Bulk import graph embeddings via WebSocket"""
pass
def export_graph_embeddings(
self,
flow: str,
**kwargs
) -> Iterator[Dict[str, Any]]:
"""Bulk export graph embeddings via WebSocket"""
pass
# ... similar for document embeddings, entity contexts, objects
def close(self) -> None:
"""Close connections"""
pass pass
``` ```
#### 2. REST Transport Implementation
Module: `trustgraph-base/trustgraph/api/transport/rest.py`
- Uses existing `requests` library
- `streaming_request()` raises `NotImplementedError` with helpful message
- `bulk_import()` / `bulk_export()` implemented via REST endpoints where available
- Maintains current behavior for backward compatibility
#### 3. WebSocket Transport Implementation
Module: `trustgraph-base/trustgraph/api/transport/websocket.py`
- Uses `websockets` library (async, but wrapped in sync interface using `asyncio.run()`)
- Maintains persistent connection
- Implements request multiplexing (up to 15 concurrent)
- Supports streaming responses
- Handles WebSocket reconnection
- Parses streaming chunk types (thoughts, observations, final-answer, RAG chunks)
**Key Features**: **Key Features**:
- Automatic request ID generation - Iterator-based for constant memory usage
- Response correlation by ID - Dedicated WebSocket connections per operation
- Graceful error handling - Progress tracking (optional callback)
- Automatic reconnection on disconnect - Error handling with partial success reporting
- Thread-safe for multi-threaded applications
#### 4. Enhanced Flow API #### 4. REST Flow API (Unchanged)
Module: `trustgraph-base/trustgraph/api/flow.py` (modified) Module: `trustgraph-base/trustgraph/api/flow.py`
**New Methods**: The REST Flow API remains **completely unchanged** for backward compatibility. All existing methods continue to work:
- `Flow.list()`, `Flow.start()`, `Flow.stop()`, etc.
- `FlowInstance.agent()`, `FlowInstance.text_completion()`, `FlowInstance.graph_rag()`, etc.
- All existing signatures and return types preserved
**New**: Add `graph_embeddings_query()` to REST FlowInstance for feature parity:
```python ```python
class FlowInstance: class FlowInstance:
# Existing methods remain unchanged # All existing methods unchanged...
# New: Graph embeddings query # New: Graph embeddings query (REST)
def graph_embeddings_query( def graph_embeddings_query(
self, self,
text: str, text: str,
@ -196,192 +347,7 @@ class FlowInstance:
**kwargs **kwargs
) -> List[Dict[str, Any]]: ) -> List[Dict[str, Any]]:
"""Query graph embeddings for semantic search""" """Query graph embeddings for semantic search"""
pass # Calls POST /api/v1/flow/{flow}/service/graph-embeddings
# New: Bulk triple import
def import_triples(
self,
triples: Iterator[Triple],
**kwargs
) -> None:
"""Bulk import triples (requires WebSocket transport)"""
pass
# New: Bulk triple export
def export_triples(
self,
**kwargs
) -> Iterator[Triple]:
"""Bulk export triples (requires WebSocket transport)"""
pass
# New: Bulk graph embeddings import
def import_graph_embeddings(
self,
embeddings: Iterator[Dict[str, Any]],
**kwargs
) -> None:
"""Bulk import graph embeddings (requires WebSocket transport)"""
pass
# New: Bulk graph embeddings export
def export_graph_embeddings(
self,
**kwargs
) -> Iterator[Dict[str, Any]]:
"""Bulk export graph embeddings (requires WebSocket transport)"""
pass
# New: Bulk document embeddings import
def import_document_embeddings(
self,
embeddings: Iterator[Dict[str, Any]],
**kwargs
) -> None:
"""Bulk import document embeddings (requires WebSocket transport)"""
pass
# New: Bulk document embeddings export
def export_document_embeddings(
self,
**kwargs
) -> Iterator[Dict[str, Any]]:
"""Bulk export document embeddings (requires WebSocket transport)"""
pass
# New: Bulk entity contexts import
def import_entity_contexts(
self,
contexts: Iterator[Dict[str, Any]],
**kwargs
) -> None:
"""Bulk import entity contexts (requires WebSocket transport)"""
pass
# New: Bulk entity contexts export
def export_entity_contexts(
self,
**kwargs
) -> Iterator[Dict[str, Any]]:
"""Bulk export entity contexts (requires WebSocket transport)"""
pass
# New: Bulk objects import
def import_objects(
self,
objects: Iterator[Dict[str, Any]],
**kwargs
) -> None:
"""Bulk import objects (requires WebSocket transport)"""
pass
```
**Modified Methods** (add streaming parameter):
```python
class FlowInstance:
def agent(
self,
question: str,
user: str,
state: Optional[Dict[str, Any]] = None,
group: Optional[str] = None,
history: Optional[List[Dict[str, Any]]] = None,
streaming: bool = False, # NEW
**kwargs
) -> Union[Dict[str, Any], Iterator[Dict[str, Any]]]:
"""
Execute agent with optional streaming.
Returns:
- If streaming=False: Dict with final response
- If streaming=True: Iterator yielding chunks
Raises:
NotImplementedError: If streaming=True with REST transport
"""
pass
def text_completion(
self,
system: str,
prompt: str,
streaming: bool = False, # NEW
**kwargs
) -> Union[str, Iterator[str]]:
"""
Text completion with optional streaming.
Returns:
- If streaming=False: Complete response string
- If streaming=True: Iterator yielding token chunks
Raises:
NotImplementedError: If streaming=True with REST transport
"""
pass
def graph_rag(
self,
question: str,
user: str,
collection: str,
max_subgraph_size: int = 1000,
max_subgraph_count: int = 5,
max_entity_distance: int = 3,
streaming: bool = False, # NEW
**kwargs
) -> Union[str, Iterator[str]]:
"""
Graph RAG with optional streaming.
Returns:
- If streaming=False: Complete response string
- If streaming=True: Iterator yielding chunks
Raises:
NotImplementedError: If streaming=True with REST transport
"""
pass
def document_rag(
self,
question: str,
user: str,
collection: str,
doc_limit: int = 10,
streaming: bool = False, # NEW
**kwargs
) -> Union[str, Iterator[str]]:
"""
Document RAG with optional streaming.
Returns:
- If streaming=False: Complete response string
- If streaming=True: Iterator yielding chunks
Raises:
NotImplementedError: If streaming=True with REST transport
"""
pass
def prompt(
self,
id: str,
variables: Dict[str, str],
streaming: bool = False, # NEW
**kwargs
) -> Union[str, Iterator[str]]:
"""
Execute prompt with optional streaming.
Returns:
- If streaming=False: Complete response string
- If streaming=True: Iterator yielding chunks
Raises:
NotImplementedError: If streaming=True with REST transport
"""
pass pass
``` ```
@ -435,8 +401,10 @@ Module: `trustgraph-base/trustgraph/api/metrics.py` (new)
```python ```python
class Metrics: class Metrics:
def __init__(self, transport: Transport): def __init__(self, url: str, timeout: int, token: Optional[str]):
self.transport = transport self.url = url
self.timeout = timeout
self.token = token
def get(self) -> str: def get(self) -> str:
"""Get Prometheus metrics as text""" """Get Prometheus metrics as text"""
@ -446,70 +414,82 @@ class Metrics:
### Implementation Approach ### Implementation Approach
#### Phase 1: Transport Abstraction (Week 1) #### Phase 1: Core API Enhancement (Week 1)
1. Create transport layer abstraction 1. Add `socket()`, `bulk()`, and `metrics()` methods to `Api` class
2. Refactor existing REST code into `RestTransport` class 2. Implement lazy initialization for WebSocket and bulk clients
3. Update `Api` class to use transport abstraction 3. Add context manager support (`__enter__`, `__exit__`)
4. Add unit tests for transport layer 4. Add `close()` method for cleanup
5. Verify backward compatibility 5. Add unit tests for API class enhancements
6. Verify backward compatibility
**Backward Compatibility**: Zero breaking changes. All existing code works unchanged. **Backward Compatibility**: Zero breaking changes. New methods only.
#### Phase 2: WebSocket Transport (Week 2-3) #### Phase 2: WebSocket Client (Week 2-3)
1. Implement `WebSocketTransport` class 1. Implement `SocketClient` class with connection management
2. Add WebSocket connection management 2. Implement `SocketFlowInstance` with same method signatures as `FlowInstance`
3. Implement request multiplexing 3. Add request multiplexing support (up to 15 concurrent)
4. Add streaming response parsing 4. Add streaming response parsing for different chunk types
5. Add unit and integration tests 5. Add automatic reconnection logic
6. Document WebSocket usage patterns 6. Add unit and integration tests
7. Document WebSocket usage patterns
**Backward Compatibility**: Opt-in via constructor parameter. Default remains REST. **Backward Compatibility**: New interface only. Zero impact on existing code.
#### Phase 3: Streaming API Enhancement (Week 3-4) #### Phase 3: Streaming Support (Week 3-4)
1. Add `streaming` parameter to LLM methods (agent, text_completion, graph_rag, document_rag, prompt) 1. Add streaming chunk type classes (`AgentThought`, `AgentObservation`, `AgentAnswer`, `RAGChunk`)
2. Implement streaming response handling 2. Implement streaming response parsing in `SocketClient`
3. Add streaming chunk types and parsers 3. Add streaming parameter to all LLM methods in `SocketFlowInstance`
4. Add unit and integration tests 4. Handle error cases during streaming
5. Add streaming examples to documentation 5. Add unit and integration tests for streaming
6. Add streaming examples to documentation
**Backward Compatibility**: `streaming=False` by default. Existing calls work unchanged. **Backward Compatibility**: New interface only. Existing REST API unchanged.
#### Phase 4: Bulk Operations (Week 4-5) #### Phase 4: Bulk Operations (Week 4-5)
1. Add bulk import/export methods to `FlowInstance` 1. Implement `BulkClient` class
2. Implement iterator-based bulk operations 2. Add bulk import/export methods for triples, embeddings, contexts, objects
3. Add progress tracking (optional callback) 3. Implement iterator-based processing for constant memory
4. Add unit and integration tests 4. Add progress tracking (optional callback)
5. Add bulk operation examples 5. Add error handling with partial success reporting
6. Add unit and integration tests
7. Add bulk operation examples
**Backward Compatibility**: New methods only. Zero impact on existing code. **Backward Compatibility**: New interface only. Zero impact on existing code.
#### Phase 5: Feature Parity (Week 5) #### Phase 5: Feature Parity & Polish (Week 5)
1. Add `graph_embeddings_query()` method 1. Add `graph_embeddings_query()` to REST `FlowInstance`
2. Add `Metrics` API 2. Implement `Metrics` class
3. Comprehensive integration tests 3. Add comprehensive integration tests
4. Update documentation 4. Performance benchmarking
5. Migration guide for users 5. Update all documentation
6. Create migration guide
**Backward Compatibility**: New methods only. Zero impact on existing code. **Backward Compatibility**: New methods only. Zero impact on existing code.
### Data Models ### Data Models
#### Transport Selection #### Interface Selection
```python ```python
# Automatic transport selection based on URL scheme # Single API instance, same URL for all interfaces
api = Api(url="http://localhost:8088/") # Uses RestTransport api = Api(url="http://localhost:8088/")
api = Api(url="https://localhost:8088/") # Uses RestTransport
api = Api(url="ws://localhost:8088/") # Uses WebSocketTransport
api = Api(url="wss://localhost:8088/") # Uses WebSocketTransport (secure)
# Explicit transport override # REST interface (existing)
api = Api(url="http://localhost:8088/", transport="websocket") # Force WebSocket rest_flow = api.flow().id("default")
# WebSocket interface (new)
socket_flow = api.socket().flow("default")
# Bulk operations interface (new)
bulk = api.bulk()
# Metrics interface (new)
metrics = api.metrics()
``` ```
#### Streaming Response Types #### Streaming Response Types
@ -517,14 +497,20 @@ api = Api(url="http://localhost:8088/", transport="websocket") # Force WebSocke
**Agent Streaming**: **Agent Streaming**:
```python ```python
flow = api.flow().id("default") api = Api(url="http://localhost:8088/")
# Non-streaming (existing) # REST interface - non-streaming (existing)
response = flow.agent(question="What is ML?", user="user123") rest_flow = api.flow().id("default")
response = rest_flow.agent(question="What is ML?", user="user123")
print(response["response"]) print(response["response"])
# Streaming (new) # WebSocket interface - non-streaming (same signature)
for chunk in flow.agent(question="What is ML?", user="user123", streaming=True): socket_flow = api.socket().flow("default")
response = socket_flow.agent(question="What is ML?", user="user123")
print(response["response"])
# WebSocket interface - streaming (new)
for chunk in socket_flow.agent(question="What is ML?", user="user123", streaming=True):
if isinstance(chunk, AgentThought): if isinstance(chunk, AgentThought):
print(f"Thinking: {chunk.content}") print(f"Thinking: {chunk.content}")
elif isinstance(chunk, AgentObservation): elif isinstance(chunk, AgentObservation):
@ -538,12 +524,16 @@ for chunk in flow.agent(question="What is ML?", user="user123", streaming=True):
**RAG Streaming**: **RAG Streaming**:
```python ```python
# Non-streaming (existing) api = Api(url="http://localhost:8088/")
response = flow.graph_rag(question="What is Python?", user="user123", collection="default")
# REST interface - non-streaming (existing)
rest_flow = api.flow().id("default")
response = rest_flow.graph_rag(question="What is Python?", user="user123", collection="default")
print(response) print(response)
# Streaming (new) # WebSocket interface - streaming (new)
for chunk in flow.graph_rag( socket_flow = api.socket().flow("default")
for chunk in socket_flow.graph_rag(
question="What is Python?", question="What is Python?",
user="user123", user="user123",
collection="default", collection="default",
@ -557,17 +547,19 @@ for chunk in flow.graph_rag(
**Bulk Operations**: **Bulk Operations**:
```python ```python
api = Api(url="http://localhost:8088/")
# Bulk import triples # Bulk import triples
def triple_generator(): def triple_generator():
yield Triple(s="http://ex.com/alice", p="http://ex.com/type", o="Person") yield Triple(s="http://ex.com/alice", p="http://ex.com/type", o="Person")
yield Triple(s="http://ex.com/alice", p="http://ex.com/name", o="Alice") yield Triple(s="http://ex.com/alice", p="http://ex.com/name", o="Alice")
yield Triple(s="http://ex.com/bob", p="http://ex.com/type", o="Person") yield Triple(s="http://ex.com/bob", p="http://ex.com/type", o="Person")
flow = api.flow().id("default") bulk = api.bulk()
flow.import_triples(triple_generator()) bulk.import_triples(flow="default", triples=triple_generator())
# Bulk export triples # Bulk export triples
for triple in flow.export_triples(): for triple in bulk.export_triples(flow="default"):
print(f"{triple.s} -> {triple.p} -> {triple.o}") print(f"{triple.s} -> {triple.p} -> {triple.o}")
``` ```
@ -575,79 +567,95 @@ for triple in flow.export_triples():
#### New APIs #### New APIs
1. **Transport Layer**: 1. **Core API Class**:
- `Transport` (abstract base class) - `Api.socket()` - Get WebSocket client for streaming operations
- `RestTransport` (REST implementation) - `Api.bulk()` - Get bulk operations client for import/export
- `WebSocketTransport` (WebSocket implementation) - `Api.metrics()` - Get metrics client
- `Api.close()` - Close all connections
- Context manager support (`__enter__`, `__exit__`)
2. **Flow-Scoped Services**: 2. **WebSocket Client**:
- `FlowInstance.graph_embeddings_query()` - Semantic graph search - `SocketClient.flow(flow_id)` - Get WebSocket flow instance
- `FlowInstance.import_triples()` - Bulk triple import - `SocketFlowInstance.agent(..., streaming: bool = False)` - Agent with optional streaming
- `FlowInstance.export_triples()` - Bulk triple export - `SocketFlowInstance.text_completion(..., streaming: bool = False)` - Text completion with optional streaming
- `FlowInstance.import_graph_embeddings()` - Bulk graph embeddings import - `SocketFlowInstance.graph_rag(..., streaming: bool = False)` - Graph RAG with optional streaming
- `FlowInstance.export_graph_embeddings()` - Bulk graph embeddings export - `SocketFlowInstance.document_rag(..., streaming: bool = False)` - Document RAG with optional streaming
- `FlowInstance.import_document_embeddings()` - Bulk document embeddings import - `SocketFlowInstance.prompt(..., streaming: bool = False)` - Prompt with optional streaming
- `FlowInstance.export_document_embeddings()` - Bulk document embeddings export - `SocketFlowInstance.graph_embeddings_query()` - Graph embeddings query
- `FlowInstance.import_entity_contexts()` - Bulk entity contexts import - All other FlowInstance methods with identical signatures
- `FlowInstance.export_entity_contexts()` - Bulk entity contexts export
- `FlowInstance.import_objects()` - Bulk objects import
3. **Metrics**: 3. **Bulk Operations Client**:
- `Api.metrics()` - Access metrics API - `BulkClient.import_triples(flow, triples)` - Bulk triple import
- `Metrics.get()` - Get Prometheus metrics - `BulkClient.export_triples(flow)` - Bulk triple export
- `BulkClient.import_graph_embeddings(flow, embeddings)` - Bulk graph embeddings import
- `BulkClient.export_graph_embeddings(flow)` - Bulk graph embeddings export
- `BulkClient.import_document_embeddings(flow, embeddings)` - Bulk document embeddings import
- `BulkClient.export_document_embeddings(flow)` - Bulk document embeddings export
- `BulkClient.import_entity_contexts(flow, contexts)` - Bulk entity contexts import
- `BulkClient.export_entity_contexts(flow)` - Bulk entity contexts export
- `BulkClient.import_objects(flow, objects)` - Bulk objects import
4. **Metrics Client**:
- `Metrics.get()` - Get Prometheus metrics as text
5. **REST Flow API Enhancement**:
- `FlowInstance.graph_embeddings_query()` - Graph embeddings query (feature parity)
#### Modified APIs #### Modified APIs
1. **Constructor**: 1. **Constructor** (minor enhancement):
```python ```python
Api(url: str, timeout: int = 60, transport: Optional[str] = None, token: Optional[str] = None) Api(url: str, timeout: int = 60, token: Optional[str] = None)
``` ```
- Added `transport` parameter (optional, auto-detected from URL scheme) - Added `token` parameter (optional, for authentication)
- Added `token` parameter (optional, for WebSocket query param auth) - No other changes - fully backward compatible
2. **LLM Services** (add `streaming` parameter): 2. **No Breaking Changes**:
- `FlowInstance.agent(..., streaming: bool = False)` - All existing REST API methods unchanged
- `FlowInstance.text_completion(..., streaming: bool = False)` - All existing signatures preserved
- `FlowInstance.graph_rag(..., streaming: bool = False)` - All existing return types preserved
- `FlowInstance.document_rag(..., streaming: bool = False)`
- `FlowInstance.prompt(..., streaming: bool = False)`
### Implementation Details ### Implementation Details
#### Error Handling #### Error Handling
**Transport Not Supported**: **WebSocket Connection Errors**:
```python ```python
try: try:
api = Api(url="http://localhost:8088/", transport="rest") api = Api(url="http://localhost:8088/")
flow = api.flow().id("default") socket = api.socket()
socket_flow = socket.flow("default")
# This raises NotImplementedError response = socket_flow.agent(question="...", user="user123")
for chunk in flow.agent(question="...", streaming=True):
print(chunk)
except NotImplementedError as e:
print(f"Error: {e}")
print("Hint: Use WebSocket transport for streaming: Api(url='ws://localhost:8088/')")
```
**Connection Errors**:
```python
try:
api = Api(url="ws://localhost:8088/")
flow = api.flow().id("default")
response = flow.agent(question="...", user="user123")
except ConnectionError as e: except ConnectionError as e:
print(f"WebSocket connection failed: {e}") print(f"WebSocket connection failed: {e}")
print("Hint: Ensure Gateway is running and WebSocket endpoint is accessible") print("Hint: Ensure Gateway is running and WebSocket endpoint is accessible")
``` ```
**Graceful Fallback**:
```python
api = Api(url="http://localhost:8088/")
try:
# Try WebSocket streaming first
socket_flow = api.socket().flow("default")
for chunk in socket_flow.agent(question="...", user="...", streaming=True):
print(chunk.content)
except ConnectionError:
# Fall back to REST non-streaming
print("WebSocket unavailable, falling back to REST")
rest_flow = api.flow().id("default")
response = rest_flow.agent(question="...", user="...")
print(response["response"])
```
**Partial Streaming Errors**: **Partial Streaming Errors**:
```python ```python
flow = api.flow().id("default") api = Api(url="http://localhost:8088/")
socket_flow = api.socket().flow("default")
accumulated = [] accumulated = []
try: try:
for chunk in flow.graph_rag(question="...", streaming=True): for chunk in socket_flow.graph_rag(question="...", streaming=True):
accumulated.append(chunk.content) accumulated.append(chunk.content)
if chunk.error: if chunk.error:
print(f"Error occurred: {chunk.error}") print(f"Error occurred: {chunk.error}")
@ -663,18 +671,18 @@ except Exception as e:
**Context Manager Support**: **Context Manager Support**:
```python ```python
# Automatic cleanup # Automatic cleanup
with Api(url="ws://localhost:8088/") as api: with Api(url="http://localhost:8088/") as api:
flow = api.flow().id("default") socket_flow = api.socket().flow("default")
response = flow.agent(question="...", user="user123") response = socket_flow.agent(question="...", user="user123")
# WebSocket connection automatically closed # All connections automatically closed
# Manual cleanup # Manual cleanup
api = Api(url="ws://localhost:8088/") api = Api(url="http://localhost:8088/")
try: try:
flow = api.flow().id("default") socket_flow = api.socket().flow("default")
response = flow.agent(question="...", user="user123") response = socket_flow.agent(question="...", user="user123")
finally: finally:
api.close() # Explicitly close WebSocket connection api.close() # Explicitly close all connections (WebSocket, bulk, etc.)
``` ```
#### Threading and Concurrency #### Threading and Concurrency
@ -868,41 +876,49 @@ response = flow.agent(question="What is ML?", user="user123")
### Phase 2: Opt-in Streaming (Simple) ### Phase 2: Opt-in Streaming (Simple)
**Add `streaming=True` parameter** to enable streaming: **Use `api.socket()` interface** to enable streaming:
```python ```python
# Before: Non-streaming # Before: Non-streaming REST
response = flow.agent(question="What is ML?", user="user123") api = Api(url="http://localhost:8088/")
rest_flow = api.flow().id("default")
response = rest_flow.agent(question="What is ML?", user="user123")
print(response["response"]) print(response["response"])
# After: Streaming # After: Streaming WebSocket (same parameters!)
api = Api(url="ws://localhost:8088/") # Use WebSocket transport api = Api(url="http://localhost:8088/") # Same URL
flow = api.flow().id("default") socket_flow = api.socket().flow("default")
for chunk in flow.agent(question="What is ML?", user="user123", streaming=True): for chunk in socket_flow.agent(question="What is ML?", user="user123", streaming=True):
if isinstance(chunk, AgentAnswer): if isinstance(chunk, AgentAnswer):
print(chunk.content, end="", flush=True) print(chunk.content, end="", flush=True)
``` ```
**Key Points**:
- Same URL for both REST and WebSocket
- Same method signatures (easy migration)
- Just add `.socket()` and `streaming=True`
### Phase 3: Bulk Operations (New Capability) ### Phase 3: Bulk Operations (New Capability)
**Use new bulk methods** for large datasets: **Use `api.bulk()` interface** for large datasets:
```python ```python
# Before: Inefficient per-item loading # Before: Inefficient per-item operations
api = Api(url="http://localhost:8088/") api = Api(url="http://localhost:8088/")
flow = api.flow().id("default") flow = api.flow().id("default")
for triple in my_large_triple_list: for triple in my_large_triple_list:
# This is slow (100 triples/second) # Slow per-item operations
flow.triples_query(...) # Not ideal for bulk insert # (no direct bulk insert in REST API)
pass
# After: Efficient bulk loading # After: Efficient bulk loading
api = Api(url="ws://localhost:8088/") api = Api(url="http://localhost:8088/") # Same URL
flow = api.flow().id("default") bulk = api.bulk()
# This is fast (10,000 triples/second) # This is fast (10,000 triples/second)
flow.import_triples(iter(my_large_triple_list)) bulk.import_triples(flow="default", triples=iter(my_large_triple_list))
``` ```
### Documentation Updates ### Documentation Updates