diff --git a/.github/workflows/test-integration.yml b/.github/workflows/test-integration.yml index 544b846e..653cd499 100644 --- a/.github/workflows/test-integration.yml +++ b/.github/workflows/test-integration.yml @@ -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 diff --git a/sdks/python/openrag_sdk/__init__.py b/sdks/python/openrag_sdk/__init__.py index 2160b2e0..3d19d3d1 100644 --- a/sdks/python/openrag_sdk/__init__.py +++ b/sdks/python/openrag_sdk/__init__.py @@ -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", ] diff --git a/sdks/python/openrag_sdk/chat.py b/sdks/python/openrag_sdk/chat.py index 4e1e7fe4..ef33bc24 100644 --- a/sdks/python/openrag_sdk/chat.py +++ b/sdks/python/openrag_sdk/chat.py @@ -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: diff --git a/sdks/python/openrag_sdk/client.py b/sdks/python/openrag_sdk/client.py index e75c913d..a38f11c1 100644 --- a/sdks/python/openrag_sdk/client.py +++ b/sdks/python/openrag_sdk/client.py @@ -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]: diff --git a/sdks/python/openrag_sdk/knowledge_filters.py b/sdks/python/openrag_sdk/knowledge_filters.py new file mode 100644 index 00000000..557bae5a --- /dev/null +++ b/sdks/python/openrag_sdk/knowledge_filters.py @@ -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"), + ) diff --git a/sdks/python/openrag_sdk/models.py b/sdks/python/openrag_sdk/models.py index 70e69abc..d65c98f1 100644 --- a/sdks/python/openrag_sdk/models.py +++ b/sdks/python/openrag_sdk/models.py @@ -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 diff --git a/sdks/python/openrag_sdk/search.py b/sdks/python/openrag_sdk/search.py index 86d6a8e8..d084840a 100644 --- a/sdks/python/openrag_sdk/search.py +++ b/sdks/python/openrag_sdk/search.py @@ -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", diff --git a/sdks/python/tests/test_integration.py b/sdks/python/tests/test_integration.py index 25a79f77..0280f33d 100644 --- a/sdks/python/tests/test_integration.py +++ b/sdks/python/tests/test_integration.py @@ -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 diff --git a/sdks/typescript/src/chat.ts b/sdks/typescript/src/chat.ts index 540722b5..edf5c5ac 100644 --- a/sdks/typescript/src/chat.ts +++ b/sdks/typescript/src/chat.ts @@ -75,6 +75,10 @@ export class ChatStream implements AsyncIterable, 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), }); diff --git a/sdks/typescript/src/client.ts b/sdks/typescript/src/client.ts index 2ea99b43..35cb32c0 100644 --- a/sdks/typescript/src/client.ts +++ b/sdks/typescript/src/client.ts @@ -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 { 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 { + 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. */ diff --git a/sdks/typescript/src/index.ts b/sdks/typescript/src/index.ts index 81f4f377..3283f118 100644 --- a/sdks/typescript/src/index.ts +++ b/sdks/typescript/src/index.ts @@ -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"; diff --git a/sdks/typescript/src/knowledge-filters.ts b/sdks/typescript/src/knowledge-filters.ts new file mode 100644 index 00000000..ac2e360d --- /dev/null +++ b/sdks/typescript/src/knowledge-filters.ts @@ -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 { + 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 { + 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 { + 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 { + const body: Record = {}; + + 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 { + 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"], + }; + } +} diff --git a/sdks/typescript/src/search.ts b/sdks/typescript/src/search.ts index 09d2d57e..c8488e8b 100644 --- a/sdks/typescript/src/search.ts +++ b/sdks/typescript/src/search.ts @@ -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), }); diff --git a/sdks/typescript/src/types.ts b/sdks/typescript/src/types.ts index 9ee40a72..7a4b73b8 100644 --- a/sdks/typescript/src/types.ts +++ b/sdks/typescript/src/types.ts @@ -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 diff --git a/sdks/typescript/tests/integration.test.ts b/sdks/typescript/tests/integration.test.ts index 23eb3755..feececa0 100644 --- a/sdks/typescript/tests/integration.test.ts +++ b/sdks/typescript/tests/integration.test.ts @@ -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); + }); }); });