From a9fdc576c8e0bd2635f1e047e5a0dddbe9622748 Mon Sep 17 00:00:00 2001 From: Hayden Date: Fri, 30 Aug 2024 21:39:44 +0700 Subject: [PATCH 1/2] feat: conversation API - bump Typesense to version 27.0 in github workflow file --- .github/workflows/tests.yml | 2 +- Sources/Typesense/Client.swift | 4 + Sources/Typesense/ConversationModel.swift | 48 +++++++++++ Sources/Typesense/ConversationModels.swift | 39 +++++++++ Sources/Typesense/Conversations.swift | 20 +++++ .../ConversationModelCreateSchema.swift | 57 +++++++++++++ .../Models/ConversationModelSchema.swift | 57 +++++++++++++ .../ConversationModelUpdateSchema.swift | 57 +++++++++++++ .../Models/MultiSearchParameters.swift | 14 +++- .../Typesense/Models/MultiSearchResult.swift | 4 +- .../Typesense/Models/SearchParameters.swift | 14 +++- Sources/Typesense/Models/SearchResult.swift | 7 +- .../Models/SearchResultConversation.swift | 33 ++++++++ .../ConversationModelTests.swift | 81 +++++++++++++++++++ Tests/TypesenseTests/TestUtils.swift | 58 +++++++------ 15 files changed, 466 insertions(+), 29 deletions(-) create mode 100644 Sources/Typesense/ConversationModel.swift create mode 100644 Sources/Typesense/ConversationModels.swift create mode 100644 Sources/Typesense/Conversations.swift create mode 100644 Sources/Typesense/Models/ConversationModelCreateSchema.swift create mode 100644 Sources/Typesense/Models/ConversationModelSchema.swift create mode 100644 Sources/Typesense/Models/ConversationModelUpdateSchema.swift create mode 100644 Sources/Typesense/Models/SearchResultConversation.swift create mode 100644 Tests/TypesenseTests/ConversationModelTests.swift diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index f397778..d9e4215 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -32,7 +32,7 @@ jobs: mkdir $(pwd)/typesense-data docker run -p 8108:8108 \ -d \ - -v$(pwd)/typesense-data:/data typesense/typesense:27.0.rc35 \ + -v$(pwd)/typesense-data:/data typesense/typesense:27.0 \ --data-dir /data \ --api-key=xyz \ --enable-cors diff --git a/Sources/Typesense/Client.swift b/Sources/Typesense/Client.swift index d735f36..93191e7 100644 --- a/Sources/Typesense/Client.swift +++ b/Sources/Typesense/Client.swift @@ -16,6 +16,10 @@ public struct Client { return Collection(apiCall: apiCall, collectionName: name) } + public func conversations() -> Conversations { + return Conversations(apiCall: apiCall) + } + public func keys() -> ApiKeys { return ApiKeys(apiCall: apiCall) } diff --git a/Sources/Typesense/ConversationModel.swift b/Sources/Typesense/ConversationModel.swift new file mode 100644 index 0000000..1d6ab09 --- /dev/null +++ b/Sources/Typesense/ConversationModel.swift @@ -0,0 +1,48 @@ +import Foundation +#if canImport(FoundationNetworking) + import FoundationNetworking +#endif + +public struct ConversationModel { + private var apiCall: ApiCall + var modelId: String + + init(apiCall: ApiCall, modelId: String) { + self.apiCall = apiCall + self.modelId = modelId + } + + public func update(params: ConversationModelUpdateSchema) async throws -> (ConversationModelSchema?, URLResponse?) { + let schemaData = try encoder.encode(params) + let (data, response) = try await self.apiCall.put(endPoint: endpointPath(), body: schemaData) + if let result = data { + let decodedData = try decoder.decode(ConversationModelSchema.self, from: result) + return (decodedData, response) + } + return (nil, response) + } + + public func retrieve() async throws -> (ConversationModelSchema?, URLResponse?) { + let (data, response) = try await self.apiCall.get(endPoint: endpointPath()) + if let result = data { + let decodedData = try decoder.decode(ConversationModelSchema.self, from: result) + return (decodedData, response) + } + return (nil, response) + } + + public func delete() async throws -> (ConversationModelSchema?, URLResponse?) { + let (data, response) = try await self.apiCall.delete(endPoint: endpointPath()) + if let result = data { + let decodedData = try decoder.decode(ConversationModelSchema.self, from: result) + return (decodedData, response) + } + return (nil, response) + } + + private func endpointPath() throws -> String { + return try "\(Conversations.RESOURCE_PATH)/\(ConversationModels.RESOURCE_PATH)/\(modelId.encodeURL())" + } + + +} diff --git a/Sources/Typesense/ConversationModels.swift b/Sources/Typesense/ConversationModels.swift new file mode 100644 index 0000000..8fd0aec --- /dev/null +++ b/Sources/Typesense/ConversationModels.swift @@ -0,0 +1,39 @@ +import Foundation +#if canImport(FoundationNetworking) + import FoundationNetworking +#endif + +public struct ConversationModels { + static let RESOURCE_PATH = "models" + private var apiCall: ApiCall + + + init(apiCall: ApiCall) { + self.apiCall = apiCall + } + + public func create(params: ConversationModelCreateSchema) async throws -> (ConversationModelSchema?, URLResponse?) { + let schemaData = try encoder.encode(params) + let (data, response) = try await self.apiCall.post(endPoint: endpointPath(), body: schemaData) + if let result = data { + let decodedData = try decoder.decode(ConversationModelSchema.self, from: result) + return (decodedData, response) + } + return (nil, response) + } + + public func retrieve() async throws -> ([ConversationModelSchema]?, URLResponse?) { + let (data, response) = try await self.apiCall.get(endPoint: endpointPath()) + if let result = data { + let decodedData = try decoder.decode([ConversationModelSchema].self, from: result) + return (decodedData, response) + } + return (nil, response) + } + + private func endpointPath() throws -> String { + return "\(Conversations.RESOURCE_PATH)/\(ConversationModels.RESOURCE_PATH)" + } + + +} diff --git a/Sources/Typesense/Conversations.swift b/Sources/Typesense/Conversations.swift new file mode 100644 index 0000000..6738b38 --- /dev/null +++ b/Sources/Typesense/Conversations.swift @@ -0,0 +1,20 @@ +import Foundation + +public struct Conversations { + static let RESOURCE_PATH = "conversations" + private var apiCall: ApiCall + + + init(apiCall: ApiCall) { + self.apiCall = apiCall + } + + public func models() -> ConversationModels { + return ConversationModels(apiCall: apiCall) + } + + public func model(modelId: String) -> ConversationModel { + return ConversationModel(apiCall: apiCall, modelId: modelId) + } + +} diff --git a/Sources/Typesense/Models/ConversationModelCreateSchema.swift b/Sources/Typesense/Models/ConversationModelCreateSchema.swift new file mode 100644 index 0000000..b9f98d2 --- /dev/null +++ b/Sources/Typesense/Models/ConversationModelCreateSchema.swift @@ -0,0 +1,57 @@ +// +// ConversationModelCreateSchema.swift +// +// Generated by swagger-codegen +// https://github.com/swagger-api/swagger-codegen +// + +import Foundation + + + +public struct ConversationModelCreateSchema: Codable { + + /** An explicit id for the model, otherwise the API will return a response with an auto-generated conversation model id. */ + public var _id: String? + /** Name of the LLM model offered by OpenAI, Cloudflare or vLLM */ + public var modelName: String + /** The LLM service's API Key */ + public var apiKey: String? + /** Typesense collection that stores the historical conversations */ + public var historyCollection: String? + /** LLM service's account ID (only applicable for Cloudflare) */ + public var accountId: String? + /** The system prompt that contains special instructions to the LLM */ + public var systemPrompt: String? + /** Time interval in seconds after which the messages would be deleted. Default: 86400 (24 hours) */ + public var ttl: Int? + /** The maximum number of bytes to send to the LLM in every API call. Consult the LLM's documentation on the number of bytes supported in the context window. */ + public var maxBytes: Int + /** URL of vLLM service */ + public var vllmUrl: String? + + public init(_id: String? = nil, modelName: String, apiKey: String? = nil, historyCollection: String? = nil, accountId: String? = nil, systemPrompt: String? = nil, ttl: Int? = nil, maxBytes: Int, vllmUrl: String? = nil) { + self._id = _id + self.modelName = modelName + self.apiKey = apiKey + self.historyCollection = historyCollection + self.accountId = accountId + self.systemPrompt = systemPrompt + self.ttl = ttl + self.maxBytes = maxBytes + self.vllmUrl = vllmUrl + } + + public enum CodingKeys: String, CodingKey { + case _id = "id" + case modelName = "model_name" + case apiKey = "api_key" + case historyCollection = "history_collection" + case accountId = "account_id" + case systemPrompt = "system_prompt" + case ttl + case maxBytes = "max_bytes" + case vllmUrl = "vllm_url" + } + +} diff --git a/Sources/Typesense/Models/ConversationModelSchema.swift b/Sources/Typesense/Models/ConversationModelSchema.swift new file mode 100644 index 0000000..6a129af --- /dev/null +++ b/Sources/Typesense/Models/ConversationModelSchema.swift @@ -0,0 +1,57 @@ +// +// ConversationModelSchema.swift +// +// Generated by swagger-codegen +// https://github.com/swagger-api/swagger-codegen +// + +import Foundation + + + +public struct ConversationModelSchema: Codable { + + /** An explicit id for the model, otherwise the API will return a response with an auto-generated conversation model id. */ + public var _id: String + /** Name of the LLM model offered by OpenAI, Cloudflare or vLLM */ + public var modelName: String? + /** The LLM service's API Key */ + public var apiKey: String? + /** Typesense collection that stores the historical conversations */ + public var historyCollection: String? + /** LLM service's account ID (only applicable for Cloudflare) */ + public var accountId: String? + /** The system prompt that contains special instructions to the LLM */ + public var systemPrompt: String? + /** Time interval in seconds after which the messages would be deleted. Default: 86400 (24 hours) */ + public var ttl: Int? + /** The maximum number of bytes to send to the LLM in every API call. Consult the LLM's documentation on the number of bytes supported in the context window. */ + public var maxBytes: Int? + /** URL of vLLM service */ + public var vllmUrl: String? + + public init(_id: String, modelName: String? = nil, apiKey: String? = nil, historyCollection: String? = nil, accountId: String? = nil, systemPrompt: String? = nil, ttl: Int? = nil, maxBytes: Int? = nil, vllmUrl: String? = nil) { + self._id = _id + self.modelName = modelName + self.apiKey = apiKey + self.historyCollection = historyCollection + self.accountId = accountId + self.systemPrompt = systemPrompt + self.ttl = ttl + self.maxBytes = maxBytes + self.vllmUrl = vllmUrl + } + + public enum CodingKeys: String, CodingKey { + case _id = "id" + case modelName = "model_name" + case apiKey = "api_key" + case historyCollection = "history_collection" + case accountId = "account_id" + case systemPrompt = "system_prompt" + case ttl + case maxBytes = "max_bytes" + case vllmUrl = "vllm_url" + } + +} diff --git a/Sources/Typesense/Models/ConversationModelUpdateSchema.swift b/Sources/Typesense/Models/ConversationModelUpdateSchema.swift new file mode 100644 index 0000000..214dcb3 --- /dev/null +++ b/Sources/Typesense/Models/ConversationModelUpdateSchema.swift @@ -0,0 +1,57 @@ +// +// ConversationModelUpdateSchema.swift +// +// Generated by swagger-codegen +// https://github.com/swagger-api/swagger-codegen +// + +import Foundation + + + +public struct ConversationModelUpdateSchema: Codable { + + /** An explicit id for the model, otherwise the API will return a response with an auto-generated conversation model id. */ + public var _id: String? + /** Name of the LLM model offered by OpenAI, Cloudflare or vLLM */ + public var modelName: String? + /** The LLM service's API Key */ + public var apiKey: String? + /** Typesense collection that stores the historical conversations */ + public var historyCollection: String? + /** LLM service's account ID (only applicable for Cloudflare) */ + public var accountId: String? + /** The system prompt that contains special instructions to the LLM */ + public var systemPrompt: String? + /** Time interval in seconds after which the messages would be deleted. Default: 86400 (24 hours) */ + public var ttl: Int? + /** The maximum number of bytes to send to the LLM in every API call. Consult the LLM's documentation on the number of bytes supported in the context window. */ + public var maxBytes: Int? + /** URL of vLLM service */ + public var vllmUrl: String? + + public init(_id: String? = nil, modelName: String? = nil, apiKey: String? = nil, historyCollection: String? = nil, accountId: String? = nil, systemPrompt: String? = nil, ttl: Int? = nil, maxBytes: Int? = nil, vllmUrl: String? = nil) { + self._id = _id + self.modelName = modelName + self.apiKey = apiKey + self.historyCollection = historyCollection + self.accountId = accountId + self.systemPrompt = systemPrompt + self.ttl = ttl + self.maxBytes = maxBytes + self.vllmUrl = vllmUrl + } + + public enum CodingKeys: String, CodingKey { + case _id = "id" + case modelName = "model_name" + case apiKey = "api_key" + case historyCollection = "history_collection" + case accountId = "account_id" + case systemPrompt = "system_prompt" + case ttl + case maxBytes = "max_bytes" + case vllmUrl = "vllm_url" + } + +} diff --git a/Sources/Typesense/Models/MultiSearchParameters.swift b/Sources/Typesense/Models/MultiSearchParameters.swift index 7578045..183e9c7 100644 --- a/Sources/Typesense/Models/MultiSearchParameters.swift +++ b/Sources/Typesense/Models/MultiSearchParameters.swift @@ -120,8 +120,14 @@ public struct MultiSearchParameters: Codable { public var facetReturnParent: String? /** The base64 encoded audio file in 16 khz 16-bit WAV format. */ public var voiceQuery: String? + /** Enable conversational search. */ + public var conversation: Bool? + /** The Id of Conversation Model to be used. */ + public var conversationModelId: String? + /** The Id of a previous conversation to continue, this tells Typesense to include prior context when communicating with the LLM. */ + public var conversationId: String? - public init(q: String? = nil, queryBy: String? = nil, queryByWeights: String? = nil, textMatchType: String? = nil, _prefix: String? = nil, _infix: String? = nil, maxExtraPrefix: Int? = nil, maxExtraSuffix: Int? = nil, filterBy: String? = nil, sortBy: String? = nil, facetBy: String? = nil, maxFacetValues: Int? = nil, facetQuery: String? = nil, numTypos: String? = nil, page: Int? = nil, perPage: Int? = nil, limit: Int? = nil, offset: Int? = nil, groupBy: String? = nil, groupLimit: Int? = nil, groupMissingValues: Bool? = nil, includeFields: String? = nil, excludeFields: String? = nil, highlightFullFields: String? = nil, highlightAffixNumTokens: Int? = nil, highlightStartTag: String? = nil, highlightEndTag: String? = nil, snippetThreshold: Int? = nil, dropTokensThreshold: Int? = nil, typoTokensThreshold: Int? = nil, pinnedHits: String? = nil, hiddenHits: String? = nil, overrideTags: String? = nil, highlightFields: String? = nil, preSegmentedQuery: Bool? = nil, preset: String? = nil, enableOverrides: Bool? = nil, prioritizeExactMatch: Bool? = nil, prioritizeTokenPosition: Bool? = nil, prioritizeNumMatchingFields: Bool? = nil, enableTyposForNumericalTokens: Bool? = nil, exhaustiveSearch: Bool? = nil, searchCutoffMs: Int? = nil, useCache: Bool? = nil, cacheTtl: Int? = nil, minLen1typo: Int? = nil, minLen2typo: Int? = nil, vectorQuery: String? = nil, remoteEmbeddingTimeoutMs: Int? = nil, remoteEmbeddingNumTries: Int? = nil, facetStrategy: String? = nil, stopwords: String? = nil, facetReturnParent: String? = nil, voiceQuery: String? = nil) { + public init(q: String? = nil, queryBy: String? = nil, queryByWeights: String? = nil, textMatchType: String? = nil, _prefix: String? = nil, _infix: String? = nil, maxExtraPrefix: Int? = nil, maxExtraSuffix: Int? = nil, filterBy: String? = nil, sortBy: String? = nil, facetBy: String? = nil, maxFacetValues: Int? = nil, facetQuery: String? = nil, numTypos: String? = nil, page: Int? = nil, perPage: Int? = nil, limit: Int? = nil, offset: Int? = nil, groupBy: String? = nil, groupLimit: Int? = nil, groupMissingValues: Bool? = nil, includeFields: String? = nil, excludeFields: String? = nil, highlightFullFields: String? = nil, highlightAffixNumTokens: Int? = nil, highlightStartTag: String? = nil, highlightEndTag: String? = nil, snippetThreshold: Int? = nil, dropTokensThreshold: Int? = nil, typoTokensThreshold: Int? = nil, pinnedHits: String? = nil, hiddenHits: String? = nil, overrideTags: String? = nil, highlightFields: String? = nil, preSegmentedQuery: Bool? = nil, preset: String? = nil, enableOverrides: Bool? = nil, prioritizeExactMatch: Bool? = nil, prioritizeTokenPosition: Bool? = nil, prioritizeNumMatchingFields: Bool? = nil, enableTyposForNumericalTokens: Bool? = nil, exhaustiveSearch: Bool? = nil, searchCutoffMs: Int? = nil, useCache: Bool? = nil, cacheTtl: Int? = nil, minLen1typo: Int? = nil, minLen2typo: Int? = nil, vectorQuery: String? = nil, remoteEmbeddingTimeoutMs: Int? = nil, remoteEmbeddingNumTries: Int? = nil, facetStrategy: String? = nil, stopwords: String? = nil, facetReturnParent: String? = nil, voiceQuery: String? = nil, conversation: Bool? = nil, conversationModelId: String? = nil, conversationId: String? = nil) { self.q = q self.queryBy = queryBy self.queryByWeights = queryByWeights @@ -176,6 +182,9 @@ public struct MultiSearchParameters: Codable { self.stopwords = stopwords self.facetReturnParent = facetReturnParent self.voiceQuery = voiceQuery + self.conversation = conversation + self.conversationModelId = conversationModelId + self.conversationId = conversationId } public enum CodingKeys: String, CodingKey { @@ -233,6 +242,9 @@ public struct MultiSearchParameters: Codable { case stopwords case facetReturnParent = "facet_return_parent" case voiceQuery = "voice_query" + case conversation + case conversationModelId = "conversation_model_id" + case conversationId = "conversation_id" } } diff --git a/Sources/Typesense/Models/MultiSearchResult.swift b/Sources/Typesense/Models/MultiSearchResult.swift index a3b85e8..d9b3b70 100644 --- a/Sources/Typesense/Models/MultiSearchResult.swift +++ b/Sources/Typesense/Models/MultiSearchResult.swift @@ -12,9 +12,11 @@ import Foundation public struct MultiSearchResult: Codable { public var results: [SearchResult] + public var conversation: SearchResultConversation? - public init(results: [SearchResult]) { + public init(results: [SearchResult], conversation: SearchResultConversation? = nil) { self.results = results + self.conversation = conversation } diff --git a/Sources/Typesense/Models/SearchParameters.swift b/Sources/Typesense/Models/SearchParameters.swift index 4efb9ed..795f28b 100644 --- a/Sources/Typesense/Models/SearchParameters.swift +++ b/Sources/Typesense/Models/SearchParameters.swift @@ -125,8 +125,14 @@ public struct SearchParameters: Codable { public var facetReturnParent: String? /** The base64 encoded audio file in 16 khz 16-bit WAV format. */ public var voiceQuery: String? + /** Enable conversational search. */ + public var conversation: Bool? + /** The Id of Conversation Model to be used. */ + public var conversationModelId: String? + /** The Id of a previous conversation to continue, this tells Typesense to include prior context when communicating with the LLM. */ + public var conversationId: String? - public init(q: String? = nil, queryBy: String? = nil, queryByWeights: String? = nil, textMatchType: String? = nil, _prefix: String? = nil, _infix: String? = nil, maxExtraPrefix: Int? = nil, maxExtraSuffix: Int? = nil, filterBy: String? = nil, sortBy: String? = nil, facetBy: String? = nil, maxFacetValues: Int? = nil, facetQuery: String? = nil, numTypos: String? = nil, page: Int? = nil, perPage: Int? = nil, limit: Int? = nil, offset: Int? = nil, groupBy: String? = nil, groupLimit: Int? = nil, groupMissingValues: Bool? = nil, includeFields: String? = nil, excludeFields: String? = nil, highlightFullFields: String? = nil, highlightAffixNumTokens: Int? = nil, highlightStartTag: String? = nil, highlightEndTag: String? = nil, enableHighlightV1: Bool? = nil, snippetThreshold: Int? = nil, dropTokensThreshold: Int? = nil, typoTokensThreshold: Int? = nil, pinnedHits: String? = nil, hiddenHits: String? = nil, overrideTags: String? = nil, highlightFields: String? = nil, splitJoinTokens: String? = nil, preSegmentedQuery: Bool? = nil, preset: String? = nil, enableOverrides: Bool? = nil, prioritizeExactMatch: Bool? = nil, maxCandidates: Int? = nil, prioritizeTokenPosition: Bool? = nil, prioritizeNumMatchingFields: Bool? = nil, enableTyposForNumericalTokens: Bool? = nil, exhaustiveSearch: Bool? = nil, searchCutoffMs: Int? = nil, useCache: Bool? = nil, cacheTtl: Int? = nil, minLen1typo: Int? = nil, minLen2typo: Int? = nil, vectorQuery: String? = nil, remoteEmbeddingTimeoutMs: Int? = nil, remoteEmbeddingNumTries: Int? = nil, facetStrategy: String? = nil, stopwords: String? = nil, facetReturnParent: String? = nil, voiceQuery: String? = nil) { + public init(q: String? = nil, queryBy: String? = nil, queryByWeights: String? = nil, textMatchType: String? = nil, _prefix: String? = nil, _infix: String? = nil, maxExtraPrefix: Int? = nil, maxExtraSuffix: Int? = nil, filterBy: String? = nil, sortBy: String? = nil, facetBy: String? = nil, maxFacetValues: Int? = nil, facetQuery: String? = nil, numTypos: String? = nil, page: Int? = nil, perPage: Int? = nil, limit: Int? = nil, offset: Int? = nil, groupBy: String? = nil, groupLimit: Int? = nil, groupMissingValues: Bool? = nil, includeFields: String? = nil, excludeFields: String? = nil, highlightFullFields: String? = nil, highlightAffixNumTokens: Int? = nil, highlightStartTag: String? = nil, highlightEndTag: String? = nil, enableHighlightV1: Bool? = nil, snippetThreshold: Int? = nil, dropTokensThreshold: Int? = nil, typoTokensThreshold: Int? = nil, pinnedHits: String? = nil, hiddenHits: String? = nil, overrideTags: String? = nil, highlightFields: String? = nil, splitJoinTokens: String? = nil, preSegmentedQuery: Bool? = nil, preset: String? = nil, enableOverrides: Bool? = nil, prioritizeExactMatch: Bool? = nil, maxCandidates: Int? = nil, prioritizeTokenPosition: Bool? = nil, prioritizeNumMatchingFields: Bool? = nil, enableTyposForNumericalTokens: Bool? = nil, exhaustiveSearch: Bool? = nil, searchCutoffMs: Int? = nil, useCache: Bool? = nil, cacheTtl: Int? = nil, minLen1typo: Int? = nil, minLen2typo: Int? = nil, vectorQuery: String? = nil, remoteEmbeddingTimeoutMs: Int? = nil, remoteEmbeddingNumTries: Int? = nil, facetStrategy: String? = nil, stopwords: String? = nil, facetReturnParent: String? = nil, voiceQuery: String? = nil, conversation: Bool? = nil, conversationModelId: String? = nil, conversationId: String? = nil) { self.q = q self.queryBy = queryBy self.queryByWeights = queryByWeights @@ -184,6 +190,9 @@ public struct SearchParameters: Codable { self.stopwords = stopwords self.facetReturnParent = facetReturnParent self.voiceQuery = voiceQuery + self.conversation = conversation + self.conversationModelId = conversationModelId + self.conversationId = conversationId } public enum CodingKeys: String, CodingKey { @@ -244,6 +253,9 @@ public struct SearchParameters: Codable { case stopwords case facetReturnParent = "facet_return_parent" case voiceQuery = "voice_query" + case conversation + case conversationModelId = "conversation_model_id" + case conversationId = "conversation_id" } } diff --git a/Sources/Typesense/Models/SearchResult.swift b/Sources/Typesense/Models/SearchResult.swift index d33218d..3267c29 100644 --- a/Sources/Typesense/Models/SearchResult.swift +++ b/Sources/Typesense/Models/SearchResult.swift @@ -26,8 +26,9 @@ public struct SearchResult: Codable { /** The documents that matched the search query */ public var hits: [SearchResultHit]? public var requestParams: SearchResultRequestParams? + public var conversation: SearchResultConversation? - public init(facetCounts: [FacetCounts]? = nil, found: Int? = nil, searchTimeMs: Int? = nil, outOf: Int? = nil, searchCutoff: Bool? = nil, page: Int? = nil, groupedHits: [SearchGroupedHit]? = nil, hits: [SearchResultHit]? = nil, requestParams: SearchResultRequestParams? = nil) { + public init(facetCounts: [FacetCounts]? = nil, found: Int? = nil, searchTimeMs: Int? = nil, outOf: Int? = nil, searchCutoff: Bool? = nil, page: Int? = nil, groupedHits: [SearchGroupedHit]? = nil, hits: [SearchResultHit]? = nil, requestParams: SearchResultRequestParams? = nil, conversation: SearchResultConversation? = nil) { self.facetCounts = facetCounts self.found = found self.searchTimeMs = searchTimeMs @@ -37,9 +38,10 @@ public struct SearchResult: Codable { self.groupedHits = groupedHits self.hits = hits self.requestParams = requestParams + self.conversation = conversation } - public enum CodingKeys: String, CodingKey { + public enum CodingKeys: String, CodingKey { case facetCounts = "facet_counts" case found case searchTimeMs = "search_time_ms" @@ -49,6 +51,7 @@ public struct SearchResult: Codable { case groupedHits = "grouped_hits" case hits case requestParams = "request_params" + case conversation } } diff --git a/Sources/Typesense/Models/SearchResultConversation.swift b/Sources/Typesense/Models/SearchResultConversation.swift new file mode 100644 index 0000000..fa8e2f8 --- /dev/null +++ b/Sources/Typesense/Models/SearchResultConversation.swift @@ -0,0 +1,33 @@ +// +// SearchResultConversation.swift +// +// Generated by swagger-codegen +// https://github.com/swagger-api/swagger-codegen +// + +import Foundation + + + +public struct SearchResultConversation: Codable { + + public var answer: String + public var conversationHistory: [String: String] + public var conversationId: String + public var query: String + + public init(answer: String, conversationHistory: [String: String], conversationId: String, query: String) { + self.answer = answer + self.conversationHistory = conversationHistory + self.conversationId = conversationId + self.query = query + } + + public enum CodingKeys: String, CodingKey { + case answer + case conversationHistory = "conversation_history" + case conversationId = "conversation_id" + case query + } + +} diff --git a/Tests/TypesenseTests/ConversationModelTests.swift b/Tests/TypesenseTests/ConversationModelTests.swift new file mode 100644 index 0000000..bfdf7ed --- /dev/null +++ b/Tests/TypesenseTests/ConversationModelTests.swift @@ -0,0 +1,81 @@ +import XCTest +@testable import Typesense + +final class ConversationModelTests: XCTestCase { + func testConversationModelsCreate() async { + let schema = ConversationModelCreateSchema( + _id: "conv-model-1", + modelName: "test/gpt-3.5-turbo", + apiKey: "sk", + historyCollection: "conversation_store", + systemPrompt: "You are an assistant for question-answering.", + ttl: 123, + maxBytes: 16384 + ) + do { + try await createConversationCollection() + let _ = try await client.conversations().models().create(params: schema) + } catch HTTPError.clientError(let code, let desc){ + print(desc) + XCTAssertEqual(code, 400) + XCTAssertTrue(desc.contains("Model namespace `test` is not supported.")) + } catch (let error) { + print(error.localizedDescription) + XCTAssertTrue(false) + } + try? await tearDownCollections() + } + + func testConversationModelsRetrieve() async { + do { + let (data, _) = try await client.conversations().models().retrieve() + guard let validData = data else { + throw DataError.dataNotFound + } + XCTAssertEqual(0, validData.count) + } catch (let error) { + print(error) + XCTAssertTrue(false) + } + } + + func testConversationModelUpdate() async { + do { + let _ = try await client.conversations().model(modelId: "conv-model-1").update(params: ConversationModelUpdateSchema()) + } catch HTTPError.clientError(let code, let desc){ + print(desc) + XCTAssertEqual(code, 404) + XCTAssertTrue(desc.contains("Model not found")) + } catch (let error) { + print(error) + XCTAssertTrue(false) + } + } + + func testConversationModelRetrieve() async { + do { + let _ = try await client.conversations().model(modelId: "conv-model-1").retrieve() + } catch HTTPError.clientError(let code, let desc){ + print(desc) + XCTAssertEqual(code, 404) + XCTAssertTrue(desc.contains("Model not found")) + } catch (let error) { + print(error) + XCTAssertTrue(false) + } + } + + func testConversationModelDelete() async { + do { + let _ = try await client.conversations().model(modelId: "conv-model-1").delete() + } catch HTTPError.clientError(let code, let desc){ + print(desc) + XCTAssertEqual(code, 404) + XCTAssertTrue(desc.contains("Model not found")) + } catch (let error) { + print(error) + XCTAssertTrue(false) + } + } + +} diff --git a/Tests/TypesenseTests/TestUtils.swift b/Tests/TypesenseTests/TestUtils.swift index b81ec19..96c5e40 100644 --- a/Tests/TypesenseTests/TestUtils.swift +++ b/Tests/TypesenseTests/TestUtils.swift @@ -1,65 +1,66 @@ import Typesense - -let CONFIG = Configuration(nodes: [Node(host: "localhost", port: "8108", nodeProtocol: "http")], apiKey: "xyz", logger: Logger(debugMode: true)) +let NODES = [Node(host: "localhost", port: "8108", nodeProtocol: "http")] +let CONFIG = Configuration(nodes: NODES, apiKey: "xyz", logger: Logger(debugMode: true)) let client = Client(config: CONFIG) +let utilClient = Client(config: Configuration(nodes: NODES, apiKey: "xyz")) func tearDownCollections() async throws { - let (collResp, _) = try await client.collections.retrieveAll() + let (collResp, _) = try await utilClient.collections.retrieveAll() guard let validData = collResp else { throw DataError.dataNotFound } for item in validData { - let _ = try await client.collection(name: item.name).delete() + let _ = try await utilClient.collection(name: item.name).delete() } } func tearDownPresets() async throws { - let (presets, _) = try await client.presets().retrieve() + let (presets, _) = try await utilClient.presets().retrieve() guard let validData = presets else { throw DataError.dataNotFound } for item in validData.presets { - let _ = try await client.preset(item.name).delete() + let _ = try await utilClient.preset(item.name).delete() } } func tearDownStopwords() async throws { - let (data, _) = try await client.stopwords().retrieve() + let (data, _) = try await utilClient.stopwords().retrieve() guard let validData = data else { throw DataError.dataNotFound } for item in validData { - let _ = try await client.stopword(item._id).delete() + let _ = try await utilClient.stopword(item._id).delete() } } func tearDownAnalyticsRules() async throws { - let (data, _) = try await client.analytics().rules().retrieveAll() + let (data, _) = try await utilClient.analytics().rules().retrieveAll() guard let validData = data?.rules else { throw DataError.dataNotFound } for item in validData { - let _ = try await client.analytics().rule(id: item.name).delete() + let _ = try await utilClient.analytics().rule(id: item.name).delete() } } func tearDownAPIKeys() async throws { - let (data, _) = try await client.keys().retrieve() + let (data, _) = try await utilClient.keys().retrieve() guard let validData = data?.keys else { throw DataError.dataNotFound } for item in validData { - let _ = try await client.keys().delete(id: item._id) + let _ = try await utilClient.keys().delete(id: item._id) } } func tearDownAliases() async throws { - let (data, _) = try await client.aliases().retrieve() + let (data, _) = try await utilClient.aliases().retrieve() guard let validData = data?.aliases else { throw DataError.dataNotFound } for item in validData { - let _ = try await client.aliases().delete(name: item.name) + let _ = try await utilClient.aliases().delete(name: item.name) } } @@ -70,23 +71,23 @@ func createCollection() async throws { Field(name: "country", type: "string", facet: true), Field(name: "metadata", type: "object", _optional: true, facet: true) ], defaultSortingField: "num_employees", enableNestedFields: true) - let _ = try await client.collections.create(schema: schema) + let _ = try await utilClient.collections.create(schema: schema) } func createDocument() async throws { let data = try encoder.encode(Company(id: "test-id", company_name: "Stark Industries", num_employees: 5215, country: "USA", metadata: ["open":false])) - let _ = try await client.collection(name: "companies").documents().create(document: data) + let _ = try await utilClient.collection(name: "companies").documents().create(document: data) } func createAnOverride() async throws { - let _ = try await client.collection(name: "companies").overrides().upsert( + let _ = try await utilClient.collection(name: "companies").overrides().upsert( overrideId: "test-id", params: SearchOverrideSchema(rule: SearchOverrideRule(filterBy: "test"), filterBy: "test:=true", metadata: SearchOverrideExclude(_id: "exclude-id")) ) } func createSingleCollectionSearchPreset() async throws { - let _ = try await client.presets().upsert( + let _ = try await utilClient.presets().upsert( presetName: "test-id", params: PresetUpsertSchema( value: .singleCollectionSearch(SearchParameters(q: "apple")) @@ -95,7 +96,7 @@ func createSingleCollectionSearchPreset() async throws { } func createMultiSearchPreset() async throws { - let _ = try await client.presets().upsert( + let _ = try await utilClient.presets().upsert( presetName: "test-id-preset-multi-search", params: PresetUpsertSchema( value: .multiSearch(MultiSearchSearchesParameter(searches: [MultiSearchCollectionParameters(q: "banana")])) @@ -104,7 +105,7 @@ func createMultiSearchPreset() async throws { } func createStopwordSet() async throws { - let _ = try await client.stopwords().upsert( + let _ = try await utilClient.stopwords().upsert( stopwordsSetId: "test-id-stopword-set", params: StopwordsSetUpsertSchema( stopwords: ["states","united"], @@ -114,7 +115,7 @@ func createStopwordSet() async throws { } func createAnalyticRule() async throws { - let _ = try await client.analytics().rules().upsert(params: AnalyticsRuleSchema( + let _ = try await utilClient.analytics().rules().upsert(params: AnalyticsRuleSchema( name: "product_queries_aggregation", type: .counter, params: AnalyticsRuleParameters( @@ -127,12 +128,23 @@ func createAnalyticRule() async throws { } func createAPIKey() async throws -> ApiKey { - let (data, _) = try await client.keys().create( ApiKeySchema(_description: "Test key with all privileges", actions: ["*"], collections: ["*"])) + let (data, _) = try await utilClient.keys().create( ApiKeySchema(_description: "Test key with all privileges", actions: ["*"], collections: ["*"])) return data! } func createAlias() async throws { - let _ = try await client.aliases().upsert(name: "companies", collection: CollectionAliasSchema(collectionName: "companies_june")) + let _ = try await utilClient.aliases().upsert(name: "companies", collection: CollectionAliasSchema(collectionName: "companies_june")) +} + +func createConversationCollection() async throws { + let schema = CollectionSchema(name: "conversation_store", fields: [ + Field(name: "conversation_id", type: "string"), + Field(name: "model_id", type: "string"), + Field(name: "timestamp", type: "int32"), + Field(name: "role", type: "string", index: false), + Field(name: "message", type: "string", index: false) + ]) + let _ = try await utilClient.collections.create(schema: schema) } struct Product: Codable, Equatable { From 900c067f23366fac9cd5b74833c190e306bc54a8 Mon Sep 17 00:00:00 2001 From: Hayden Date: Fri, 30 Aug 2024 22:26:34 +0700 Subject: [PATCH 2/2] Update README API documentation Add API reference for: - aliases - API keys - synonyms - operations - conversation models - documents import, export and update, delete by filter --- README.md | 196 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 195 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b72b4a1..6fe5918 100644 --- a/README.md +++ b/README.md @@ -86,6 +86,134 @@ let (data, response) = try await client.collection(name: "schools").documents(). This returns a `SearchResult` object as the data, which can be further parsed as desired. +### Bulk import documents + +```swift +let jsonL = Data("{}".utf8) +let (data, response) = try await client.collection(name: "companies").documents().importBatch(jsonL, options: ImportDocumentsParameters( + action: .upsert, + batchSize: 10, + dirtyValues: .drop, + remoteEmbeddingBatchSize: 10, + returnDoc: true, + returnId: false +)) +``` + +### Update multiple documents by query + +```swift +let (data, response) = try await client.collection(name: "companies").documents().update( + document: ["company_size": "large"], + options: UpdateDocumentsByFilterParameters(filterBy: "num_employees:>1000") +) +``` + +### Delete multiple documents by query + +```swift +let (data, response) = try await client.collection(name: "companies").documents().delete( + options: DeleteDocumentsParameters(filterBy: "num_employees:>100") +) +``` + +### Export documents + +```swift +let (data, response) = try await client.collection(name: "companies").documents().export(options: ExportDocumentsParameters(excludeFields: "country")) +``` + +### Create or update a collection alias + +```swift +let schema = CollectionAliasSchema(collectionName: "companies_june") +let (data, response) = try await client.aliases().upsert(name: "companies", collection: schema) +``` + +### Retrieve all aliases + +```swift +let (data, response) = try await client.aliases().retrieve() +``` + +### Retrieve an alias + +```swift +let (data, response) = try await client.aliases().retrieve(name: "companies") +``` + +### Delete an alias + +```swift +let (data, response) = try await client.aliases().delete(name: "companies") +``` + +### Create an API key + +```swift +let adminKey = ApiKeySchema(_description: "Test key with all privileges", actions: ["*"], collections: ["*"]) +let (data, response) = try await client.keys().create(adminKey) +``` + +### Retrieve all API keys + +```swift +let (data, response) = try await client.keys().retrieve() +``` + +### Retrieve an API key + +```swift +let (data, response) = try await client.keys().retrieve(id: 1) +``` + +### Delete an API key + +```swift +let (data, response) = try await client.keys().delete(id: 1) +``` + +### Create a conversation model + +```swift +let schema = ConversationModelCreateSchema( + _id: "conv-model-1", + modelName: "openai/gpt-3.5-turbo", + apiKey: "OPENAI_API_KEY", + historyCollection: "conversation_store", + systemPrompt: "You are an assistant for question-answering...", + ttl: 10000, + maxBytes: 16384 +) +let (data, response) = try await client.conversations().models().create(params: schema) +``` + +### Retrieve all conversation models + +```swift +let (data, response) = try await client.conversations().models().retrieve() +``` + +### Retrieve a conversation model + +```swift +let (data, response) = try await client.conversations().model(modelId: "conv-model-1").retrieve() +``` + +### Update a conversation model + +```swift +let (data, response) = try await client.conversations().model(modelId: "conv-model-1").update(params: ConversationModelUpdateSchema( + systemPrompt: "..." +)) +``` + +### Delete a conversation model + +```swift +let (data, response) = try await client.conversations().model(modelId: "conv-model-1").delete() +``` + ### Create or update an override ```swift @@ -187,12 +315,79 @@ let (data, response) = try await client.stopword("stopword_set1").retrieve() let (data, response) = try await client.stopword("stopword_set1").delete() ``` +### Create or update a synonym + +```swift +let schema = SearchSynonymSchema(synonyms: ["blazer", "coat", "jacket"]) +let (data, response) = try await client.collection(name: "products").synonyms().upsert(id: "coat-synonyms", schema) +``` + +### Retrieve all synonyms + +```swift +let (data, response) = try await client.collection(name: "products").synonyms().retrieve() +``` + +### Retrieve a synonym + +```swift +let (data, response) = try await client.collection(name: "products").synonyms().retrieve(id: "coat-synonyms") +``` + +### Delete a synonym + +```swift +let (data, response) = try await myClient.collection(name: "products").synonyms().delete(id: "coat-synonyms") +``` + ### Retrieve debug information ```swift let (data, response) = try await client.operations().getDebug() ``` +### Retrieve health status + +```swift +let (data, response) = try await client.operations().getHealth() +``` + +### Retrieve API stats + +```swift +let (data, response) = try await client.operations().getStats() +``` + +### Retrieve Cluster Metrics + +```swift +let (data, response) = try await client.operations().getMetrics() +``` + +### Re-elect Leader + +```swift +let (data, response) = try await client.operations().vote() +``` + +### Toggle Slow Request Log + +```swift +let (data, response) = try await client.operations().toggleSlowRequestLog(seconds: 2) +``` + +### Clear cache + +```swift +let (data, response) = try await client.operations().clearCache() +``` + +### Create Snapshot (for backups) + +```swift +let (data, response) = try await client.operations().snapshot(path: "/tmp/typesense-data-snapshot") +``` + ## Contributing Issues and pull requests are welcome on GitHub at [Typesense Swift](https://github.com/typesense/typesense-swift). Do note that the Models used in the Swift client are generated by [Swagger-Codegen](https://github.com/swagger-api/swagger-codegen) and are automated to be modified in order to prevent major errors. So please do use the shell script that is provided in the repo to generate the models: @@ -205,5 +400,4 @@ The generated Models (inside the Models directory) are to be used inside the Mod ## TODO: Features -- Dealing with Dirty Data - Scoped Search Key