From bfbefb3794f46315ca37e75936d679b22c4c190e Mon Sep 17 00:00:00 2001 From: "jiangqi.rrt" Date: Fri, 29 Aug 2025 17:04:08 +0800 Subject: [PATCH 1/2] support pydantic v1 and v2 --- .../integration/langchain/trace_callback.py | 8 +- cozeloop/internal/httpclient/auth_client.py | 9 ++- cozeloop/internal/httpclient/client.py | 6 +- cozeloop/internal/httpclient/http_client.py | 10 ++- cozeloop/internal/prompt/openapi.py | 7 +- cozeloop/internal/prompt/prompt.py | 23 +++++- cozeloop/internal/trace/exporter.py | 80 +++++++++++-------- cozeloop/internal/trace/model/model.py | 12 +-- cozeloop/internal/trace/span.py | 8 +- cozeloop/internal/utils/convert.py | 7 +- pyproject.toml | 4 +- 11 files changed, 122 insertions(+), 52 deletions(-) diff --git a/cozeloop/integration/langchain/trace_callback.py b/cozeloop/integration/langchain/trace_callback.py index 4cec791..02ab5e5 100644 --- a/cozeloop/integration/langchain/trace_callback.py +++ b/cozeloop/integration/langchain/trace_callback.py @@ -7,6 +7,7 @@ import traceback from typing import List, Dict, Union, Any, Optional +import pydantic from pydantic import Field, BaseModel from langchain.callbacks.base import BaseCallbackHandler from langchain.schema import AgentFinish, AgentAction, LLMResult @@ -445,7 +446,10 @@ def _convert_inputs(inputs: Any) -> Any: if isinstance(inputs, PromptValue): return _convert_inputs(inputs.to_messages()) if isinstance(inputs, BaseModel): - return inputs.model_dump_json() + if pydantic.VERSION.startswith('1'): + return inputs.json() + else: + return inputs.model_dump_json() if inputs is None: return 'None' - return 'type of inputs is not supported' + return str(inputs) diff --git a/cozeloop/internal/httpclient/auth_client.py b/cozeloop/internal/httpclient/auth_client.py index 0d11eaa..c02bc84 100644 --- a/cozeloop/internal/httpclient/auth_client.py +++ b/cozeloop/internal/httpclient/auth_client.py @@ -7,6 +7,7 @@ from urllib.parse import urlparse, quote_plus import httpx +import pydantic from authlib.jose import jwt from pydantic import BaseModel @@ -199,9 +200,15 @@ def get_access_token( jwt_token = self._gen_jwt(self._public_key_id, self._private_key, 3600, session_name) url = f"{self._base_url}/api/permission/oauth2/token" headers = {"Authorization": f"Bearer {jwt_token}"} + scope_str = None + if scope: + if pydantic.VERSION.startswith('1'): + scope_str = scope.dict() + else: + scope_str = scope.model_dump() body = { "duration_seconds": ttl, "grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer", - "scope": scope.model_dump() if scope else None, + "scope": scope_str, } return self._do_request(url, "POST", OAuthToken, headers=headers, json=body) diff --git a/cozeloop/internal/httpclient/client.py b/cozeloop/internal/httpclient/client.py index 003693f..8ee77af 100644 --- a/cozeloop/internal/httpclient/client.py +++ b/cozeloop/internal/httpclient/client.py @@ -6,6 +6,7 @@ from typing import Optional, Dict, Union, IO, Type, Tuple import httpx +import pydantic from pydantic import BaseModel from cozeloop.internal import consts @@ -71,7 +72,10 @@ def request( _timeout = timeout if timeout is not None else self.timeout if isinstance(json, BaseModel): - json = json.model_dump(by_alias=True) + if pydantic.VERSION.startswith('1'): + json = json.dict(by_alias=True) + else: + json = json.model_dump(by_alias=True) try: response = self.http_client.request( diff --git a/cozeloop/internal/httpclient/http_client.py b/cozeloop/internal/httpclient/http_client.py index 160e1b5..fcb25b0 100644 --- a/cozeloop/internal/httpclient/http_client.py +++ b/cozeloop/internal/httpclient/http_client.py @@ -5,6 +5,7 @@ from typing import Dict, Type, TypeVar import httpx +import pydantic from pydantic import ValidationError from cozeloop.internal import consts @@ -50,7 +51,14 @@ def parse_response(url: str, response: httpx.Response, response_model: Type[T]) raise e try: - res = response_model.model_validate(data) if data is not None else response_model() + res = None + if data is not None: + if pydantic.VERSION.startswith('1'): + res = response_model.parse_obj(data) + else: + res = response_model.model_validate(data) + else: + res = response_model() except ValidationError as e: logger.error(f"Failed to parse response. Path: {url}, http code: {http_code}, log id: {log_id}, error: {e}.") raise consts.InternalError from e diff --git a/cozeloop/internal/prompt/openapi.py b/cozeloop/internal/prompt/openapi.py index 7335d4b..841525e 100644 --- a/cozeloop/internal/prompt/openapi.py +++ b/cozeloop/internal/prompt/openapi.py @@ -4,6 +4,7 @@ from enum import Enum from typing import List, Optional +import pydantic from pydantic import BaseModel from cozeloop.internal.httpclient import Client, BaseResponse @@ -152,6 +153,10 @@ def _do_mpull_prompt(self, workspace_id: str, queries: List[PromptQuery]) -> Opt return None request = MPullPromptRequest(workspace_id=workspace_id, queries=queries) response = self.http_client.post(MPULL_PROMPT_PATH, MPullPromptResponse, request) - real_resp = MPullPromptResponse.model_validate(response) + real_resp = None + if pydantic.VERSION.startswith('1'): + real_resp = MPullPromptResponse.parse_obj(response) + else: + real_resp = MPullPromptResponse.model_validate(response) if real_resp.data is not None: return real_resp.data.items diff --git a/cozeloop/internal/prompt/prompt.py b/cozeloop/internal/prompt/prompt.py index fd04232..e5d3af2 100644 --- a/cozeloop/internal/prompt/prompt.py +++ b/cozeloop/internal/prompt/prompt.py @@ -4,6 +4,7 @@ import json from typing import Dict, Any, List, Optional +import pydantic from jinja2 import Environment, BaseLoader, Undefined from jinja2.utils import missing, object_type_repr from jinja2.sandbox import SandboxedEnvironment @@ -52,9 +53,15 @@ def get_prompt(self, prompt_key: str, version: str = '') -> Optional[Prompt]: try: prompt = self._get_prompt(prompt_key, version) if prompt is not None: + + output = None + if pydantic.VERSION.startswith('1'): + output = prompt.json() + else: + output = prompt.model_dump_json(exclude_none=True) prompt_hub_pan.set_tags({ PROMPT_VERSION: prompt.version, - consts.OUTPUT: prompt.model_dump_json(exclude_none=True), + consts.OUTPUT: output, }) return prompt except RemoteServiceError as e: @@ -91,15 +98,25 @@ def prompt_format( with self.trace_provider.start_span(consts.TRACE_PROMPT_TEMPLATE_SPAN_NAME, consts.TRACE_PROMPT_TEMPLATE_SPAN_TYPE, scene=V_SCENE_PROMPT_TEMPLATE) as prompt_template_span: + input = None + if pydantic.VERSION.startswith('1'): + input = _to_span_prompt_input(prompt.prompt_template.messages, variables).json() + else: + input = _to_span_prompt_input(prompt.prompt_template.messages, variables).model_dump_json(exclude_none=True) prompt_template_span.set_tags({ PROMPT_KEY: prompt.prompt_key, PROMPT_VERSION: prompt.version, - consts.INPUT: _to_span_prompt_input(prompt.prompt_template.messages, variables).model_dump_json(exclude_none=True) + consts.INPUT: input }) try: results = self._prompt_format(prompt, variables) + output = None + if pydantic.VERSION.startswith('1'): + output = _to_span_prompt_output(results).json() + else: + output = _to_span_prompt_output(results).model_dump_json(exclude_none=True) prompt_template_span.set_tags({ - consts.OUTPUT: _to_span_prompt_output(results).model_dump_json(exclude_none=True), + consts.OUTPUT: output, }) return results except RemoteServiceError as e: diff --git a/cozeloop/internal/trace/exporter.py b/cozeloop/internal/trace/exporter.py index 7c92c3a..16143e7 100644 --- a/cozeloop/internal/trace/exporter.py +++ b/cozeloop/internal/trace/exporter.py @@ -6,6 +6,8 @@ import time from typing import Dict, List, Optional, Tuple, Callable, Any +import pydantic + from cozeloop.spec.tracespec import ModelInput, ModelMessagePart, ModelMessagePartType, ModelImageURL, ModelFileURL, ModelOutput from cozeloop.internal.consts import * from cozeloop.internal.httpclient import Client, BaseResponse @@ -17,6 +19,35 @@ logger = logging.getLogger(__name__) + +class UploadSpan(BaseModel): + started_at_micros: int + log_id: str + span_id: str + parent_id: str + trace_id: str + duration_micros: int + service_name: str + workspace_id: str + span_name: str + span_type: str + status_code: int + input: str + output: str + object_storage: str + system_tags_string: Dict[str, str] + system_tags_long: Dict[str, int] + system_tags_double: Dict[str, float] + tags_string: Dict[str, str] + tags_long: Dict[str, int] + tags_double: Dict[str, float] + tags_bool: Dict[str, bool] + + +class UploadSpanData(BaseModel): + spans: List['UploadSpan'] + + class Exporter: def export_spans(self, ctx: dict, spans: List['UploadSpan']): raise NotImplementedError @@ -92,34 +123,6 @@ def export_spans(self, ctx: dict, spans: List['UploadSpan']): raise Exception(f"export spans fail, err:[{e}]") -class UploadSpanData(BaseModel): - spans: List['UploadSpan'] - - -class UploadSpan(BaseModel): - started_at_micros: int - log_id: str - span_id: str - parent_id: str - trace_id: str - duration_micros: int - service_name: str - workspace_id: str - span_name: str - span_type: str - status_code: int - input: str - output: str - object_storage: str - system_tags_string: Dict[str, str] - system_tags_long: Dict[str, int] - system_tags_double: Dict[str, float] - tags_string: Dict[str, str] - tags_long: Dict[str, int] - tags_double: Dict[str, float] - tags_bool: Dict[str, bool] - - class UploadFile(BaseModel): class Config: arbitrary_types_allowed = True @@ -216,7 +219,10 @@ def convert_input(span_key: str, span: Span) -> (str, List[UploadFile]): model_input = ModelInput() if isinstance(value, str): try: - model_input = ModelInput.model_validate_json(value) + if pydantic.VERSION.startswith('1'): + model_input = ModelInput.parse_raw(value) + else: + model_input = ModelInput.model_validate_json(value) except Exception as e: logger.error(f"unmarshal ModelInput failed, err: {e}") return "", [] @@ -226,7 +232,10 @@ def convert_input(span_key: str, span: Span) -> (str, List[UploadFile]): files = transfer_message_part(part, span, span_key) upload_files.extend(files) - value_res = model_input.model_dump_json() + if pydantic.VERSION.startswith('1'): + value_res = model_input.json() + else: + value_res = model_input.model_dump_json() if len(value_res) > MAX_BYTES_OF_ONE_TAG_VALUE_OF_INPUT_OUTPUT: value_res, f = transfer_text(value_res, span, span_key) @@ -251,7 +260,10 @@ def convert_output(span_key: str, span: Span) -> (str, List[UploadFile]): model_output = ModelOutput() if isinstance(value, str): try: - model_output = ModelOutput.model_validate_json(value) + if pydantic.VERSION.startswith('1'): + model_output = ModelOutput.parse_raw(value) + else: + model_output = ModelOutput.model_validate_json(value) except Exception as e: logger.error(f"unmarshal ModelOutput failed, err: {e}") return "", [] @@ -330,8 +342,10 @@ def transfer_object_storage(span_upload_files: List[UploadFile]) -> str: if not is_exist: return "" - - return object_storage.model_dump_json() + if pydantic.VERSION.startswith('1'): + return object_storage.json() + else: + return object_storage.model_dump_json() def transfer_message_part(src: ModelMessagePart, span: 'Span', tag_key: str) -> List[UploadFile]: diff --git a/cozeloop/internal/trace/model/model.py b/cozeloop/internal/trace/model/model.py index 3497d96..ddc9210 100644 --- a/cozeloop/internal/trace/model/model.py +++ b/cozeloop/internal/trace/model/model.py @@ -8,12 +8,6 @@ from pydantic.dataclasses import dataclass -class ObjectStorage(BaseModel): - input_tos_key: Optional[str] = None # The key for reporting long input data - output_tos_key: Optional[str] = None # The key for reporting long output data - attachments: List['Attachment'] = None # attachments in input or output - - class Attachment(BaseModel): field: Optional[str] = None name: Optional[str] = None @@ -21,6 +15,12 @@ class Attachment(BaseModel): tos_key: Optional[str] = None +class ObjectStorage(BaseModel): + input_tos_key: Optional[str] = None # The key for reporting long input data + output_tos_key: Optional[str] = None # The key for reporting long output data + attachments: List['Attachment'] = None # attachments in input or output + + class UploadType(str, Enum): LONG = 1 MULTI_MODALITY = 2 diff --git a/cozeloop/internal/trace/span.py b/cozeloop/internal/trace/span.py index 6189a5a..e479750 100644 --- a/cozeloop/internal/trace/span.py +++ b/cozeloop/internal/trace/span.py @@ -9,6 +9,8 @@ import json import urllib.parse +import pydantic + from cozeloop import span from cozeloop.internal.trace.model.model import TagTruncateConf from cozeloop.spec.tracespec import (ModelInput, ModelOutput, ModelMessagePartType, ModelMessage, ModelMessagePart, @@ -218,7 +220,11 @@ def get_model_input_bytes_size(self, m_content): part.file_url.url = "" try: - m_content_json = m_content.model_dump_json() + m_content_json = "" + if pydantic.VERSION.startswith('1'): + m_content_json = m_content.json() + else: + m_content_json = m_content.model_dump_json() return len(m_content_json) except Exception as e: logger.error(f"Failed to get model input size, m_content model_dump_json err: {e}") diff --git a/cozeloop/internal/utils/convert.py b/cozeloop/internal/utils/convert.py index 7495112..a469d3e 100644 --- a/cozeloop/internal/utils/convert.py +++ b/cozeloop/internal/utils/convert.py @@ -6,6 +6,8 @@ import string from typing import Any, Dict, List, Optional, TypeVar, Sequence from functools import singledispatch + +import pydantic from pydantic import BaseModel T = TypeVar('T') @@ -75,7 +77,10 @@ def to_json(param: Any) -> str: return param try: if isinstance(param, BaseModel): - return param.model_dump_json() + if pydantic.VERSION.startswith('1'): + return param.json() + else: + return param.model_dump_json() return json.dumps(param, ensure_ascii=False) except json.JSONDecodeError: return param.__str__() diff --git a/pyproject.toml b/pyproject.toml index e5ea77d..d11b675 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "cozeloop" -version = "0.1.11" +version = "0.1.12b2" description = "coze loop sdk" authors = ["JiangQi715 "] license = "MIT" @@ -9,7 +9,7 @@ readme = "README.md" [tool.poetry.dependencies] python = ">=3.8,<4.0" httpx = ">=0.23.0,<1.0.0" -pydantic = ">=2.10.6,<3.0.0" +pydantic = ">=1.10.12,<3.0.0" cachetools = "^5.5.2" apscheduler = "^3.11.0" jinja2 = "^3.1.6" From 38fc510f92e139357abef6c95ad3c362591fbefc Mon Sep 17 00:00:00 2001 From: "jiangqi.rrt" Date: Tue, 2 Sep 2025 19:24:12 +0800 Subject: [PATCH 2/2] version 0.1.12 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index d11b675..a5c8ed9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "cozeloop" -version = "0.1.12b2" +version = "0.1.12" description = "coze loop sdk" authors = ["JiangQi715 "] license = "MIT"