From 02f28358dd69adf9ea0e95aa501343af09ad60f9 Mon Sep 17 00:00:00 2001 From: Alexander Barannikov <32936723+japdubengsub@users.noreply.github.com> Date: Thu, 7 Nov 2024 11:01:33 +0000 Subject: [PATCH] [OPIK-318] - [Prompt library] SDK Prompt implementation - create/get (#557) * update openapi spec * update openapi spec (with prompt) * update openapi spec (WIP) * fix openapi for fern * draft implementation of create_prompt() and get_prompt() * update openapi spec * create/get return PromptDetail * use public Prompt class WIP * fix circular import * add new method create_prompt_detail * update openapi spec * add e2e tests * move prompt-related api calls to new file * remove description param * rename Prompt.template to Prompt.prompt * use double curly brackets in prompts template * make id field private * simplified solution --- .../code_generation/fern/openapi/openapi.yaml | 630 +++++++++- sdks/python/src/opik/__init__.py | 2 + .../src/opik/api_objects/opik_client.py | 43 + .../src/opik/api_objects/prompt/__init__.py | 5 + .../src/opik/api_objects/prompt/client.py | 93 ++ .../src/opik/api_objects/prompt/prompt.py | 83 ++ sdks/python/src/opik/rest_api/__init__.py | 20 + sdks/python/src/opik/rest_api/client.py | 75 ++ .../src/opik/rest_api/datasets/client.py | 20 +- .../opik/rest_api/errors/bad_request_error.py | 5 +- .../opik/rest_api/errors/conflict_error.py | 5 +- .../errors/unprocessable_entity_error.py | 5 +- .../src/opik/rest_api/projects/client.py | 35 +- .../src/opik/rest_api/prompts/__init__.py | 1 + .../src/opik/rest_api/prompts/client.py | 1085 +++++++++++++++++ .../src/opik/rest_api/types/__init__.py | 18 + .../python/src/opik/rest_api/types/dataset.py | 2 + .../src/opik/rest_api/types/dataset_public.py | 2 + .../src/opik/rest_api/types/error_message.py | 4 +- .../rest_api/types/error_message_detail.py | 44 + .../rest_api/types/project_page_public.py | 5 + sdks/python/src/opik/rest_api/types/prompt.py | 52 + .../src/opik/rest_api/types/prompt_detail.py | 51 + .../opik/rest_api/types/prompt_page_public.py | 46 + .../src/opik/rest_api/types/prompt_public.py | 49 + .../src/opik/rest_api/types/prompt_version.py | 56 + .../rest_api/types/prompt_version_detail.py | 56 + .../types/prompt_version_page_public.py | 46 + .../rest_api/types/prompt_version_public.py | 55 + sdks/python/tests/e2e/test_prompt.py | 154 +++ 30 files changed, 2718 insertions(+), 29 deletions(-) create mode 100644 sdks/python/src/opik/api_objects/prompt/__init__.py create mode 100644 sdks/python/src/opik/api_objects/prompt/client.py create mode 100644 sdks/python/src/opik/api_objects/prompt/prompt.py create mode 100644 sdks/python/src/opik/rest_api/prompts/__init__.py create mode 100644 sdks/python/src/opik/rest_api/prompts/client.py create mode 100644 sdks/python/src/opik/rest_api/types/error_message_detail.py create mode 100644 sdks/python/src/opik/rest_api/types/prompt.py create mode 100644 sdks/python/src/opik/rest_api/types/prompt_detail.py create mode 100644 sdks/python/src/opik/rest_api/types/prompt_page_public.py create mode 100644 sdks/python/src/opik/rest_api/types/prompt_public.py create mode 100644 sdks/python/src/opik/rest_api/types/prompt_version.py create mode 100644 sdks/python/src/opik/rest_api/types/prompt_version_detail.py create mode 100644 sdks/python/src/opik/rest_api/types/prompt_version_page_public.py create mode 100644 sdks/python/src/opik/rest_api/types/prompt_version_public.py create mode 100644 sdks/python/tests/e2e/test_prompt.py diff --git a/sdks/python/code_generation/fern/openapi/openapi.yaml b/sdks/python/code_generation/fern/openapi/openapi.yaml index f59e273c8..2d17712ea 100644 --- a/sdks/python/code_generation/fern/openapi/openapi.yaml +++ b/sdks/python/code_generation/fern/openapi/openapi.yaml @@ -45,6 +45,8 @@ tags: description: Feedback definitions related resources - name: Projects description: Project related resources +- name: Prompts + description: Prompt resources - name: Spans description: Span related resources - name: Traces @@ -86,6 +88,10 @@ paths: type: integer format: int32 default: 10 + - name: with_experiments_only + in: query + schema: + type: boolean - name: name in: query schema: @@ -693,6 +699,10 @@ paths: in: query schema: type: string + - name: sorting + in: query + schema: + type: string responses: "200": description: Project resource @@ -704,7 +714,7 @@ paths: tags: - Projects summary: Create project - description: Get project + description: Create project operationId: createProject requestBody: content: @@ -809,6 +819,302 @@ paths: application/json: schema: $ref: '#/components/schemas/ErrorMessage' + /v1/private/prompts: + get: + tags: + - Prompts + summary: Get prompts + description: Get prompts + operationId: getPrompts + parameters: + - name: page + in: query + schema: + minimum: 1 + type: integer + format: int32 + default: 1 + - name: size + in: query + schema: + minimum: 1 + type: integer + format: int32 + default: 10 + - name: name + in: query + schema: + type: string + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/PromptPage_Public' + post: + tags: + - Prompts + summary: Create prompt + description: Create prompt + operationId: createPrompt + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Prompt_Write' + responses: + "201": + description: Created + headers: + Location: + required: true + style: simple + schema: + type: string + example: "${basePath}/v1/private/prompts/{promptId}" + "422": + description: Unprocessable Content + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorMessage' + "400": + description: Bad Request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorMessage' + "409": + description: Conflict + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorMessage' + /v1/private/prompts/versions: + post: + tags: + - Prompts + summary: Create prompt version + description: Create prompt version + operationId: createPromptVersion + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/CreatePromptVersion_Detail' + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/PromptVersion_Detail' + "422": + description: Unprocessable Content + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorMessage_Detail' + "400": + description: Bad Request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorMessage_Detail' + "409": + description: Conflict + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorMessage_Detail' + /v1/private/prompts/{id}: + get: + tags: + - Prompts + summary: Get prompt by id + description: Get prompt by id + operationId: getPromptById + parameters: + - name: id + in: path + required: true + schema: + type: string + format: uuid + responses: + "200": + description: Prompt resource + content: + application/json: + schema: + $ref: '#/components/schemas/Prompt_Detail' + "404": + description: Not Found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorMessage_Detail' + put: + tags: + - Prompts + summary: Update prompt + description: Update prompt + operationId: updatePrompt + parameters: + - name: id + in: path + required: true + schema: + type: string + format: uuid + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Prompt_Updatable' + responses: + "204": + description: No content + "422": + description: Unprocessable Content + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorMessage' + "400": + description: Bad Request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorMessage' + "404": + description: Not Found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorMessage' + "409": + description: Conflict + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorMessage' + delete: + tags: + - Prompts + summary: Delete prompt + description: Delete prompt + operationId: deletePrompt + parameters: + - name: id + in: path + required: true + schema: + type: string + format: uuid + responses: + "204": + description: No content + /v1/private/prompts/versions/{versionId}: + get: + tags: + - Prompts + summary: Get prompt version by id + description: Get prompt version by id + operationId: getPromptVersionById + parameters: + - name: versionId + in: path + required: true + schema: + type: string + format: uuid + responses: + "200": + description: Prompt version resource + content: + application/json: + schema: + $ref: '#/components/schemas/PromptVersion_Detail' + "404": + description: Not Found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorMessage_Detail' + /v1/private/prompts/{id}/versions: + get: + tags: + - Prompts + summary: Get prompt versions + description: Get prompt versions + operationId: getPromptVersions + parameters: + - name: id + in: path + required: true + schema: + type: string + format: uuid + - name: page + in: query + schema: + minimum: 1 + type: integer + format: int32 + default: 1 + - name: size + in: query + schema: + minimum: 1 + type: integer + format: int32 + default: 10 + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/PromptVersionPage_Public' + /v1/private/prompts/versions/retrieve: + post: + tags: + - Prompts + summary: Retrieve prompt version + description: Retrieve prompt version + operationId: retrievePromptVersion + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PromptVersionRetrieve_Detail' + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/PromptVersion_Detail' + "422": + description: Unprocessable Content + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorMessage_Detail' + "400": + description: Bad Request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorMessage_Detail' + "404": + description: Not Found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorMessage_Detail' /v1/private/spans/{id}/feedback-scores: put: tags: @@ -1246,6 +1552,14 @@ paths: description: default response content: application/json: {} + /is-alive/ver: + get: + operationId: version + responses: + default: + description: default response + content: + application/json: {} components: schemas: TraceCountResponse: @@ -1294,10 +1608,18 @@ components: type: integer format: int64 readOnly: true + dataset_items_count: + type: integer + format: int64 + readOnly: true most_recent_experiment_at: type: string format: date-time readOnly: true + last_created_experiment_at: + type: string + format: date-time + readOnly: true Dataset_Write: required: - name @@ -1748,10 +2070,18 @@ components: type: integer format: int64 readOnly: true + dataset_items_count: + type: integer + format: int64 + readOnly: true most_recent_experiment_at: type: string format: date-time readOnly: true + last_created_experiment_at: + type: string + format: date-time + readOnly: true DatasetIdentifier_Public: required: - dataset_name @@ -1883,10 +2213,13 @@ components: ErrorMessage: type: object properties: - errors: - type: array - items: - type: string + code: + type: integer + format: int32 + message: + type: string + details: + type: string ChunkedOutputJsonNode: type: object properties: @@ -2588,6 +2921,10 @@ components: type: array items: $ref: '#/components/schemas/Project_Public' + sortableBy: + type: array + items: + type: string Project_Public: required: - name @@ -2629,6 +2966,289 @@ components: description: pattern: (?s)^\s*(\S.*\S|\S)\s*$ type: string + Prompt: + required: + - name + type: object + properties: + id: + type: string + format: uuid + name: + type: string + description: + pattern: (?s)^\s*(\S.*\S|\S)\s*$ + type: string + template: + pattern: (?s)^\s*(\S.*\S|\S)\s*$ + type: string + created_at: + type: string + format: date-time + readOnly: true + created_by: + type: string + readOnly: true + last_updated_at: + type: string + format: date-time + readOnly: true + last_updated_by: + type: string + readOnly: true + version_count: + type: integer + format: int64 + readOnly: true + latest_version: + $ref: '#/components/schemas/PromptVersion' + PromptVersion: + required: + - template + type: object + properties: + id: + type: string + description: "version unique identifier, generated if absent" + format: uuid + prompt_id: + type: string + format: uuid + readOnly: true + commit: + pattern: "^[a-zA-Z0-9]{8}$" + type: string + description: "version short unique identifier, generated if absent. it must\ + \ be 8 characters long" + template: + type: string + variables: + uniqueItems: true + type: array + readOnly: true + items: + type: string + readOnly: true + created_at: + type: string + format: date-time + readOnly: true + created_by: + type: string + readOnly: true + readOnly: true + Prompt_Write: + required: + - name + type: object + properties: + id: + type: string + format: uuid + name: + type: string + description: + pattern: (?s)^\s*(\S.*\S|\S)\s*$ + type: string + template: + pattern: (?s)^\s*(\S.*\S|\S)\s*$ + type: string + PromptVersion_Detail: + required: + - template + type: object + properties: + id: + type: string + description: "version unique identifier, generated if absent" + format: uuid + prompt_id: + type: string + format: uuid + readOnly: true + commit: + pattern: "^[a-zA-Z0-9]{8}$" + type: string + description: "version short unique identifier, generated if absent. it must\ + \ be 8 characters long" + template: + type: string + variables: + uniqueItems: true + type: array + readOnly: true + items: + type: string + readOnly: true + created_at: + type: string + format: date-time + readOnly: true + created_by: + type: string + readOnly: true + ErrorMessage_Detail: + type: object + properties: + code: + type: integer + format: int32 + message: + type: string + details: + type: string + CreatePromptVersion_Detail: + required: + - name + - version + type: object + properties: + name: + type: string + version: + $ref: '#/components/schemas/PromptVersion_Detail' + Prompt_Detail: + required: + - name + type: object + properties: + id: + type: string + format: uuid + name: + type: string + description: + pattern: (?s)^\s*(\S.*\S|\S)\s*$ + type: string + created_at: + type: string + format: date-time + readOnly: true + created_by: + type: string + readOnly: true + last_updated_at: + type: string + format: date-time + readOnly: true + last_updated_by: + type: string + readOnly: true + version_count: + type: integer + format: int64 + readOnly: true + latest_version: + $ref: '#/components/schemas/PromptVersion_Detail' + PromptVersionPage_Public: + type: object + properties: + page: + type: integer + format: int32 + size: + type: integer + format: int32 + total: + type: integer + format: int64 + content: + type: array + items: + $ref: '#/components/schemas/PromptVersion_Public' + PromptVersion_Public: + required: + - template + type: object + properties: + id: + type: string + description: "version unique identifier, generated if absent" + format: uuid + prompt_id: + type: string + format: uuid + readOnly: true + commit: + pattern: "^[a-zA-Z0-9]{8}$" + type: string + description: "version short unique identifier, generated if absent. it must\ + \ be 8 characters long" + template: + type: string + created_at: + type: string + format: date-time + readOnly: true + created_by: + type: string + readOnly: true + PromptPage_Public: + type: object + properties: + page: + type: integer + format: int32 + size: + type: integer + format: int32 + total: + type: integer + format: int64 + content: + type: array + items: + $ref: '#/components/schemas/Prompt_Public' + Prompt_Public: + required: + - name + type: object + properties: + id: + type: string + format: uuid + name: + type: string + description: + pattern: (?s)^\s*(\S.*\S|\S)\s*$ + type: string + created_at: + type: string + format: date-time + readOnly: true + created_by: + type: string + readOnly: true + last_updated_at: + type: string + format: date-time + readOnly: true + last_updated_by: + type: string + readOnly: true + version_count: + type: integer + format: int64 + readOnly: true + PromptVersionRetrieve_Detail: + required: + - name + type: object + properties: + name: + type: string + commit: + type: string + Prompt_Updatable: + required: + - name + type: object + properties: + name: + type: string + description: + pattern: (?s)^\s*(\S.*\S|\S)\s*$ + type: string Span: required: - name diff --git a/sdks/python/src/opik/__init__.py b/sdks/python/src/opik/__init__.py index da5fde2b9..524e796df 100644 --- a/sdks/python/src/opik/__init__.py +++ b/sdks/python/src/opik/__init__.py @@ -1,3 +1,4 @@ +from .api_objects.prompt import Prompt from .decorator.tracker import track, flush_tracker from .api_objects.opik_client import Opik from .api_objects.trace import Trace @@ -23,4 +24,5 @@ "Dataset", "llm_unit", "configure", + "Prompt", ] diff --git a/sdks/python/src/opik/api_objects/opik_client.py b/sdks/python/src/opik/api_objects/opik_client.py index 7a65ed355..cad296cb7 100644 --- a/sdks/python/src/opik/api_objects/opik_client.py +++ b/sdks/python/src/opik/api_objects/opik_client.py @@ -5,6 +5,9 @@ from typing import Optional, Any, Dict, List, Mapping +from .prompt import Prompt +from .prompt.client import PromptClient + from ..types import SpanType, UsageDict, FeedbackScoreDict from . import ( opik_query_language, @@ -605,6 +608,46 @@ def get_project(self, id: str) -> project_public.ProjectPublic: """ return self._rest_client.projects.get_project_by_id(id) + def create_prompt( + self, + name: str, + prompt: str, + ) -> Prompt: + """ + Creates a new prompt with the given name and template. + If a prompt with the same name already exists, it will create a new version of the existing prompt if the templates differ. + + Parameters: + name: The name of the prompt. + prompt: The template content of the prompt. + + Returns: + A Prompt object containing details of the created or retrieved prompt. + + Raises: + ApiError: If there is an error during the creation of the prompt and the status code is not 409. + """ + prompt_client = PromptClient(self._rest_client) + return prompt_client.create_prompt(name=name, prompt=prompt) + + def get_prompt( + self, + name: str, + commit: Optional[str] = None, + ) -> Optional[Prompt]: + """ + Retrieve the prompt detail for a given prompt name and commit version. + + Parameters: + name: The name of the prompt. + commit: An optional commit version of the prompt. If not provided, the latest version is retrieved. + + Returns: + Prompt: The details of the specified prompt. + """ + prompt_client = PromptClient(self._rest_client) + return prompt_client.get_prompt(name=name, commit=commit) + @functools.lru_cache() def get_client_cached() -> Opik: diff --git a/sdks/python/src/opik/api_objects/prompt/__init__.py b/sdks/python/src/opik/api_objects/prompt/__init__.py new file mode 100644 index 000000000..846ffbb0f --- /dev/null +++ b/sdks/python/src/opik/api_objects/prompt/__init__.py @@ -0,0 +1,5 @@ +from .prompt import Prompt + +__all__ = [ + "Prompt", +] diff --git a/sdks/python/src/opik/api_objects/prompt/client.py b/sdks/python/src/opik/api_objects/prompt/client.py new file mode 100644 index 000000000..48fbcd836 --- /dev/null +++ b/sdks/python/src/opik/api_objects/prompt/client.py @@ -0,0 +1,93 @@ +from typing import Optional + +from opik import Prompt +from opik.rest_api import PromptVersionDetail, client as rest_client +from opik.rest_api.core import ApiError + + +class PromptClient: + def __init__(self, client: rest_client.OpikApi): + self._rest_client = client + + def create_prompt( + self, + name: str, + prompt: str, + ) -> Prompt: + """ + Creates the prompt detail for the given prompt name and template. + + Parameters: + - name: The name of the prompt. + - prompt: The template content for the prompt. + + Returns: + - A Prompt object for the provided prompt name and template. + """ + prompt_version = self._get_latest_version(name) + + if prompt_version is None or prompt_version.template != prompt: + prompt_version = self._create_new_version(name=name, prompt=prompt) + + prompt_obj = Prompt.from_fern_prompt_version( + name=name, prompt_version=prompt_version + ) + + return prompt_obj + + def _create_new_version( + self, + name: str, + prompt: str, + ) -> PromptVersionDetail: + new_prompt_version_detail_data = PromptVersionDetail(template=prompt) + new_prompt_version_detail: PromptVersionDetail = ( + self._rest_client.prompts.create_prompt_version( + name=name, + version=new_prompt_version_detail_data, + ) + ) + return new_prompt_version_detail + + def _get_latest_version(self, name: str) -> Optional[PromptVersionDetail]: + try: + prompt_latest_version = self._rest_client.prompts.retrieve_prompt_version( + name=name + ) + return prompt_latest_version + except ApiError as e: + if e.status_code != 404: + raise e + return None + + def get_prompt( + self, + name: str, + commit: Optional[str] = None, + ) -> Optional[Prompt]: + """ + Retrieve the prompt detail for a given prompt name and commit version. + + Parameters: + name: The name of the prompt. + commit: An optional commit version of the prompt. If not provided, the latest version is retrieved. + + Returns: + Prompt: The details of the specified prompt. + """ + try: + prompt_version = self._rest_client.prompts.retrieve_prompt_version( + name=name, + commit=commit, + ) + prompt_obj = Prompt.from_fern_prompt_version( + name=name, prompt_version=prompt_version + ) + + return prompt_obj + + except ApiError as e: + if e.status_code != 404: + raise e + + return None diff --git a/sdks/python/src/opik/api_objects/prompt/prompt.py b/sdks/python/src/opik/api_objects/prompt/prompt.py new file mode 100644 index 000000000..507df4318 --- /dev/null +++ b/sdks/python/src/opik/api_objects/prompt/prompt.py @@ -0,0 +1,83 @@ +from typing import Any + +from opik.rest_api import PromptVersionDetail + + +class Prompt: + """ + Prompt class represents a prompt with a name, prompt text/template and commit hash. + """ + + def __init__( + self, + name: str, + prompt: str, + ) -> None: + """ + Initializes a new instance of the class with the given parameters. + Creates a new prompt using the opik client and sets the initial state of the instance attributes based on the created prompt. + + Parameters: + name: The name for the prompt. + prompt: The template for the prompt. + """ + # we will import opik client here to avoid circular import issue + from opik.api_objects import opik_client + + client = opik_client.get_client_cached() + + new_instance = client.create_prompt( + name=name, + prompt=prompt, + ) + self._name = new_instance.name + self._prompt = new_instance.prompt + self._commit = new_instance.commit + self.__internal_api__prompt_id__: str = new_instance.__internal_api__prompt_id__ + + @property + def name(self) -> str: + """The name of the prompt.""" + return self._name + + @property + def prompt(self) -> str: + """The latest template of the prompt.""" + return self._prompt + + @property + def commit(self) -> str: + """The commit hash of the prompt.""" + return self._commit + + def format(self, **kwargs: Any) -> str: + """ + Replaces placeholders in the template with provided keyword arguments. + + Args: + **kwargs: Arbitrary keyword arguments where the key represents the placeholder + in the template and the value is the value to replace the placeholder with. + + Returns: + A string with all placeholders replaced by their corresponding values from kwargs. + """ + template = self._prompt + for key, value in kwargs.items(): + template = template.replace(f"{{{{{key}}}}}", str(value)) + return template + + @classmethod + def from_fern_prompt_version( + cls, + name: str, + prompt_version: PromptVersionDetail, + ) -> "Prompt": + # will not call __init__ to avoid API calls, create new instance with __new__ + prompt = cls.__new__(cls) + + prompt.__internal_api__prompt_id__ = prompt_version.id + prompt._name = name + prompt._prompt = prompt_version.template + prompt._commit = prompt_version.commit + + return prompt diff --git a/sdks/python/src/opik/rest_api/__init__.py b/sdks/python/src/opik/rest_api/__init__.py index 9ed192796..822486e35 100644 --- a/sdks/python/src/opik/rest_api/__init__.py +++ b/sdks/python/src/opik/rest_api/__init__.py @@ -31,6 +31,7 @@ DatasetPublic, DeleteFeedbackScore, ErrorMessage, + ErrorMessageDetail, ErrorMessagePublic, Experiment, ExperimentItem, @@ -80,6 +81,14 @@ Project, ProjectPagePublic, ProjectPublic, + Prompt, + PromptDetail, + PromptPagePublic, + PromptPublic, + PromptVersion, + PromptVersionDetail, + PromptVersionPagePublic, + PromptVersionPublic, Span, SpanBatch, SpanPagePublic, @@ -108,6 +117,7 @@ experiments, feedback_definitions, projects, + prompts, spans, system_usage, traces, @@ -149,6 +159,7 @@ "DatasetPublic", "DeleteFeedbackScore", "ErrorMessage", + "ErrorMessageDetail", "ErrorMessagePublic", "Experiment", "ExperimentItem", @@ -203,6 +214,14 @@ "Project", "ProjectPagePublic", "ProjectPublic", + "Prompt", + "PromptDetail", + "PromptPagePublic", + "PromptPublic", + "PromptVersion", + "PromptVersionDetail", + "PromptVersionPagePublic", + "PromptVersionPublic", "Span", "SpanBatch", "SpanPagePublic", @@ -223,6 +242,7 @@ "experiments", "feedback_definitions", "projects", + "prompts", "spans", "system_usage", "traces", diff --git a/sdks/python/src/opik/rest_api/client.py b/sdks/python/src/opik/rest_api/client.py index d6332dd50..8933b254d 100644 --- a/sdks/python/src/opik/rest_api/client.py +++ b/sdks/python/src/opik/rest_api/client.py @@ -17,6 +17,7 @@ FeedbackDefinitionsClient, ) from .projects.client import AsyncProjectsClient, ProjectsClient +from .prompts.client import AsyncPromptsClient, PromptsClient from .spans.client import AsyncSpansClient, SpansClient from .system_usage.client import AsyncSystemUsageClient, SystemUsageClient from .traces.client import AsyncTracesClient, TracesClient @@ -86,6 +87,7 @@ def __init__( client_wrapper=self._client_wrapper ) self.projects = ProjectsClient(client_wrapper=self._client_wrapper) + self.prompts = PromptsClient(client_wrapper=self._client_wrapper) self.spans = SpansClient(client_wrapper=self._client_wrapper) self.traces = TracesClient(client_wrapper=self._client_wrapper) @@ -121,6 +123,38 @@ def is_alive( raise ApiError(status_code=_response.status_code, body=_response.text) raise ApiError(status_code=_response.status_code, body=_response_json) + def version( + self, *, request_options: typing.Optional[RequestOptions] = None + ) -> typing.Any: + """ + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + typing.Any + default response + + Examples + -------- + from Opik.client import OpikApi + + client = OpikApi() + client.version() + """ + _response = self._client_wrapper.httpx_client.request( + "is-alive/ver", method="GET", request_options=request_options + ) + try: + if 200 <= _response.status_code < 300: + return pydantic_v1.parse_obj_as(typing.Any, _response.json()) # type: ignore + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, body=_response.text) + raise ApiError(status_code=_response.status_code, body=_response_json) + class AsyncOpikApi: """ @@ -186,6 +220,7 @@ def __init__( client_wrapper=self._client_wrapper ) self.projects = AsyncProjectsClient(client_wrapper=self._client_wrapper) + self.prompts = AsyncPromptsClient(client_wrapper=self._client_wrapper) self.spans = AsyncSpansClient(client_wrapper=self._client_wrapper) self.traces = AsyncTracesClient(client_wrapper=self._client_wrapper) @@ -229,6 +264,46 @@ async def main() -> None: raise ApiError(status_code=_response.status_code, body=_response.text) raise ApiError(status_code=_response.status_code, body=_response_json) + async def version( + self, *, request_options: typing.Optional[RequestOptions] = None + ) -> typing.Any: + """ + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + typing.Any + default response + + Examples + -------- + import asyncio + + from Opik.client import AsyncOpikApi + + client = AsyncOpikApi() + + + async def main() -> None: + await client.version() + + + asyncio.run(main()) + """ + _response = await self._client_wrapper.httpx_client.request( + "is-alive/ver", method="GET", request_options=request_options + ) + try: + if 200 <= _response.status_code < 300: + return pydantic_v1.parse_obj_as(typing.Any, _response.json()) # type: ignore + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, body=_response.text) + raise ApiError(status_code=_response.status_code, body=_response_json) + def _get_base_url( *, base_url: typing.Optional[str] = None, environment: OpikApiEnvironment diff --git a/sdks/python/src/opik/rest_api/datasets/client.py b/sdks/python/src/opik/rest_api/datasets/client.py index 135b4f63d..c810e8cd6 100644 --- a/sdks/python/src/opik/rest_api/datasets/client.py +++ b/sdks/python/src/opik/rest_api/datasets/client.py @@ -28,6 +28,7 @@ def find_datasets( *, page: typing.Optional[int] = None, size: typing.Optional[int] = None, + with_experiments_only: typing.Optional[bool] = None, name: typing.Optional[str] = None, request_options: typing.Optional[RequestOptions] = None, ) -> DatasetPagePublic: @@ -40,6 +41,8 @@ def find_datasets( size : typing.Optional[int] + with_experiments_only : typing.Optional[bool] + name : typing.Optional[str] request_options : typing.Optional[RequestOptions] @@ -60,7 +63,12 @@ def find_datasets( _response = self._client_wrapper.httpx_client.request( "v1/private/datasets", method="GET", - params={"page": page, "size": size, "name": name}, + params={ + "page": page, + "size": size, + "with_experiments_only": with_experiments_only, + "name": name, + }, request_options=request_options, ) try: @@ -669,6 +677,7 @@ async def find_datasets( *, page: typing.Optional[int] = None, size: typing.Optional[int] = None, + with_experiments_only: typing.Optional[bool] = None, name: typing.Optional[str] = None, request_options: typing.Optional[RequestOptions] = None, ) -> DatasetPagePublic: @@ -681,6 +690,8 @@ async def find_datasets( size : typing.Optional[int] + with_experiments_only : typing.Optional[bool] + name : typing.Optional[str] request_options : typing.Optional[RequestOptions] @@ -709,7 +720,12 @@ async def main() -> None: _response = await self._client_wrapper.httpx_client.request( "v1/private/datasets", method="GET", - params={"page": page, "size": size, "name": name}, + params={ + "page": page, + "size": size, + "with_experiments_only": with_experiments_only, + "name": name, + }, request_options=request_options, ) try: diff --git a/sdks/python/src/opik/rest_api/errors/bad_request_error.py b/sdks/python/src/opik/rest_api/errors/bad_request_error.py index 4fabe5ea8..44a05f49b 100644 --- a/sdks/python/src/opik/rest_api/errors/bad_request_error.py +++ b/sdks/python/src/opik/rest_api/errors/bad_request_error.py @@ -1,9 +1,10 @@ # This file was auto-generated by Fern from our API Definition. +import typing + from ..core.api_error import ApiError -from ..types.error_message import ErrorMessage class BadRequestError(ApiError): - def __init__(self, body: ErrorMessage): + def __init__(self, body: typing.Any): super().__init__(status_code=400, body=body) diff --git a/sdks/python/src/opik/rest_api/errors/conflict_error.py b/sdks/python/src/opik/rest_api/errors/conflict_error.py index 4501f08df..416399a3d 100644 --- a/sdks/python/src/opik/rest_api/errors/conflict_error.py +++ b/sdks/python/src/opik/rest_api/errors/conflict_error.py @@ -1,9 +1,10 @@ # This file was auto-generated by Fern from our API Definition. +import typing + from ..core.api_error import ApiError -from ..types.error_message import ErrorMessage class ConflictError(ApiError): - def __init__(self, body: ErrorMessage): + def __init__(self, body: typing.Any): super().__init__(status_code=409, body=body) diff --git a/sdks/python/src/opik/rest_api/errors/unprocessable_entity_error.py b/sdks/python/src/opik/rest_api/errors/unprocessable_entity_error.py index ec1e45460..64448b6bd 100644 --- a/sdks/python/src/opik/rest_api/errors/unprocessable_entity_error.py +++ b/sdks/python/src/opik/rest_api/errors/unprocessable_entity_error.py @@ -1,9 +1,10 @@ # This file was auto-generated by Fern from our API Definition. +import typing + from ..core.api_error import ApiError -from ..types.error_message import ErrorMessage class UnprocessableEntityError(ApiError): - def __init__(self, body: ErrorMessage): + def __init__(self, body: typing.Any): super().__init__(status_code=422, body=body) diff --git a/sdks/python/src/opik/rest_api/projects/client.py b/sdks/python/src/opik/rest_api/projects/client.py index 7607b6348..9a2b964ec 100644 --- a/sdks/python/src/opik/rest_api/projects/client.py +++ b/sdks/python/src/opik/rest_api/projects/client.py @@ -11,7 +11,6 @@ from ..errors.bad_request_error import BadRequestError from ..errors.conflict_error import ConflictError from ..errors.unprocessable_entity_error import UnprocessableEntityError -from ..types.error_message import ErrorMessage from ..types.project_page_public import ProjectPagePublic from ..types.project_public import ProjectPublic @@ -29,6 +28,7 @@ def find_projects( page: typing.Optional[int] = None, size: typing.Optional[int] = None, name: typing.Optional[str] = None, + sorting: typing.Optional[str] = None, request_options: typing.Optional[RequestOptions] = None, ) -> ProjectPagePublic: """ @@ -42,6 +42,8 @@ def find_projects( name : typing.Optional[str] + sorting : typing.Optional[str] + request_options : typing.Optional[RequestOptions] Request-specific configuration. @@ -60,7 +62,7 @@ def find_projects( _response = self._client_wrapper.httpx_client.request( "v1/private/projects", method="GET", - params={"page": page, "size": size, "name": name}, + params={"page": page, "size": size, "name": name, "sorting": sorting}, request_options=request_options, ) try: @@ -79,7 +81,7 @@ def create_project( request_options: typing.Optional[RequestOptions] = None, ) -> None: """ - Get project + Create project Parameters ---------- @@ -115,11 +117,11 @@ def create_project( return if _response.status_code == 400: raise BadRequestError( - pydantic_v1.parse_obj_as(ErrorMessage, _response.json()) + pydantic_v1.parse_obj_as(typing.Any, _response.json()) ) # type: ignore if _response.status_code == 422: raise UnprocessableEntityError( - pydantic_v1.parse_obj_as(ErrorMessage, _response.json()) + pydantic_v1.parse_obj_as(typing.Any, _response.json()) ) # type: ignore _response_json = _response.json() except JSONDecodeError: @@ -202,7 +204,7 @@ def delete_project_by_id( return if _response.status_code == 409: raise ConflictError( - pydantic_v1.parse_obj_as(ErrorMessage, _response.json()) + pydantic_v1.parse_obj_as(typing.Any, _response.json()) ) # type: ignore _response_json = _response.json() except JSONDecodeError: @@ -256,11 +258,11 @@ def update_project( return if _response.status_code == 400: raise BadRequestError( - pydantic_v1.parse_obj_as(ErrorMessage, _response.json()) + pydantic_v1.parse_obj_as(typing.Any, _response.json()) ) # type: ignore if _response.status_code == 422: raise UnprocessableEntityError( - pydantic_v1.parse_obj_as(ErrorMessage, _response.json()) + pydantic_v1.parse_obj_as(typing.Any, _response.json()) ) # type: ignore _response_json = _response.json() except JSONDecodeError: @@ -278,6 +280,7 @@ async def find_projects( page: typing.Optional[int] = None, size: typing.Optional[int] = None, name: typing.Optional[str] = None, + sorting: typing.Optional[str] = None, request_options: typing.Optional[RequestOptions] = None, ) -> ProjectPagePublic: """ @@ -291,6 +294,8 @@ async def find_projects( name : typing.Optional[str] + sorting : typing.Optional[str] + request_options : typing.Optional[RequestOptions] Request-specific configuration. @@ -317,7 +322,7 @@ async def main() -> None: _response = await self._client_wrapper.httpx_client.request( "v1/private/projects", method="GET", - params={"page": page, "size": size, "name": name}, + params={"page": page, "size": size, "name": name, "sorting": sorting}, request_options=request_options, ) try: @@ -336,7 +341,7 @@ async def create_project( request_options: typing.Optional[RequestOptions] = None, ) -> None: """ - Get project + Create project Parameters ---------- @@ -380,11 +385,11 @@ async def main() -> None: return if _response.status_code == 400: raise BadRequestError( - pydantic_v1.parse_obj_as(ErrorMessage, _response.json()) + pydantic_v1.parse_obj_as(typing.Any, _response.json()) ) # type: ignore if _response.status_code == 422: raise UnprocessableEntityError( - pydantic_v1.parse_obj_as(ErrorMessage, _response.json()) + pydantic_v1.parse_obj_as(typing.Any, _response.json()) ) # type: ignore _response_json = _response.json() except JSONDecodeError: @@ -483,7 +488,7 @@ async def main() -> None: return if _response.status_code == 409: raise ConflictError( - pydantic_v1.parse_obj_as(ErrorMessage, _response.json()) + pydantic_v1.parse_obj_as(typing.Any, _response.json()) ) # type: ignore _response_json = _response.json() except JSONDecodeError: @@ -545,11 +550,11 @@ async def main() -> None: return if _response.status_code == 400: raise BadRequestError( - pydantic_v1.parse_obj_as(ErrorMessage, _response.json()) + pydantic_v1.parse_obj_as(typing.Any, _response.json()) ) # type: ignore if _response.status_code == 422: raise UnprocessableEntityError( - pydantic_v1.parse_obj_as(ErrorMessage, _response.json()) + pydantic_v1.parse_obj_as(typing.Any, _response.json()) ) # type: ignore _response_json = _response.json() except JSONDecodeError: diff --git a/sdks/python/src/opik/rest_api/prompts/__init__.py b/sdks/python/src/opik/rest_api/prompts/__init__.py new file mode 100644 index 000000000..67a41b274 --- /dev/null +++ b/sdks/python/src/opik/rest_api/prompts/__init__.py @@ -0,0 +1 @@ +# This file was auto-generated by Fern from our API Definition. diff --git a/sdks/python/src/opik/rest_api/prompts/client.py b/sdks/python/src/opik/rest_api/prompts/client.py new file mode 100644 index 000000000..b2e0a22d6 --- /dev/null +++ b/sdks/python/src/opik/rest_api/prompts/client.py @@ -0,0 +1,1085 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing +from json.decoder import JSONDecodeError + +from ..core.api_error import ApiError +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.jsonable_encoder import jsonable_encoder +from ..core.pydantic_utilities import pydantic_v1 +from ..core.request_options import RequestOptions +from ..errors.bad_request_error import BadRequestError +from ..errors.conflict_error import ConflictError +from ..errors.not_found_error import NotFoundError +from ..errors.unprocessable_entity_error import UnprocessableEntityError +from ..types.prompt_detail import PromptDetail +from ..types.prompt_page_public import PromptPagePublic +from ..types.prompt_version_detail import PromptVersionDetail +from ..types.prompt_version_page_public import PromptVersionPagePublic + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class PromptsClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._client_wrapper = client_wrapper + + def get_prompts( + self, + *, + page: typing.Optional[int] = None, + size: typing.Optional[int] = None, + name: typing.Optional[str] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> PromptPagePublic: + """ + Get prompts + + Parameters + ---------- + page : typing.Optional[int] + + size : typing.Optional[int] + + name : typing.Optional[str] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + PromptPagePublic + OK + + Examples + -------- + from Opik.client import OpikApi + + client = OpikApi() + client.prompts.get_prompts() + """ + _response = self._client_wrapper.httpx_client.request( + "v1/private/prompts", + method="GET", + params={"page": page, "size": size, "name": name}, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + return pydantic_v1.parse_obj_as(PromptPagePublic, _response.json()) # type: ignore + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, body=_response.text) + raise ApiError(status_code=_response.status_code, body=_response_json) + + def create_prompt( + self, + *, + name: str, + id: typing.Optional[str] = OMIT, + description: typing.Optional[str] = OMIT, + template: typing.Optional[str] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> None: + """ + Create prompt + + Parameters + ---------- + name : str + + id : typing.Optional[str] + + description : typing.Optional[str] + + template : typing.Optional[str] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + None + + Examples + -------- + from Opik.client import OpikApi + + client = OpikApi() + client.prompts.create_prompt( + name="name", + ) + """ + _response = self._client_wrapper.httpx_client.request( + "v1/private/prompts", + method="POST", + json={ + "id": id, + "name": name, + "description": description, + "template": template, + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + return + if _response.status_code == 400: + raise BadRequestError( + pydantic_v1.parse_obj_as(typing.Any, _response.json()) + ) # type: ignore + if _response.status_code == 409: + raise ConflictError( + pydantic_v1.parse_obj_as(typing.Any, _response.json()) + ) # type: ignore + if _response.status_code == 422: + raise UnprocessableEntityError( + pydantic_v1.parse_obj_as(typing.Any, _response.json()) + ) # type: ignore + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, body=_response.text) + raise ApiError(status_code=_response.status_code, body=_response_json) + + def create_prompt_version( + self, + *, + name: str, + version: PromptVersionDetail, + request_options: typing.Optional[RequestOptions] = None, + ) -> PromptVersionDetail: + """ + Create prompt version + + Parameters + ---------- + name : str + + version : PromptVersionDetail + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + PromptVersionDetail + OK + + Examples + -------- + from Opik import PromptVersionDetail + from Opik.client import OpikApi + + client = OpikApi() + client.prompts.create_prompt_version( + name="name", + version=PromptVersionDetail( + template="template", + ), + ) + """ + _response = self._client_wrapper.httpx_client.request( + "v1/private/prompts/versions", + method="POST", + json={"name": name, "version": version}, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + return pydantic_v1.parse_obj_as(PromptVersionDetail, _response.json()) # type: ignore + if _response.status_code == 400: + raise BadRequestError( + pydantic_v1.parse_obj_as(typing.Any, _response.json()) + ) # type: ignore + if _response.status_code == 409: + raise ConflictError( + pydantic_v1.parse_obj_as(typing.Any, _response.json()) + ) # type: ignore + if _response.status_code == 422: + raise UnprocessableEntityError( + pydantic_v1.parse_obj_as(typing.Any, _response.json()) + ) # type: ignore + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, body=_response.text) + raise ApiError(status_code=_response.status_code, body=_response_json) + + def get_prompt_by_id( + self, id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> PromptDetail: + """ + Get prompt by id + + Parameters + ---------- + id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + PromptDetail + Prompt resource + + Examples + -------- + from Opik.client import OpikApi + + client = OpikApi() + client.prompts.get_prompt_by_id( + id="id", + ) + """ + _response = self._client_wrapper.httpx_client.request( + f"v1/private/prompts/{jsonable_encoder(id)}", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + return pydantic_v1.parse_obj_as(PromptDetail, _response.json()) # type: ignore + if _response.status_code == 404: + raise NotFoundError( + pydantic_v1.parse_obj_as(typing.Any, _response.json()) + ) # type: ignore + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, body=_response.text) + raise ApiError(status_code=_response.status_code, body=_response_json) + + def update_prompt( + self, + id: str, + *, + name: str, + description: typing.Optional[str] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> None: + """ + Update prompt + + Parameters + ---------- + id : str + + name : str + + description : typing.Optional[str] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + None + + Examples + -------- + from Opik.client import OpikApi + + client = OpikApi() + client.prompts.update_prompt( + id="id", + name="name", + ) + """ + _response = self._client_wrapper.httpx_client.request( + f"v1/private/prompts/{jsonable_encoder(id)}", + method="PUT", + json={"name": name, "description": description}, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + return + if _response.status_code == 400: + raise BadRequestError( + pydantic_v1.parse_obj_as(typing.Any, _response.json()) + ) # type: ignore + if _response.status_code == 404: + raise NotFoundError( + pydantic_v1.parse_obj_as(typing.Any, _response.json()) + ) # type: ignore + if _response.status_code == 409: + raise ConflictError( + pydantic_v1.parse_obj_as(typing.Any, _response.json()) + ) # type: ignore + if _response.status_code == 422: + raise UnprocessableEntityError( + pydantic_v1.parse_obj_as(typing.Any, _response.json()) + ) # type: ignore + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, body=_response.text) + raise ApiError(status_code=_response.status_code, body=_response_json) + + def delete_prompt( + self, id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> None: + """ + Delete prompt + + Parameters + ---------- + id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + None + + Examples + -------- + from Opik.client import OpikApi + + client = OpikApi() + client.prompts.delete_prompt( + id="id", + ) + """ + _response = self._client_wrapper.httpx_client.request( + f"v1/private/prompts/{jsonable_encoder(id)}", + method="DELETE", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + return + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, body=_response.text) + raise ApiError(status_code=_response.status_code, body=_response_json) + + def get_prompt_version_by_id( + self, + version_id: str, + *, + request_options: typing.Optional[RequestOptions] = None, + ) -> PromptVersionDetail: + """ + Get prompt version by id + + Parameters + ---------- + version_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + PromptVersionDetail + Prompt version resource + + Examples + -------- + from Opik.client import OpikApi + + client = OpikApi() + client.prompts.get_prompt_version_by_id( + version_id="versionId", + ) + """ + _response = self._client_wrapper.httpx_client.request( + f"v1/private/prompts/versions/{jsonable_encoder(version_id)}", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + return pydantic_v1.parse_obj_as(PromptVersionDetail, _response.json()) # type: ignore + if _response.status_code == 404: + raise NotFoundError( + pydantic_v1.parse_obj_as(typing.Any, _response.json()) + ) # type: ignore + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, body=_response.text) + raise ApiError(status_code=_response.status_code, body=_response_json) + + def get_prompt_versions( + self, + id: str, + *, + page: typing.Optional[int] = None, + size: typing.Optional[int] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> PromptVersionPagePublic: + """ + Get prompt versions + + Parameters + ---------- + id : str + + page : typing.Optional[int] + + size : typing.Optional[int] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + PromptVersionPagePublic + OK + + Examples + -------- + from Opik.client import OpikApi + + client = OpikApi() + client.prompts.get_prompt_versions( + id="id", + ) + """ + _response = self._client_wrapper.httpx_client.request( + f"v1/private/prompts/{jsonable_encoder(id)}/versions", + method="GET", + params={"page": page, "size": size}, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + return pydantic_v1.parse_obj_as( + PromptVersionPagePublic, _response.json() + ) # type: ignore + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, body=_response.text) + raise ApiError(status_code=_response.status_code, body=_response_json) + + def retrieve_prompt_version( + self, + *, + name: str, + commit: typing.Optional[str] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> PromptVersionDetail: + """ + Retrieve prompt version + + Parameters + ---------- + name : str + + commit : typing.Optional[str] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + PromptVersionDetail + OK + + Examples + -------- + from Opik.client import OpikApi + + client = OpikApi() + client.prompts.retrieve_prompt_version( + name="name", + ) + """ + _response = self._client_wrapper.httpx_client.request( + "v1/private/prompts/versions/retrieve", + method="POST", + json={"name": name, "commit": commit}, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + return pydantic_v1.parse_obj_as(PromptVersionDetail, _response.json()) # type: ignore + if _response.status_code == 400: + raise BadRequestError( + pydantic_v1.parse_obj_as(typing.Any, _response.json()) + ) # type: ignore + if _response.status_code == 404: + raise NotFoundError( + pydantic_v1.parse_obj_as(typing.Any, _response.json()) + ) # type: ignore + if _response.status_code == 422: + raise UnprocessableEntityError( + pydantic_v1.parse_obj_as(typing.Any, _response.json()) + ) # type: ignore + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, body=_response.text) + raise ApiError(status_code=_response.status_code, body=_response_json) + + +class AsyncPromptsClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._client_wrapper = client_wrapper + + async def get_prompts( + self, + *, + page: typing.Optional[int] = None, + size: typing.Optional[int] = None, + name: typing.Optional[str] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> PromptPagePublic: + """ + Get prompts + + Parameters + ---------- + page : typing.Optional[int] + + size : typing.Optional[int] + + name : typing.Optional[str] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + PromptPagePublic + OK + + Examples + -------- + import asyncio + + from Opik.client import AsyncOpikApi + + client = AsyncOpikApi() + + + async def main() -> None: + await client.prompts.get_prompts() + + + asyncio.run(main()) + """ + _response = await self._client_wrapper.httpx_client.request( + "v1/private/prompts", + method="GET", + params={"page": page, "size": size, "name": name}, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + return pydantic_v1.parse_obj_as(PromptPagePublic, _response.json()) # type: ignore + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, body=_response.text) + raise ApiError(status_code=_response.status_code, body=_response_json) + + async def create_prompt( + self, + *, + name: str, + id: typing.Optional[str] = OMIT, + description: typing.Optional[str] = OMIT, + template: typing.Optional[str] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> None: + """ + Create prompt + + Parameters + ---------- + name : str + + id : typing.Optional[str] + + description : typing.Optional[str] + + template : typing.Optional[str] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + None + + Examples + -------- + import asyncio + + from Opik.client import AsyncOpikApi + + client = AsyncOpikApi() + + + async def main() -> None: + await client.prompts.create_prompt( + name="name", + ) + + + asyncio.run(main()) + """ + _response = await self._client_wrapper.httpx_client.request( + "v1/private/prompts", + method="POST", + json={ + "id": id, + "name": name, + "description": description, + "template": template, + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + return + if _response.status_code == 400: + raise BadRequestError( + pydantic_v1.parse_obj_as(typing.Any, _response.json()) + ) # type: ignore + if _response.status_code == 409: + raise ConflictError( + pydantic_v1.parse_obj_as(typing.Any, _response.json()) + ) # type: ignore + if _response.status_code == 422: + raise UnprocessableEntityError( + pydantic_v1.parse_obj_as(typing.Any, _response.json()) + ) # type: ignore + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, body=_response.text) + raise ApiError(status_code=_response.status_code, body=_response_json) + + async def create_prompt_version( + self, + *, + name: str, + version: PromptVersionDetail, + request_options: typing.Optional[RequestOptions] = None, + ) -> PromptVersionDetail: + """ + Create prompt version + + Parameters + ---------- + name : str + + version : PromptVersionDetail + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + PromptVersionDetail + OK + + Examples + -------- + import asyncio + + from Opik import PromptVersionDetail + from Opik.client import AsyncOpikApi + + client = AsyncOpikApi() + + + async def main() -> None: + await client.prompts.create_prompt_version( + name="name", + version=PromptVersionDetail( + template="template", + ), + ) + + + asyncio.run(main()) + """ + _response = await self._client_wrapper.httpx_client.request( + "v1/private/prompts/versions", + method="POST", + json={"name": name, "version": version}, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + return pydantic_v1.parse_obj_as(PromptVersionDetail, _response.json()) # type: ignore + if _response.status_code == 400: + raise BadRequestError( + pydantic_v1.parse_obj_as(typing.Any, _response.json()) + ) # type: ignore + if _response.status_code == 409: + raise ConflictError( + pydantic_v1.parse_obj_as(typing.Any, _response.json()) + ) # type: ignore + if _response.status_code == 422: + raise UnprocessableEntityError( + pydantic_v1.parse_obj_as(typing.Any, _response.json()) + ) # type: ignore + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, body=_response.text) + raise ApiError(status_code=_response.status_code, body=_response_json) + + async def get_prompt_by_id( + self, id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> PromptDetail: + """ + Get prompt by id + + Parameters + ---------- + id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + PromptDetail + Prompt resource + + Examples + -------- + import asyncio + + from Opik.client import AsyncOpikApi + + client = AsyncOpikApi() + + + async def main() -> None: + await client.prompts.get_prompt_by_id( + id="id", + ) + + + asyncio.run(main()) + """ + _response = await self._client_wrapper.httpx_client.request( + f"v1/private/prompts/{jsonable_encoder(id)}", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + return pydantic_v1.parse_obj_as(PromptDetail, _response.json()) # type: ignore + if _response.status_code == 404: + raise NotFoundError( + pydantic_v1.parse_obj_as(typing.Any, _response.json()) + ) # type: ignore + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, body=_response.text) + raise ApiError(status_code=_response.status_code, body=_response_json) + + async def update_prompt( + self, + id: str, + *, + name: str, + description: typing.Optional[str] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> None: + """ + Update prompt + + Parameters + ---------- + id : str + + name : str + + description : typing.Optional[str] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + None + + Examples + -------- + import asyncio + + from Opik.client import AsyncOpikApi + + client = AsyncOpikApi() + + + async def main() -> None: + await client.prompts.update_prompt( + id="id", + name="name", + ) + + + asyncio.run(main()) + """ + _response = await self._client_wrapper.httpx_client.request( + f"v1/private/prompts/{jsonable_encoder(id)}", + method="PUT", + json={"name": name, "description": description}, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + return + if _response.status_code == 400: + raise BadRequestError( + pydantic_v1.parse_obj_as(typing.Any, _response.json()) + ) # type: ignore + if _response.status_code == 404: + raise NotFoundError( + pydantic_v1.parse_obj_as(typing.Any, _response.json()) + ) # type: ignore + if _response.status_code == 409: + raise ConflictError( + pydantic_v1.parse_obj_as(typing.Any, _response.json()) + ) # type: ignore + if _response.status_code == 422: + raise UnprocessableEntityError( + pydantic_v1.parse_obj_as(typing.Any, _response.json()) + ) # type: ignore + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, body=_response.text) + raise ApiError(status_code=_response.status_code, body=_response_json) + + async def delete_prompt( + self, id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> None: + """ + Delete prompt + + Parameters + ---------- + id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + None + + Examples + -------- + import asyncio + + from Opik.client import AsyncOpikApi + + client = AsyncOpikApi() + + + async def main() -> None: + await client.prompts.delete_prompt( + id="id", + ) + + + asyncio.run(main()) + """ + _response = await self._client_wrapper.httpx_client.request( + f"v1/private/prompts/{jsonable_encoder(id)}", + method="DELETE", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + return + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, body=_response.text) + raise ApiError(status_code=_response.status_code, body=_response_json) + + async def get_prompt_version_by_id( + self, + version_id: str, + *, + request_options: typing.Optional[RequestOptions] = None, + ) -> PromptVersionDetail: + """ + Get prompt version by id + + Parameters + ---------- + version_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + PromptVersionDetail + Prompt version resource + + Examples + -------- + import asyncio + + from Opik.client import AsyncOpikApi + + client = AsyncOpikApi() + + + async def main() -> None: + await client.prompts.get_prompt_version_by_id( + version_id="versionId", + ) + + + asyncio.run(main()) + """ + _response = await self._client_wrapper.httpx_client.request( + f"v1/private/prompts/versions/{jsonable_encoder(version_id)}", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + return pydantic_v1.parse_obj_as(PromptVersionDetail, _response.json()) # type: ignore + if _response.status_code == 404: + raise NotFoundError( + pydantic_v1.parse_obj_as(typing.Any, _response.json()) + ) # type: ignore + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, body=_response.text) + raise ApiError(status_code=_response.status_code, body=_response_json) + + async def get_prompt_versions( + self, + id: str, + *, + page: typing.Optional[int] = None, + size: typing.Optional[int] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> PromptVersionPagePublic: + """ + Get prompt versions + + Parameters + ---------- + id : str + + page : typing.Optional[int] + + size : typing.Optional[int] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + PromptVersionPagePublic + OK + + Examples + -------- + import asyncio + + from Opik.client import AsyncOpikApi + + client = AsyncOpikApi() + + + async def main() -> None: + await client.prompts.get_prompt_versions( + id="id", + ) + + + asyncio.run(main()) + """ + _response = await self._client_wrapper.httpx_client.request( + f"v1/private/prompts/{jsonable_encoder(id)}/versions", + method="GET", + params={"page": page, "size": size}, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + return pydantic_v1.parse_obj_as( + PromptVersionPagePublic, _response.json() + ) # type: ignore + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, body=_response.text) + raise ApiError(status_code=_response.status_code, body=_response_json) + + async def retrieve_prompt_version( + self, + *, + name: str, + commit: typing.Optional[str] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> PromptVersionDetail: + """ + Retrieve prompt version + + Parameters + ---------- + name : str + + commit : typing.Optional[str] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + PromptVersionDetail + OK + + Examples + -------- + import asyncio + + from Opik.client import AsyncOpikApi + + client = AsyncOpikApi() + + + async def main() -> None: + await client.prompts.retrieve_prompt_version( + name="name", + ) + + + asyncio.run(main()) + """ + _response = await self._client_wrapper.httpx_client.request( + "v1/private/prompts/versions/retrieve", + method="POST", + json={"name": name, "commit": commit}, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + return pydantic_v1.parse_obj_as(PromptVersionDetail, _response.json()) # type: ignore + if _response.status_code == 400: + raise BadRequestError( + pydantic_v1.parse_obj_as(typing.Any, _response.json()) + ) # type: ignore + if _response.status_code == 404: + raise NotFoundError( + pydantic_v1.parse_obj_as(typing.Any, _response.json()) + ) # type: ignore + if _response.status_code == 422: + raise UnprocessableEntityError( + pydantic_v1.parse_obj_as(typing.Any, _response.json()) + ) # type: ignore + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, body=_response.text) + raise ApiError(status_code=_response.status_code, body=_response_json) diff --git a/sdks/python/src/opik/rest_api/types/__init__.py b/sdks/python/src/opik/rest_api/types/__init__.py index b5ce03d0f..62be2d12f 100644 --- a/sdks/python/src/opik/rest_api/types/__init__.py +++ b/sdks/python/src/opik/rest_api/types/__init__.py @@ -30,6 +30,7 @@ from .dataset_public import DatasetPublic from .delete_feedback_score import DeleteFeedbackScore from .error_message import ErrorMessage +from .error_message_detail import ErrorMessageDetail from .error_message_public import ErrorMessagePublic from .experiment import Experiment from .experiment_item import ExperimentItem @@ -85,6 +86,14 @@ from .project import Project from .project_page_public import ProjectPagePublic from .project_public import ProjectPublic +from .prompt import Prompt +from .prompt_detail import PromptDetail +from .prompt_page_public import PromptPagePublic +from .prompt_public import PromptPublic +from .prompt_version import PromptVersion +from .prompt_version_detail import PromptVersionDetail +from .prompt_version_page_public import PromptVersionPagePublic +from .prompt_version_public import PromptVersionPublic from .span import Span from .span_batch import SpanBatch from .span_page_public import SpanPagePublic @@ -132,6 +141,7 @@ "DatasetPublic", "DeleteFeedbackScore", "ErrorMessage", + "ErrorMessageDetail", "ErrorMessagePublic", "Experiment", "ExperimentItem", @@ -181,6 +191,14 @@ "Project", "ProjectPagePublic", "ProjectPublic", + "Prompt", + "PromptDetail", + "PromptPagePublic", + "PromptPublic", + "PromptVersion", + "PromptVersionDetail", + "PromptVersionPagePublic", + "PromptVersionPublic", "Span", "SpanBatch", "SpanPagePublic", diff --git a/sdks/python/src/opik/rest_api/types/dataset.py b/sdks/python/src/opik/rest_api/types/dataset.py index 0fb58b2e7..3dfbfb07d 100644 --- a/sdks/python/src/opik/rest_api/types/dataset.py +++ b/sdks/python/src/opik/rest_api/types/dataset.py @@ -16,7 +16,9 @@ class Dataset(pydantic_v1.BaseModel): last_updated_at: typing.Optional[dt.datetime] = None last_updated_by: typing.Optional[str] = None experiment_count: typing.Optional[int] = None + dataset_items_count: typing.Optional[int] = None most_recent_experiment_at: typing.Optional[dt.datetime] = None + last_created_experiment_at: typing.Optional[dt.datetime] = None def json(self, **kwargs: typing.Any) -> str: kwargs_with_defaults: typing.Any = { diff --git a/sdks/python/src/opik/rest_api/types/dataset_public.py b/sdks/python/src/opik/rest_api/types/dataset_public.py index 700545486..84950279c 100644 --- a/sdks/python/src/opik/rest_api/types/dataset_public.py +++ b/sdks/python/src/opik/rest_api/types/dataset_public.py @@ -16,7 +16,9 @@ class DatasetPublic(pydantic_v1.BaseModel): last_updated_at: typing.Optional[dt.datetime] = None last_updated_by: typing.Optional[str] = None experiment_count: typing.Optional[int] = None + dataset_items_count: typing.Optional[int] = None most_recent_experiment_at: typing.Optional[dt.datetime] = None + last_created_experiment_at: typing.Optional[dt.datetime] = None def json(self, **kwargs: typing.Any) -> str: kwargs_with_defaults: typing.Any = { diff --git a/sdks/python/src/opik/rest_api/types/error_message.py b/sdks/python/src/opik/rest_api/types/error_message.py index 8499dee91..3569837d3 100644 --- a/sdks/python/src/opik/rest_api/types/error_message.py +++ b/sdks/python/src/opik/rest_api/types/error_message.py @@ -8,7 +8,9 @@ class ErrorMessage(pydantic_v1.BaseModel): - errors: typing.Optional[typing.List[str]] = None + code: typing.Optional[int] = None + message: typing.Optional[str] = None + details: typing.Optional[str] = None def json(self, **kwargs: typing.Any) -> str: kwargs_with_defaults: typing.Any = { diff --git a/sdks/python/src/opik/rest_api/types/error_message_detail.py b/sdks/python/src/opik/rest_api/types/error_message_detail.py new file mode 100644 index 000000000..879c0e484 --- /dev/null +++ b/sdks/python/src/opik/rest_api/types/error_message_detail.py @@ -0,0 +1,44 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing + +from ..core.datetime_utils import serialize_datetime +from ..core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 + + +class ErrorMessageDetail(pydantic_v1.BaseModel): + code: typing.Optional[int] = None + message: typing.Optional[str] = None + details: typing.Optional[str] = None + + def json(self, **kwargs: typing.Any) -> str: + kwargs_with_defaults: typing.Any = { + "by_alias": True, + "exclude_unset": True, + **kwargs, + } + return super().json(**kwargs_with_defaults) + + def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: + kwargs_with_defaults_exclude_unset: typing.Any = { + "by_alias": True, + "exclude_unset": True, + **kwargs, + } + kwargs_with_defaults_exclude_none: typing.Any = { + "by_alias": True, + "exclude_none": True, + **kwargs, + } + + return deep_union_pydantic_dicts( + super().dict(**kwargs_with_defaults_exclude_unset), + super().dict(**kwargs_with_defaults_exclude_none), + ) + + class Config: + frozen = True + smart_union = True + extra = pydantic_v1.Extra.allow + json_encoders = {dt.datetime: serialize_datetime} diff --git a/sdks/python/src/opik/rest_api/types/project_page_public.py b/sdks/python/src/opik/rest_api/types/project_page_public.py index aa5b1769b..4572e3190 100644 --- a/sdks/python/src/opik/rest_api/types/project_page_public.py +++ b/sdks/python/src/opik/rest_api/types/project_page_public.py @@ -13,6 +13,9 @@ class ProjectPagePublic(pydantic_v1.BaseModel): size: typing.Optional[int] = None total: typing.Optional[int] = None content: typing.Optional[typing.List[ProjectPublic]] = None + sortable_by: typing.Optional[typing.List[str]] = pydantic_v1.Field( + alias="sortableBy", default=None + ) def json(self, **kwargs: typing.Any) -> str: kwargs_with_defaults: typing.Any = { @@ -42,5 +45,7 @@ def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: class Config: frozen = True smart_union = True + allow_population_by_field_name = True + populate_by_name = True extra = pydantic_v1.Extra.allow json_encoders = {dt.datetime: serialize_datetime} diff --git a/sdks/python/src/opik/rest_api/types/prompt.py b/sdks/python/src/opik/rest_api/types/prompt.py new file mode 100644 index 000000000..ed2d44022 --- /dev/null +++ b/sdks/python/src/opik/rest_api/types/prompt.py @@ -0,0 +1,52 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing + +from ..core.datetime_utils import serialize_datetime +from ..core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 +from .prompt_version import PromptVersion + + +class Prompt(pydantic_v1.BaseModel): + id: typing.Optional[str] = None + name: str + description: typing.Optional[str] = None + template: typing.Optional[str] = None + created_at: typing.Optional[dt.datetime] = None + created_by: typing.Optional[str] = None + last_updated_at: typing.Optional[dt.datetime] = None + last_updated_by: typing.Optional[str] = None + version_count: typing.Optional[int] = None + latest_version: typing.Optional[PromptVersion] = None + + def json(self, **kwargs: typing.Any) -> str: + kwargs_with_defaults: typing.Any = { + "by_alias": True, + "exclude_unset": True, + **kwargs, + } + return super().json(**kwargs_with_defaults) + + def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: + kwargs_with_defaults_exclude_unset: typing.Any = { + "by_alias": True, + "exclude_unset": True, + **kwargs, + } + kwargs_with_defaults_exclude_none: typing.Any = { + "by_alias": True, + "exclude_none": True, + **kwargs, + } + + return deep_union_pydantic_dicts( + super().dict(**kwargs_with_defaults_exclude_unset), + super().dict(**kwargs_with_defaults_exclude_none), + ) + + class Config: + frozen = True + smart_union = True + extra = pydantic_v1.Extra.allow + json_encoders = {dt.datetime: serialize_datetime} diff --git a/sdks/python/src/opik/rest_api/types/prompt_detail.py b/sdks/python/src/opik/rest_api/types/prompt_detail.py new file mode 100644 index 000000000..95d729f0b --- /dev/null +++ b/sdks/python/src/opik/rest_api/types/prompt_detail.py @@ -0,0 +1,51 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing + +from ..core.datetime_utils import serialize_datetime +from ..core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 +from .prompt_version_detail import PromptVersionDetail + + +class PromptDetail(pydantic_v1.BaseModel): + id: typing.Optional[str] = None + name: str + description: typing.Optional[str] = None + created_at: typing.Optional[dt.datetime] = None + created_by: typing.Optional[str] = None + last_updated_at: typing.Optional[dt.datetime] = None + last_updated_by: typing.Optional[str] = None + version_count: typing.Optional[int] = None + latest_version: typing.Optional[PromptVersionDetail] = None + + def json(self, **kwargs: typing.Any) -> str: + kwargs_with_defaults: typing.Any = { + "by_alias": True, + "exclude_unset": True, + **kwargs, + } + return super().json(**kwargs_with_defaults) + + def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: + kwargs_with_defaults_exclude_unset: typing.Any = { + "by_alias": True, + "exclude_unset": True, + **kwargs, + } + kwargs_with_defaults_exclude_none: typing.Any = { + "by_alias": True, + "exclude_none": True, + **kwargs, + } + + return deep_union_pydantic_dicts( + super().dict(**kwargs_with_defaults_exclude_unset), + super().dict(**kwargs_with_defaults_exclude_none), + ) + + class Config: + frozen = True + smart_union = True + extra = pydantic_v1.Extra.allow + json_encoders = {dt.datetime: serialize_datetime} diff --git a/sdks/python/src/opik/rest_api/types/prompt_page_public.py b/sdks/python/src/opik/rest_api/types/prompt_page_public.py new file mode 100644 index 000000000..66c3d8488 --- /dev/null +++ b/sdks/python/src/opik/rest_api/types/prompt_page_public.py @@ -0,0 +1,46 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing + +from ..core.datetime_utils import serialize_datetime +from ..core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 +from .prompt_public import PromptPublic + + +class PromptPagePublic(pydantic_v1.BaseModel): + page: typing.Optional[int] = None + size: typing.Optional[int] = None + total: typing.Optional[int] = None + content: typing.Optional[typing.List[PromptPublic]] = None + + def json(self, **kwargs: typing.Any) -> str: + kwargs_with_defaults: typing.Any = { + "by_alias": True, + "exclude_unset": True, + **kwargs, + } + return super().json(**kwargs_with_defaults) + + def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: + kwargs_with_defaults_exclude_unset: typing.Any = { + "by_alias": True, + "exclude_unset": True, + **kwargs, + } + kwargs_with_defaults_exclude_none: typing.Any = { + "by_alias": True, + "exclude_none": True, + **kwargs, + } + + return deep_union_pydantic_dicts( + super().dict(**kwargs_with_defaults_exclude_unset), + super().dict(**kwargs_with_defaults_exclude_none), + ) + + class Config: + frozen = True + smart_union = True + extra = pydantic_v1.Extra.allow + json_encoders = {dt.datetime: serialize_datetime} diff --git a/sdks/python/src/opik/rest_api/types/prompt_public.py b/sdks/python/src/opik/rest_api/types/prompt_public.py new file mode 100644 index 000000000..45b674ac7 --- /dev/null +++ b/sdks/python/src/opik/rest_api/types/prompt_public.py @@ -0,0 +1,49 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing + +from ..core.datetime_utils import serialize_datetime +from ..core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 + + +class PromptPublic(pydantic_v1.BaseModel): + id: typing.Optional[str] = None + name: str + description: typing.Optional[str] = None + created_at: typing.Optional[dt.datetime] = None + created_by: typing.Optional[str] = None + last_updated_at: typing.Optional[dt.datetime] = None + last_updated_by: typing.Optional[str] = None + version_count: typing.Optional[int] = None + + def json(self, **kwargs: typing.Any) -> str: + kwargs_with_defaults: typing.Any = { + "by_alias": True, + "exclude_unset": True, + **kwargs, + } + return super().json(**kwargs_with_defaults) + + def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: + kwargs_with_defaults_exclude_unset: typing.Any = { + "by_alias": True, + "exclude_unset": True, + **kwargs, + } + kwargs_with_defaults_exclude_none: typing.Any = { + "by_alias": True, + "exclude_none": True, + **kwargs, + } + + return deep_union_pydantic_dicts( + super().dict(**kwargs_with_defaults_exclude_unset), + super().dict(**kwargs_with_defaults_exclude_none), + ) + + class Config: + frozen = True + smart_union = True + extra = pydantic_v1.Extra.allow + json_encoders = {dt.datetime: serialize_datetime} diff --git a/sdks/python/src/opik/rest_api/types/prompt_version.py b/sdks/python/src/opik/rest_api/types/prompt_version.py new file mode 100644 index 000000000..a505b075d --- /dev/null +++ b/sdks/python/src/opik/rest_api/types/prompt_version.py @@ -0,0 +1,56 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing + +from ..core.datetime_utils import serialize_datetime +from ..core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 + + +class PromptVersion(pydantic_v1.BaseModel): + id: typing.Optional[str] = pydantic_v1.Field(default=None) + """ + version unique identifier, generated if absent + """ + + prompt_id: typing.Optional[str] = None + commit: typing.Optional[str] = pydantic_v1.Field(default=None) + """ + version short unique identifier, generated if absent. it must be 8 characters long + """ + + template: str + variables: typing.Optional[typing.List[str]] = None + created_at: typing.Optional[dt.datetime] = None + created_by: typing.Optional[str] = None + + def json(self, **kwargs: typing.Any) -> str: + kwargs_with_defaults: typing.Any = { + "by_alias": True, + "exclude_unset": True, + **kwargs, + } + return super().json(**kwargs_with_defaults) + + def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: + kwargs_with_defaults_exclude_unset: typing.Any = { + "by_alias": True, + "exclude_unset": True, + **kwargs, + } + kwargs_with_defaults_exclude_none: typing.Any = { + "by_alias": True, + "exclude_none": True, + **kwargs, + } + + return deep_union_pydantic_dicts( + super().dict(**kwargs_with_defaults_exclude_unset), + super().dict(**kwargs_with_defaults_exclude_none), + ) + + class Config: + frozen = True + smart_union = True + extra = pydantic_v1.Extra.allow + json_encoders = {dt.datetime: serialize_datetime} diff --git a/sdks/python/src/opik/rest_api/types/prompt_version_detail.py b/sdks/python/src/opik/rest_api/types/prompt_version_detail.py new file mode 100644 index 000000000..4e9c79490 --- /dev/null +++ b/sdks/python/src/opik/rest_api/types/prompt_version_detail.py @@ -0,0 +1,56 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing + +from ..core.datetime_utils import serialize_datetime +from ..core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 + + +class PromptVersionDetail(pydantic_v1.BaseModel): + id: typing.Optional[str] = pydantic_v1.Field(default=None) + """ + version unique identifier, generated if absent + """ + + prompt_id: typing.Optional[str] = None + commit: typing.Optional[str] = pydantic_v1.Field(default=None) + """ + version short unique identifier, generated if absent. it must be 8 characters long + """ + + template: str + variables: typing.Optional[typing.List[str]] = None + created_at: typing.Optional[dt.datetime] = None + created_by: typing.Optional[str] = None + + def json(self, **kwargs: typing.Any) -> str: + kwargs_with_defaults: typing.Any = { + "by_alias": True, + "exclude_unset": True, + **kwargs, + } + return super().json(**kwargs_with_defaults) + + def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: + kwargs_with_defaults_exclude_unset: typing.Any = { + "by_alias": True, + "exclude_unset": True, + **kwargs, + } + kwargs_with_defaults_exclude_none: typing.Any = { + "by_alias": True, + "exclude_none": True, + **kwargs, + } + + return deep_union_pydantic_dicts( + super().dict(**kwargs_with_defaults_exclude_unset), + super().dict(**kwargs_with_defaults_exclude_none), + ) + + class Config: + frozen = True + smart_union = True + extra = pydantic_v1.Extra.allow + json_encoders = {dt.datetime: serialize_datetime} diff --git a/sdks/python/src/opik/rest_api/types/prompt_version_page_public.py b/sdks/python/src/opik/rest_api/types/prompt_version_page_public.py new file mode 100644 index 000000000..c33d83611 --- /dev/null +++ b/sdks/python/src/opik/rest_api/types/prompt_version_page_public.py @@ -0,0 +1,46 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing + +from ..core.datetime_utils import serialize_datetime +from ..core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 +from .prompt_version_public import PromptVersionPublic + + +class PromptVersionPagePublic(pydantic_v1.BaseModel): + page: typing.Optional[int] = None + size: typing.Optional[int] = None + total: typing.Optional[int] = None + content: typing.Optional[typing.List[PromptVersionPublic]] = None + + def json(self, **kwargs: typing.Any) -> str: + kwargs_with_defaults: typing.Any = { + "by_alias": True, + "exclude_unset": True, + **kwargs, + } + return super().json(**kwargs_with_defaults) + + def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: + kwargs_with_defaults_exclude_unset: typing.Any = { + "by_alias": True, + "exclude_unset": True, + **kwargs, + } + kwargs_with_defaults_exclude_none: typing.Any = { + "by_alias": True, + "exclude_none": True, + **kwargs, + } + + return deep_union_pydantic_dicts( + super().dict(**kwargs_with_defaults_exclude_unset), + super().dict(**kwargs_with_defaults_exclude_none), + ) + + class Config: + frozen = True + smart_union = True + extra = pydantic_v1.Extra.allow + json_encoders = {dt.datetime: serialize_datetime} diff --git a/sdks/python/src/opik/rest_api/types/prompt_version_public.py b/sdks/python/src/opik/rest_api/types/prompt_version_public.py new file mode 100644 index 000000000..8b2c36d18 --- /dev/null +++ b/sdks/python/src/opik/rest_api/types/prompt_version_public.py @@ -0,0 +1,55 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing + +from ..core.datetime_utils import serialize_datetime +from ..core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 + + +class PromptVersionPublic(pydantic_v1.BaseModel): + id: typing.Optional[str] = pydantic_v1.Field(default=None) + """ + version unique identifier, generated if absent + """ + + prompt_id: typing.Optional[str] = None + commit: typing.Optional[str] = pydantic_v1.Field(default=None) + """ + version short unique identifier, generated if absent. it must be 8 characters long + """ + + template: str + created_at: typing.Optional[dt.datetime] = None + created_by: typing.Optional[str] = None + + def json(self, **kwargs: typing.Any) -> str: + kwargs_with_defaults: typing.Any = { + "by_alias": True, + "exclude_unset": True, + **kwargs, + } + return super().json(**kwargs_with_defaults) + + def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: + kwargs_with_defaults_exclude_unset: typing.Any = { + "by_alias": True, + "exclude_unset": True, + **kwargs, + } + kwargs_with_defaults_exclude_none: typing.Any = { + "by_alias": True, + "exclude_none": True, + **kwargs, + } + + return deep_union_pydantic_dicts( + super().dict(**kwargs_with_defaults_exclude_unset), + super().dict(**kwargs_with_defaults_exclude_none), + ) + + class Config: + frozen = True + smart_union = True + extra = pydantic_v1.Extra.allow + json_encoders = {dt.datetime: serialize_datetime} diff --git a/sdks/python/tests/e2e/test_prompt.py b/sdks/python/tests/e2e/test_prompt.py new file mode 100644 index 000000000..9ae3ad867 --- /dev/null +++ b/sdks/python/tests/e2e/test_prompt.py @@ -0,0 +1,154 @@ +import uuid + +from opik import Prompt + + +def test_prompt__create__happyflow(opik_client): + unique_identifier = str(uuid.uuid4())[-6:] + + prompt_name = f"some-prompt-name-{unique_identifier}" + prompt_template = f"some-prompt-text-{unique_identifier}" + + prompt = opik_client.create_prompt( + name=prompt_name, + prompt=prompt_template, + ) + + assert prompt.name == prompt_name + assert prompt.prompt == prompt_template + assert prompt.__internal_api__prompt_id__ is not None + assert prompt.commit is not None + + +def test_prompt__create_new_version__happyflow(opik_client): + unique_identifier = str(uuid.uuid4())[-6:] + + prompt_name = f"some-prompt-name-{unique_identifier}" + prompt_template = f"some-prompt-text-{unique_identifier}" + + # create initial version + prompt = opik_client.create_prompt( + name=prompt_name, + prompt=prompt_template, + ) + + unique_identifier_new = str(uuid.uuid4())[-6:] + prompt_template_new = f"some-prompt-text-{unique_identifier_new}" + + # must create new version + new_prompt = opik_client.create_prompt( + name=prompt_name, + prompt=prompt_template_new, + ) + + assert new_prompt.name == prompt.name + assert new_prompt.prompt == prompt_template_new + assert new_prompt.__internal_api__prompt_id__ != prompt.__internal_api__prompt_id__ + assert new_prompt.commit != prompt.commit + + +def test_prompt__do_not_create_new_version_with_the_same_template(opik_client): + unique_identifier = str(uuid.uuid4())[-6:] + + prompt_name = f"some-prompt-name-{unique_identifier}" + prompt_template = f"some-prompt-text-{unique_identifier}" + + # create initial version + prompt = opik_client.create_prompt( + name=prompt_name, + prompt=prompt_template, + ) + + # must NOT create new version + new_prompt = opik_client.create_prompt( + name=prompt_name, + prompt=prompt_template, + ) + + assert new_prompt.name == prompt.name + assert new_prompt.prompt == prompt.prompt + assert new_prompt.__internal_api__prompt_id__ == prompt.__internal_api__prompt_id__ + assert new_prompt.commit == prompt.commit + + +def test_prompt__get__happyflow(opik_client): + unique_identifier = str(uuid.uuid4())[-6:] + + prompt_name = f"some-prompt-name-{unique_identifier}" + prompt_template = f"some-prompt-text-{unique_identifier}" + + prompt = opik_client.create_prompt( + name=prompt_name, + prompt=prompt_template, + ) + + unique_identifier_new = str(uuid.uuid4())[-6:] + prompt_template_new = f"some-prompt-text-{unique_identifier_new}" + + # must create new version + new_prompt = opik_client.create_prompt( + name=prompt_name, + prompt=prompt_template_new, + ) + + # ASSERTIONS + p1 = opik_client.get_prompt(name=prompt.name) + + assert p1.name == new_prompt.name + assert p1.prompt == new_prompt.prompt + assert p1.__internal_api__prompt_id__ == new_prompt.__internal_api__prompt_id__ + assert p1.commit == new_prompt.commit + + p2 = opik_client.get_prompt(name=prompt.name, commit=prompt.commit) + + assert p2.name == prompt.name + assert p2.prompt == prompt.prompt + assert p2.__internal_api__prompt_id__ == prompt.__internal_api__prompt_id__ + assert p2.commit == prompt.commit + + +def test_prompt__get__not_exists(opik_client): + unique_identifier = str(uuid.uuid4())[-6:] + + prompt_name = f"some-prompt-name-{unique_identifier}" + + prompt = opik_client.get_prompt(prompt_name) + + assert prompt is None + + +def test_prompt__initialize_class_instance(opik_client): + unique_identifier = str(uuid.uuid4())[-6:] + template = "Hello, {name} from {place}! Nice to meet you, {name}." + + prompt = Prompt(name=f"test-{unique_identifier}", prompt=template) + prompt_from_api = opik_client.get_prompt(name=prompt.name) + + assert prompt.name == prompt_from_api.name + assert prompt.prompt == prompt_from_api.prompt + assert ( + prompt.__internal_api__prompt_id__ + == prompt_from_api.__internal_api__prompt_id__ + ) + assert prompt.commit == prompt_from_api.commit + + +def test_prompt__format(opik_client): + unique_identifier = str(uuid.uuid4())[-6:] + template = "Hello, {{name}} from {{place}}! Nice to meet you, {{name}}." + + prompt = Prompt(name=f"test-{unique_identifier}", prompt=template) + + result = prompt.format() + assert result == template + + result = prompt.format(name="John") + assert result == "Hello, John from {{place}}! Nice to meet you, John." + + result = prompt.format(name="John", place="The Earth") + assert result == "Hello, John from The Earth! Nice to meet you, John." + + result = prompt.format(name="John", place="The Earth", unexisting_key="value") + assert result == "Hello, John from The Earth! Nice to meet you, John." + + assert prompt.prompt == template