filters and settings + ci fixes

This commit is contained in:
phact 2025-12-17 10:10:01 -05:00
parent 956674e0ae
commit 648074d3c8
15 changed files with 1054 additions and 2 deletions

View file

@ -7,6 +7,7 @@ on:
- 'tests/**.py'
- 'pyproject.toml'
- 'uv.lock'
- 'sdks/**'
- '.github/workflows/test-integration.yml'
workflow_dispatch:
inputs:
@ -58,6 +59,11 @@ jobs:
with:
version: latest
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Python version
run: uv python install 3.13

View file

@ -36,6 +36,7 @@ from .exceptions import (
ServerError,
ValidationError,
)
from .knowledge_filters import KnowledgeFiltersClient
from .models import (
AgentSettings,
ChatResponse,
@ -43,18 +44,28 @@ from .models import (
Conversation,
ConversationDetail,
ConversationListResponse,
CreateKnowledgeFilterOptions,
CreateKnowledgeFilterResponse,
DeleteDocumentResponse,
DeleteKnowledgeFilterResponse,
DoneEvent,
GetKnowledgeFilterResponse,
IngestResponse,
KnowledgeFilter,
KnowledgeFilterQueryData,
KnowledgeFilterSearchResponse,
KnowledgeSettings,
Message,
SearchFilters,
SearchResponse,
SearchResult,
SettingsResponse,
SettingsUpdateOptions,
SettingsUpdateResponse,
Source,
SourcesEvent,
StreamEvent,
UpdateKnowledgeFilterOptions,
)
__version__ = "0.1.0"
@ -62,6 +73,8 @@ __version__ = "0.1.0"
__all__ = [
# Main client
"OpenRAGClient",
# Sub-clients
"KnowledgeFiltersClient",
# Exceptions
"OpenRAGError",
"AuthenticationError",
@ -86,6 +99,17 @@ __all__ = [
"ConversationListResponse",
"Message",
"SettingsResponse",
"SettingsUpdateOptions",
"SettingsUpdateResponse",
"AgentSettings",
"KnowledgeSettings",
# Knowledge filter models
"KnowledgeFilter",
"KnowledgeFilterQueryData",
"CreateKnowledgeFilterOptions",
"UpdateKnowledgeFilterOptions",
"CreateKnowledgeFilterResponse",
"KnowledgeFilterSearchResponse",
"GetKnowledgeFilterResponse",
"DeleteKnowledgeFilterResponse",
]

View file

@ -58,6 +58,7 @@ class ChatStream:
filters: SearchFilters | dict[str, Any] | None = None,
limit: int = 10,
score_threshold: float = 0,
filter_id: str | None = None,
):
self._client = client
self._message = message
@ -65,6 +66,7 @@ class ChatStream:
self._filters = filters
self._limit = limit
self._score_threshold = score_threshold
self._filter_id = filter_id
# Aggregated data
self._text = ""
@ -105,6 +107,9 @@ class ChatStream:
else:
body["filters"] = self._filters
if self._filter_id:
body["filter_id"] = self._filter_id
self._response = await self._client._http.send(
self._client._http.build_request(
"POST",
@ -214,6 +219,7 @@ class ChatClient:
filters: SearchFilters | dict[str, Any] | None = None,
limit: int = 10,
score_threshold: float = 0,
filter_id: str | None = None,
) -> ChatResponse: ...
@overload
@ -226,6 +232,7 @@ class ChatClient:
filters: SearchFilters | dict[str, Any] | None = None,
limit: int = 10,
score_threshold: float = 0,
filter_id: str | None = None,
) -> AsyncIterator[StreamEvent]: ...
async def create(
@ -237,6 +244,7 @@ class ChatClient:
filters: SearchFilters | dict[str, Any] | None = None,
limit: int = 10,
score_threshold: float = 0,
filter_id: str | None = None,
) -> ChatResponse | AsyncIterator[StreamEvent]:
"""
Send a chat message.
@ -248,6 +256,7 @@ class ChatClient:
filters: Optional search filters (data_sources, document_types).
limit: Maximum number of search results (default 10).
score_threshold: Minimum search score threshold (default 0).
filter_id: Optional knowledge filter ID to apply.
Returns:
ChatResponse if stream=False, AsyncIterator[StreamEvent] if stream=True.
@ -269,6 +278,7 @@ class ChatClient:
filters=filters,
limit=limit,
score_threshold=score_threshold,
filter_id=filter_id,
)
else:
return await self._create_response(
@ -277,6 +287,7 @@ class ChatClient:
filters=filters,
limit=limit,
score_threshold=score_threshold,
filter_id=filter_id,
)
async def _create_response(
@ -286,6 +297,7 @@ class ChatClient:
filters: SearchFilters | dict[str, Any] | None,
limit: int,
score_threshold: float,
filter_id: str | None = None,
) -> ChatResponse:
"""Send a non-streaming chat message."""
body: dict[str, Any] = {
@ -304,6 +316,9 @@ class ChatClient:
else:
body["filters"] = filters
if filter_id:
body["filter_id"] = filter_id
response = await self._client._request(
"POST",
"/api/v1/chat",
@ -326,6 +341,7 @@ class ChatClient:
filters: SearchFilters | dict[str, Any] | None,
limit: int,
score_threshold: float,
filter_id: str | None = None,
) -> AsyncIterator[StreamEvent]:
"""Stream a chat response as an async iterator."""
body: dict[str, Any] = {
@ -344,6 +360,9 @@ class ChatClient:
else:
body["filters"] = filters
if filter_id:
body["filter_id"] = filter_id
async with self._client._http.stream(
"POST",
f"{self._client._base_url}/api/v1/chat",
@ -387,6 +406,7 @@ class ChatClient:
filters: SearchFilters | dict[str, Any] | None = None,
limit: int = 10,
score_threshold: float = 0,
filter_id: str | None = None,
) -> ChatStream:
"""
Create a streaming chat context manager.
@ -397,6 +417,7 @@ class ChatClient:
filters: Optional search filters (data_sources, document_types).
limit: Maximum number of search results (default 10).
score_threshold: Minimum search score threshold (default 0).
filter_id: Optional knowledge filter ID to apply.
Returns:
ChatStream context manager.
@ -418,6 +439,7 @@ class ChatClient:
filters=filters,
limit=limit,
score_threshold=score_threshold,
filter_id=filter_id,
)
async def list(self) -> ConversationListResponse:

View file

@ -15,6 +15,7 @@ from .exceptions import (
ServerError,
ValidationError,
)
from .knowledge_filters import KnowledgeFiltersClient
from .search import SearchClient
@ -26,7 +27,7 @@ class SettingsClient:
async def get(self):
"""
Get current OpenRAG configuration (read-only).
Get current OpenRAG configuration.
Returns:
SettingsResponse with agent and knowledge settings.
@ -37,6 +38,27 @@ class SettingsClient:
data = response.json()
return SettingsResponse(**data)
async def update(self, options):
"""
Update OpenRAG configuration.
Args:
options: SettingsUpdateOptions or dict with settings to update.
Returns:
SettingsUpdateResponse with success message.
"""
from .models import SettingsUpdateOptions, SettingsUpdateResponse
if isinstance(options, SettingsUpdateOptions):
body = options.model_dump(exclude_none=True)
else:
body = {k: v for k, v in options.items() if v is not None}
response = await self._client._request("POST", "/settings", json=body)
data = response.json()
return SettingsUpdateResponse(message=data.get("message", "Settings updated"))
class OpenRAGClient:
"""
@ -110,6 +132,7 @@ class OpenRAGClient:
self.search = SearchClient(self)
self.documents = DocumentsClient(self)
self.settings = SettingsClient(self)
self.knowledge_filters = KnowledgeFiltersClient(self)
@property
def _headers(self) -> dict[str, str]:

View file

@ -0,0 +1,230 @@
"""OpenRAG SDK knowledge filters client."""
import json
from typing import TYPE_CHECKING, Any
from .models import (
CreateKnowledgeFilterOptions,
CreateKnowledgeFilterResponse,
DeleteKnowledgeFilterResponse,
GetKnowledgeFilterResponse,
KnowledgeFilter,
KnowledgeFilterQueryData,
KnowledgeFilterSearchResponse,
UpdateKnowledgeFilterOptions,
)
if TYPE_CHECKING:
from .client import OpenRAGClient
class KnowledgeFiltersClient:
"""Client for knowledge filter operations."""
def __init__(self, client: "OpenRAGClient"):
self._client = client
async def create(
self,
options: CreateKnowledgeFilterOptions | dict[str, Any],
) -> CreateKnowledgeFilterResponse:
"""
Create a new knowledge filter.
Args:
options: The filter options including name and query_data.
Returns:
The created filter response with ID.
"""
if isinstance(options, CreateKnowledgeFilterOptions):
name = options.name
description = options.description or ""
query_data = options.query_data
else:
name = options["name"]
description = options.get("description", "")
query_data = options.get("query_data") or options.get("queryData", {})
# Convert query_data to JSON string if it's a model
if isinstance(query_data, KnowledgeFilterQueryData):
query_data_str = query_data.model_dump_json(by_alias=True, exclude_none=True)
elif isinstance(query_data, dict):
query_data_str = json.dumps(query_data)
else:
query_data_str = str(query_data)
body = {
"name": name,
"description": description,
"queryData": query_data_str,
}
response = await self._client._request(
"POST",
"/knowledge-filter",
json=body,
)
data = response.json()
return CreateKnowledgeFilterResponse(
success=data.get("success", False),
id=data.get("id"),
error=data.get("error"),
)
async def search(
self,
query: str = "",
limit: int = 20,
) -> list[KnowledgeFilter]:
"""
Search for knowledge filters by name, description, or query content.
Args:
query: Optional search query text.
limit: Maximum number of results (default 20).
Returns:
List of matching knowledge filters.
"""
body = {
"query": query,
"limit": limit,
}
response = await self._client._request(
"POST",
"/knowledge-filter/search",
json=body,
)
data = response.json()
if not data.get("success") or not data.get("filters"):
return []
return [self._parse_filter(f) for f in data["filters"]]
async def get(self, filter_id: str) -> KnowledgeFilter | None:
"""
Get a specific knowledge filter by ID.
Args:
filter_id: The ID of the filter to retrieve.
Returns:
The knowledge filter or None if not found.
"""
try:
response = await self._client._request(
"GET",
f"/knowledge-filter/{filter_id}",
)
data = response.json()
if not data.get("success") or not data.get("filter"):
return None
return self._parse_filter(data["filter"])
except Exception:
# Filter not found or other error
return None
async def update(
self,
filter_id: str,
options: UpdateKnowledgeFilterOptions | dict[str, Any],
) -> bool:
"""
Update an existing knowledge filter.
Args:
filter_id: The ID of the filter to update.
options: The fields to update.
Returns:
Success status.
"""
body: dict[str, Any] = {}
if isinstance(options, UpdateKnowledgeFilterOptions):
if options.name is not None:
body["name"] = options.name
if options.description is not None:
body["description"] = options.description
if options.query_data is not None:
body["queryData"] = options.query_data.model_dump_json(
by_alias=True, exclude_none=True
)
else:
if "name" in options:
body["name"] = options["name"]
if "description" in options:
body["description"] = options["description"]
query_data = options.get("query_data") or options.get("queryData")
if query_data is not None:
if isinstance(query_data, KnowledgeFilterQueryData):
body["queryData"] = query_data.model_dump_json(
by_alias=True, exclude_none=True
)
elif isinstance(query_data, dict):
body["queryData"] = json.dumps(query_data)
else:
body["queryData"] = str(query_data)
response = await self._client._request(
"PUT",
f"/knowledge-filter/{filter_id}",
json=body,
)
data = response.json()
return data.get("success", False)
async def delete(self, filter_id: str) -> bool:
"""
Delete a knowledge filter.
Args:
filter_id: The ID of the filter to delete.
Returns:
Success status.
"""
response = await self._client._request(
"DELETE",
f"/knowledge-filter/{filter_id}",
)
data = response.json()
return data.get("success", False)
def _parse_filter(self, data: dict[str, Any]) -> KnowledgeFilter:
"""Parse a filter from API response, handling JSON-stringified query_data."""
query_data = data.get("query_data") or data.get("queryData")
if isinstance(query_data, str):
try:
query_data = json.loads(query_data)
except json.JSONDecodeError:
query_data = {}
parsed_query_data = None
if query_data:
parsed_query_data = KnowledgeFilterQueryData(
query=query_data.get("query"),
filters=query_data.get("filters"),
limit=query_data.get("limit"),
score_threshold=query_data.get("scoreThreshold"),
color=query_data.get("color"),
icon=query_data.get("icon"),
)
return KnowledgeFilter(
id=data["id"],
name=data["name"],
description=data.get("description"),
query_data=parsed_query_data,
owner=data.get("owner"),
created_at=data.get("created_at") or data.get("createdAt"),
updated_at=data.get("updated_at") or data.get("updatedAt"),
)

View file

@ -158,3 +158,103 @@ class SearchFilters(BaseModel):
data_sources: list[str] | None = None
document_types: list[str] | None = None
# Settings update models
class SettingsUpdateOptions(BaseModel):
"""Options for updating settings."""
llm_model: str | None = None
llm_provider: str | None = None
system_prompt: str | None = None
embedding_model: str | None = None
embedding_provider: str | None = None
chunk_size: int | None = None
chunk_overlap: int | None = None
table_structure: bool | None = None
ocr: bool | None = None
picture_descriptions: bool | None = None
class SettingsUpdateResponse(BaseModel):
"""Response from settings update."""
message: str
# Knowledge filter models
class KnowledgeFilterQueryData(BaseModel):
"""Query configuration stored in a knowledge filter."""
query: str | None = None
filters: dict[str, list[str]] | None = None
limit: int | None = None
score_threshold: float | None = Field(default=None, alias="scoreThreshold")
color: str | None = None
icon: str | None = None
model_config = {"populate_by_name": True}
class KnowledgeFilter(BaseModel):
"""A knowledge filter definition."""
id: str
name: str
description: str | None = None
query_data: KnowledgeFilterQueryData | None = Field(default=None, alias="queryData")
owner: str | None = None
created_at: str | None = Field(default=None, alias="createdAt")
updated_at: str | None = Field(default=None, alias="updatedAt")
model_config = {"populate_by_name": True}
class CreateKnowledgeFilterOptions(BaseModel):
"""Options for creating a knowledge filter."""
name: str
description: str | None = None
query_data: KnowledgeFilterQueryData = Field(alias="queryData")
model_config = {"populate_by_name": True}
class UpdateKnowledgeFilterOptions(BaseModel):
"""Options for updating a knowledge filter."""
name: str | None = None
description: str | None = None
query_data: KnowledgeFilterQueryData | None = Field(default=None, alias="queryData")
model_config = {"populate_by_name": True}
class CreateKnowledgeFilterResponse(BaseModel):
"""Response from creating a knowledge filter."""
success: bool
id: str | None = None
error: str | None = None
class KnowledgeFilterSearchResponse(BaseModel):
"""Response from searching knowledge filters."""
success: bool
filters: list[KnowledgeFilter] = Field(default_factory=list)
class GetKnowledgeFilterResponse(BaseModel):
"""Response from getting a knowledge filter."""
success: bool
filter: KnowledgeFilter | None = None
error: str | None = None
class DeleteKnowledgeFilterResponse(BaseModel):
"""Response from deleting a knowledge filter."""
success: bool
error: str | None = None

View file

@ -23,6 +23,7 @@ class SearchClient:
filters: SearchFilters | dict[str, Any] | None = None,
limit: int = 10,
score_threshold: float = 0,
filter_id: str | None = None,
) -> SearchResponse:
"""
Perform semantic search on documents.
@ -32,6 +33,7 @@ class SearchClient:
filters: Optional filters (data_sources, document_types).
limit: Maximum number of results (default 10).
score_threshold: Minimum score threshold (default 0).
filter_id: Optional knowledge filter ID to apply.
Returns:
SearchResponse containing the search results.
@ -48,6 +50,9 @@ class SearchClient:
else:
body["filters"] = filters
if filter_id:
body["filter_id"] = filter_id
response = await self._client._request(
"POST",
"/api/v1/search",

View file

@ -73,6 +73,124 @@ class TestSettings:
assert settings.agent is not None
assert settings.knowledge is not None
@pytest.mark.asyncio
async def test_update_settings(self, client):
"""Test updating settings."""
# Get current settings first
current_settings = await client.settings.get()
current_chunk_size = current_settings.knowledge.chunk_size or 1000
# Update with the same value (safe for tests)
result = await client.settings.update({"chunk_size": current_chunk_size})
assert result.message is not None
# Verify the setting persisted
updated_settings = await client.settings.get()
assert updated_settings.knowledge.chunk_size == current_chunk_size
class TestKnowledgeFilters:
"""Test knowledge filter operations."""
@pytest.mark.asyncio
async def test_knowledge_filter_crud(self, client):
"""Test create, read, update, delete for knowledge filters."""
# Create
create_result = await client.knowledge_filters.create({
"name": "Python SDK Test Filter",
"description": "Filter created by Python SDK integration tests",
"queryData": {
"query": "test documents",
"limit": 10,
"scoreThreshold": 0.5,
},
})
assert create_result.success is True
assert create_result.id is not None
filter_id = create_result.id
# Search
filters = await client.knowledge_filters.search("Python SDK Test")
assert isinstance(filters, list)
found = any(f.name == "Python SDK Test Filter" for f in filters)
assert found is True
# Get
filter_obj = await client.knowledge_filters.get(filter_id)
assert filter_obj is not None
assert filter_obj.id == filter_id
assert filter_obj.name == "Python SDK Test Filter"
# Update
update_success = await client.knowledge_filters.update(
filter_id,
{"description": "Updated description from Python SDK test"},
)
assert update_success is True
# Verify update
updated_filter = await client.knowledge_filters.get(filter_id)
assert updated_filter.description == "Updated description from Python SDK test"
# Delete
delete_success = await client.knowledge_filters.delete(filter_id)
assert delete_success is True
# Verify deletion
deleted_filter = await client.knowledge_filters.get(filter_id)
assert deleted_filter is None
@pytest.mark.asyncio
async def test_filter_id_in_chat(self, client):
"""Test using filter_id in chat."""
# Create a filter first
create_result = await client.knowledge_filters.create({
"name": "Chat Test Filter Python",
"description": "Filter for testing chat with filter_id",
"queryData": {
"query": "test",
"limit": 5,
},
})
assert create_result.success is True
filter_id = create_result.id
try:
# Use filter in chat
response = await client.chat.create(
message="Hello with filter",
filter_id=filter_id,
)
assert response.response is not None
finally:
# Cleanup
await client.knowledge_filters.delete(filter_id)
@pytest.mark.asyncio
async def test_filter_id_in_search(self, client):
"""Test using filter_id in search."""
# Create a filter first
create_result = await client.knowledge_filters.create({
"name": "Search Test Filter Python",
"description": "Filter for testing search with filter_id",
"queryData": {
"query": "test",
"limit": 5,
},
})
assert create_result.success is True
filter_id = create_result.id
try:
# Use filter in search
results = await client.search.query("test query", filter_id=filter_id)
assert results.results is not None
finally:
# Cleanup
await client.knowledge_filters.delete(filter_id)
class TestDocuments:
"""Test document operations."""
@ -222,3 +340,30 @@ class TestChat:
assert result.conversations is not None
assert isinstance(result.conversations, list)
@pytest.mark.asyncio
async def test_get_conversation(self, client):
"""Test getting a specific conversation."""
# Create a conversation first
response = await client.chat.create(message="Test message for get.")
assert response.chat_id is not None
# Get the conversation
conversation = await client.chat.get(response.chat_id)
assert conversation.chat_id == response.chat_id
assert conversation.messages is not None
assert isinstance(conversation.messages, list)
assert len(conversation.messages) >= 1
@pytest.mark.asyncio
async def test_delete_conversation(self, client):
"""Test deleting a conversation."""
# Create a conversation first
response = await client.chat.create(message="Test message for delete.")
assert response.chat_id is not None
# Delete the conversation
result = await client.chat.delete(response.chat_id)
assert result is True

View file

@ -75,6 +75,10 @@ export class ChatStream implements AsyncIterable<StreamEvent>, Disposable {
body["filters"] = this.options.filters;
}
if (this.options.filterId) {
body["filter_id"] = this.options.filterId;
}
this._response = await this.client._request("POST", "/api/v1/chat", {
body: JSON.stringify(body),
stream: true,
@ -218,6 +222,10 @@ export class ChatClient {
body["filters"] = options.filters;
}
if (options.filterId) {
body["filter_id"] = options.filterId;
}
const response = await this.client._request("POST", "/api/v1/chat", {
body: JSON.stringify(body),
});

View file

@ -5,6 +5,7 @@
import { ChatClient } from "./chat";
import { DocumentsClient } from "./documents";
import { SearchClient } from "./search";
import { KnowledgeFiltersClient } from "./knowledge-filters";
import {
AuthenticationError,
NotFoundError,
@ -13,6 +14,8 @@ import {
RateLimitError,
ServerError,
SettingsResponse,
SettingsUpdateOptions,
SettingsUpdateResponse,
ValidationError,
} from "./types";
@ -31,7 +34,7 @@ class SettingsClient {
constructor(private client: OpenRAGClient) {}
/**
* Get current OpenRAG configuration (read-only).
* Get current OpenRAG configuration.
*/
async get(): Promise<SettingsResponse> {
const response = await this.client._request("GET", "/api/v1/settings");
@ -41,6 +44,22 @@ class SettingsClient {
knowledge: data.knowledge || {},
};
}
/**
* Update OpenRAG configuration.
*
* @param options - The settings to update.
* @returns Success response with message.
*/
async update(options: SettingsUpdateOptions): Promise<SettingsUpdateResponse> {
const response = await this.client._request("POST", "/settings", {
body: JSON.stringify(options),
});
const data = await response.json();
return {
message: data.message || "Settings updated",
};
}
}
interface RequestOptions {
@ -84,6 +103,8 @@ export class OpenRAGClient {
readonly documents: DocumentsClient;
/** Settings client for configuration. */
readonly settings: SettingsClient;
/** Knowledge filters client for managing filters. */
readonly knowledgeFilters: KnowledgeFiltersClient;
constructor(options: OpenRAGClientOptions = {}) {
// Resolve API key from argument or environment
@ -108,6 +129,7 @@ export class OpenRAGClient {
this.search = new SearchClient(this);
this.documents = new DocumentsClient(this);
this.settings = new SettingsClient(this);
this.knowledgeFilters = new KnowledgeFiltersClient(this);
}
/** @internal Get request headers with authentication. */

View file

@ -37,6 +37,7 @@ export { OpenRAGClient } from "./client";
export { ChatClient, ChatStream } from "./chat";
export { SearchClient } from "./search";
export { DocumentsClient } from "./documents";
export { KnowledgeFiltersClient } from "./knowledge-filters";
export {
// Error types
@ -71,6 +72,17 @@ export {
Message,
// Settings types
SettingsResponse,
SettingsUpdateOptions,
SettingsUpdateResponse,
AgentSettings,
KnowledgeSettings,
// Knowledge filter types
KnowledgeFilter,
KnowledgeFilterQueryData,
CreateKnowledgeFilterOptions,
UpdateKnowledgeFilterOptions,
CreateKnowledgeFilterResponse,
KnowledgeFilterSearchResponse,
GetKnowledgeFilterResponse,
DeleteKnowledgeFilterResponse,
} from "./types";

View file

@ -0,0 +1,177 @@
/**
* OpenRAG SDK knowledge filters client.
*/
import type { OpenRAGClient } from "./client";
import type {
CreateKnowledgeFilterOptions,
CreateKnowledgeFilterResponse,
DeleteKnowledgeFilterResponse,
GetKnowledgeFilterResponse,
KnowledgeFilter,
KnowledgeFilterSearchResponse,
UpdateKnowledgeFilterOptions,
} from "./types";
export class KnowledgeFiltersClient {
constructor(private client: OpenRAGClient) {}
/**
* Create a new knowledge filter.
*
* @param options - The filter options including name and queryData.
* @returns The created filter response with ID.
*/
async create(
options: CreateKnowledgeFilterOptions
): Promise<CreateKnowledgeFilterResponse> {
const body = {
name: options.name,
description: options.description ?? "",
queryData: JSON.stringify(options.queryData),
};
const response = await this.client._request("POST", "/knowledge-filter", {
body: JSON.stringify(body),
});
const data = await response.json();
return {
success: data.success ?? false,
id: data.id,
error: data.error,
};
}
/**
* Search for knowledge filters by name, description, or query content.
*
* @param query - Optional search query text.
* @param limit - Maximum number of results (default 20).
* @returns List of matching knowledge filters.
*/
async search(query?: string, limit?: number): Promise<KnowledgeFilter[]> {
const body = {
query: query ?? "",
limit: limit ?? 20,
};
const response = await this.client._request(
"POST",
"/knowledge-filter/search",
{
body: JSON.stringify(body),
}
);
const data = (await response.json()) as KnowledgeFilterSearchResponse;
if (!data.success || !data.filters) {
return [];
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return data.filters.map((f: any) => this._parseFilter(f));
}
/**
* Get a specific knowledge filter by ID.
*
* @param filterId - The ID of the filter to retrieve.
* @returns The knowledge filter or null if not found.
*/
async get(filterId: string): Promise<KnowledgeFilter | null> {
try {
const response = await this.client._request(
"GET",
`/knowledge-filter/${filterId}`
);
const data = (await response.json()) as GetKnowledgeFilterResponse;
if (!data.success || !data.filter) {
return null;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return this._parseFilter(data.filter as any);
} catch {
// Filter not found or other error
return null;
}
}
/**
* Update an existing knowledge filter.
*
* @param filterId - The ID of the filter to update.
* @param options - The fields to update.
* @returns Success status.
*/
async update(
filterId: string,
options: UpdateKnowledgeFilterOptions
): Promise<boolean> {
const body: Record<string, unknown> = {};
if (options.name !== undefined) {
body["name"] = options.name;
}
if (options.description !== undefined) {
body["description"] = options.description;
}
if (options.queryData !== undefined) {
body["queryData"] = JSON.stringify(options.queryData);
}
const response = await this.client._request(
"PUT",
`/knowledge-filter/${filterId}`,
{
body: JSON.stringify(body),
}
);
const data = await response.json();
return data.success ?? false;
}
/**
* Delete a knowledge filter.
*
* @param filterId - The ID of the filter to delete.
* @returns Success status.
*/
async delete(filterId: string): Promise<boolean> {
const response = await this.client._request(
"DELETE",
`/knowledge-filter/${filterId}`
);
const data = (await response.json()) as DeleteKnowledgeFilterResponse;
return data.success ?? false;
}
/**
* Parse a filter from API response, handling JSON-stringified queryData.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private _parseFilter(filter: any): KnowledgeFilter {
let queryData = filter["query_data"] ?? filter["queryData"];
if (typeof queryData === "string") {
try {
queryData = JSON.parse(queryData);
} catch {
queryData = {};
}
}
return {
id: filter["id"] as string,
name: filter["name"] as string,
description: filter["description"],
queryData: queryData as KnowledgeFilter["queryData"],
owner: filter["owner"],
createdAt: filter["created_at"] ?? filter["createdAt"],
updatedAt: filter["updated_at"] ?? filter["updatedAt"],
};
}
}

View file

@ -29,6 +29,10 @@ export class SearchClient {
body["filters"] = options.filters;
}
if (options?.filterId) {
body["filter_id"] = options.filterId;
}
const response = await this.client._request("POST", "/api/v1/search", {
body: JSON.stringify(body),
});

View file

@ -117,6 +117,114 @@ export interface SettingsResponse {
knowledge: KnowledgeSettings;
}
/** Options for updating settings. */
export interface SettingsUpdateOptions {
/** LLM model name. */
llm_model?: string;
/** LLM provider (openai, anthropic, watsonx, ollama). */
llm_provider?: string;
/** System prompt for the agent. */
system_prompt?: string;
/** Embedding model name. */
embedding_model?: string;
/** Embedding provider (openai, watsonx, ollama). */
embedding_provider?: string;
/** Chunk size for document splitting. */
chunk_size?: number;
/** Chunk overlap for document splitting. */
chunk_overlap?: number;
/** Enable table structure parsing. */
table_structure?: boolean;
/** Enable OCR for text extraction. */
ocr?: boolean;
/** Enable picture descriptions. */
picture_descriptions?: boolean;
}
/** Response from settings update. */
export interface SettingsUpdateResponse {
message: string;
}
// Knowledge filter types
/** Query configuration stored in a knowledge filter. */
export interface KnowledgeFilterQueryData {
/** Semantic search query text. */
query?: string;
/** Filter criteria for documents. */
filters?: {
data_sources?: string[];
document_types?: string[];
owners?: string[];
connector_types?: string[];
};
/** Maximum number of results. */
limit?: number;
/** Minimum relevance score threshold. */
scoreThreshold?: number;
/** UI color for the filter. */
color?: string;
/** UI icon for the filter. */
icon?: string;
}
/** A knowledge filter definition. */
export interface KnowledgeFilter {
id: string;
name: string;
description?: string;
queryData: KnowledgeFilterQueryData;
owner?: string;
createdAt?: string;
updatedAt?: string;
}
/** Options for creating a knowledge filter. */
export interface CreateKnowledgeFilterOptions {
/** Filter name (required). */
name: string;
/** Filter description. */
description?: string;
/** Query configuration for the filter. */
queryData: KnowledgeFilterQueryData;
}
/** Options for updating a knowledge filter. */
export interface UpdateKnowledgeFilterOptions {
/** New filter name. */
name?: string;
/** New filter description. */
description?: string;
/** New query configuration. */
queryData?: KnowledgeFilterQueryData;
}
/** Response from creating a knowledge filter. */
export interface CreateKnowledgeFilterResponse {
success: boolean;
id?: string;
error?: string;
}
/** Response from searching knowledge filters. */
export interface KnowledgeFilterSearchResponse {
success: boolean;
filters: KnowledgeFilter[];
}
/** Response from getting a knowledge filter. */
export interface GetKnowledgeFilterResponse {
success: boolean;
filter?: KnowledgeFilter;
error?: string;
}
/** Response from deleting a knowledge filter. */
export interface DeleteKnowledgeFilterResponse {
success: boolean;
error?: string;
}
// Client options
export interface OpenRAGClientOptions {
/** API key for authentication. Falls back to OPENRAG_API_KEY env var. */
@ -135,6 +243,8 @@ export interface ChatCreateOptions {
filters?: SearchFilters;
limit?: number;
scoreThreshold?: number;
/** Knowledge filter ID to apply to the chat. */
filterId?: string;
}
export interface SearchQueryOptions {
@ -142,6 +252,8 @@ export interface SearchQueryOptions {
filters?: SearchFilters;
limit?: number;
scoreThreshold?: number;
/** Knowledge filter ID to apply to the search. */
filterId?: string;
}
// Error types

View file

@ -75,6 +75,139 @@ describe.skipIf(SKIP_TESTS)("OpenRAG TypeScript SDK Integration", () => {
expect(settings.agent).toBeDefined();
expect(settings.knowledge).toBeDefined();
});
it("should update settings", async () => {
// Get current settings first
const currentSettings = await client.settings.get();
const currentChunkSize = currentSettings.knowledge.chunk_size || 1000;
// Update with a new value
const result = await client.settings.update({
chunk_size: currentChunkSize,
});
expect(result.message).toBeDefined();
// Verify the setting persisted
const updatedSettings = await client.settings.get();
expect(updatedSettings.knowledge.chunk_size).toBe(currentChunkSize);
});
});
describe("Knowledge Filters", () => {
let createdFilterId: string;
it("should create a knowledge filter", async () => {
const result = await client.knowledgeFilters.create({
name: "SDK Test Filter",
description: "Filter created by TypeScript SDK integration tests",
queryData: {
query: "test documents",
limit: 10,
scoreThreshold: 0.5,
},
});
expect(result.success).toBe(true);
expect(result.id).toBeDefined();
createdFilterId = result.id!;
});
it("should search knowledge filters", async () => {
const filters = await client.knowledgeFilters.search("SDK Test");
expect(Array.isArray(filters)).toBe(true);
// Should find the filter we created
const found = filters.some((f) => f.name === "SDK Test Filter");
expect(found).toBe(true);
});
it("should get a knowledge filter by ID", async () => {
expect(createdFilterId).toBeDefined();
const filter = await client.knowledgeFilters.get(createdFilterId);
expect(filter).not.toBeNull();
expect(filter!.id).toBe(createdFilterId);
expect(filter!.name).toBe("SDK Test Filter");
});
it("should update a knowledge filter", async () => {
expect(createdFilterId).toBeDefined();
const success = await client.knowledgeFilters.update(createdFilterId, {
description: "Updated description from SDK test",
});
expect(success).toBe(true);
// Verify the update
const filter = await client.knowledgeFilters.get(createdFilterId);
expect(filter!.description).toBe("Updated description from SDK test");
});
it("should delete a knowledge filter", async () => {
expect(createdFilterId).toBeDefined();
const success = await client.knowledgeFilters.delete(createdFilterId);
expect(success).toBe(true);
// Verify deletion
const filter = await client.knowledgeFilters.get(createdFilterId);
expect(filter).toBeNull();
});
it("should use filterId in chat", async () => {
// Create a filter first
const createResult = await client.knowledgeFilters.create({
name: "Chat Test Filter",
description: "Filter for testing chat with filterId",
queryData: {
query: "test",
limit: 5,
},
});
expect(createResult.success).toBe(true);
const filterId = createResult.id!;
try {
// Use filter in chat
const response = await client.chat.create({
message: "Hello with filter",
filterId,
});
expect(response.response).toBeDefined();
} finally {
// Cleanup
await client.knowledgeFilters.delete(filterId);
}
});
it("should use filterId in search", async () => {
// Create a filter first
const createResult = await client.knowledgeFilters.create({
name: "Search Test Filter",
description: "Filter for testing search with filterId",
queryData: {
query: "test",
limit: 5,
},
});
expect(createResult.success).toBe(true);
const filterId = createResult.id!;
try {
// Use filter in search
const results = await client.search.query("test query", { filterId });
expect(results.results).toBeDefined();
} finally {
// Cleanup
await client.knowledgeFilters.delete(filterId);
}
});
});
describe("Documents", () => {
@ -221,5 +354,34 @@ describe.skipIf(SKIP_TESTS)("OpenRAG TypeScript SDK Integration", () => {
expect(result.conversations).toBeDefined();
expect(Array.isArray(result.conversations)).toBe(true);
});
it("should get a specific conversation", async () => {
// Create a conversation first
const response = await client.chat.create({
message: "Test message for get.",
});
expect(response.chatId).toBeDefined();
// Get the conversation
const conversation = await client.chat.get(response.chatId!);
expect(conversation.chatId).toBe(response.chatId);
expect(conversation.messages).toBeDefined();
expect(Array.isArray(conversation.messages)).toBe(true);
expect(conversation.messages.length).toBeGreaterThanOrEqual(1);
});
it("should delete a conversation", async () => {
// Create a conversation first
const response = await client.chat.create({
message: "Test message for delete.",
});
expect(response.chatId).toBeDefined();
// Delete the conversation
const result = await client.chat.delete(response.chatId!);
expect(result).toBe(true);
});
});
});