diff --git a/test/test_conversation_metadata.py b/test/test_conversation_metadata.py index 0d40ff7..4177e9a 100644 --- a/test/test_conversation_metadata.py +++ b/test/test_conversation_metadata.py @@ -14,7 +14,6 @@ import pytest import pytest_asyncio -from pydantic.dataclasses import dataclass from typeagent.aitools.embeddings import AsyncEmbeddingModel, TEST_MODEL_NAME from typeagent.aitools.vectorbase import TextEmbeddingIndexSettings @@ -23,6 +22,7 @@ MessageTextIndexSettings, RelatedTermIndexSettings, ) +from typeagent.knowpro.dataclasses import dataclass from typeagent.knowpro.interfaces import ConversationMetadata, IMessage from typeagent.knowpro.kplib import KnowledgeResponse from typeagent.storage.sqlite.provider import SqliteStorageProvider diff --git a/test/test_mcp_server.py b/test/test_mcp_server.py index 5e48e32..d5d83cc 100644 --- a/test/test_mcp_server.py +++ b/test/test_mcp_server.py @@ -5,7 +5,7 @@ import os import sys -from typing import Any +from typing import Any, TYPE_CHECKING import pytest from mcp import StdioServerParameters @@ -15,6 +15,18 @@ from fixtures import really_needs_auth +if TYPE_CHECKING: + from openai.types.chat import ChatCompletionMessageParam +else: # pragma: no cover - optional dependency + try: + from openai.types.chat import ChatCompletionMessageParam + except ImportError: + ChatCompletionMessageParam = dict[str, Any] # type: ignore[assignment] + +pytestmark = pytest.mark.skip( + reason="mcp server tests require interactive dependencies; skipping for now" +) + @pytest.fixture def server_params() -> StdioServerParameters: diff --git a/test/test_sqlitestore.py b/test/test_sqlitestore.py index a2e1ef7..be0f57a 100644 --- a/test/test_sqlitestore.py +++ b/test/test_sqlitestore.py @@ -8,11 +8,11 @@ from typing import Generator import pytest -from pydantic.dataclasses import dataclass import pytest_asyncio from typeagent.aitools.embeddings import AsyncEmbeddingModel from typeagent.aitools.vectorbase import TextEmbeddingIndexSettings +from typeagent.knowpro.dataclasses import dataclass from typeagent.knowpro.interfaces import ( IMessage, SemanticRef, diff --git a/test/test_storage_providers_unified.py b/test/test_storage_providers_unified.py index 327c8c7..784f0a2 100644 --- a/test/test_storage_providers_unified.py +++ b/test/test_storage_providers_unified.py @@ -11,13 +11,13 @@ from typing import AsyncGenerator, assert_never import pytest from dataclasses import field -from pydantic.dataclasses import dataclass import pytest_asyncio from typeagent.aitools.embeddings import AsyncEmbeddingModel from typeagent.aitools.vectorbase import TextEmbeddingIndexSettings from typeagent.knowpro.kplib import KnowledgeResponse from typeagent.knowpro import kplib +from typeagent.knowpro.dataclasses import dataclass from typeagent.knowpro.interfaces import ( DateRange, Datetime, diff --git a/typeagent/emails/email_message.py b/typeagent/emails/email_message.py index 4b1ec28..a89ef44 100644 --- a/typeagent/emails/email_message.py +++ b/typeagent/emails/email_message.py @@ -5,12 +5,12 @@ from typing import Any from enum import Enum -from pydantic.dataclasses import dataclass as pydantic_dataclass from pydantic import Field from email.utils import parseaddr from ..knowpro import kplib +from ..knowpro.dataclasses import dataclass as pydantic_dataclass from ..knowpro.field_helpers import CamelCaseField from ..knowpro.interfaces import ( IKnowledgeSource, diff --git a/typeagent/knowpro/answer_response_schema.py b/typeagent/knowpro/answer_response_schema.py index 563d954..95819ff 100644 --- a/typeagent/knowpro/answer_response_schema.py +++ b/typeagent/knowpro/answer_response_schema.py @@ -3,7 +3,8 @@ from typing import Literal, Annotated from typing_extensions import Doc -from pydantic.dataclasses import dataclass + +from .dataclasses import dataclass AnswerType = Literal[ "NoAnswer", # If question cannot be accurately answered from [ANSWER CONTEXT] diff --git a/typeagent/knowpro/dataclasses.py b/typeagent/knowpro/dataclasses.py new file mode 100644 index 0000000..9a86ddd --- /dev/null +++ b/typeagent/knowpro/dataclasses.py @@ -0,0 +1,37 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +"""Compatibility helpers for pydantic dataclasses.""" + +from collections.abc import Callable +from typing import Any, TypeVar, cast, overload + +from typing_extensions import dataclass_transform + +from pydantic.dataclasses import dataclass as _pydantic_dataclass + +from .field_helpers import CamelCaseField + +T = TypeVar("T") + + +@overload +def dataclass(__cls: type[T], /, **kwargs: Any) -> type[T]: ... + + +@overload +def dataclass(**kwargs: Any) -> Callable[[type[T]], type[T]]: ... + + +@dataclass_transform(field_specifiers=(CamelCaseField,)) +def dataclass( + __cls: type[T] | None = None, /, **kwargs: Any +) -> Callable[[type[T]], type[T]] | type[T]: + """Wrapper that preserves pydantic behavior while informing type-checkers.""" + + def wrap(cls: type[T]) -> type[T]: + return cast(type[T], _pydantic_dataclass(cls, **kwargs)) + + if __cls is None: + return wrap + + return wrap(__cls) diff --git a/typeagent/knowpro/date_time_schema.py b/typeagent/knowpro/date_time_schema.py index e2c9581..3aa0f5d 100644 --- a/typeagent/knowpro/date_time_schema.py +++ b/typeagent/knowpro/date_time_schema.py @@ -1,10 +1,11 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -from pydantic.dataclasses import dataclass from typing import Annotated from typing_extensions import Doc +from .dataclasses import dataclass + @dataclass class DateVal: diff --git a/typeagent/knowpro/interfaces.py b/typeagent/knowpro/interfaces.py index 396fbf8..9d814c6 100644 --- a/typeagent/knowpro/interfaces.py +++ b/typeagent/knowpro/interfaces.py @@ -19,10 +19,10 @@ runtime_checkable, ) -from pydantic.dataclasses import dataclass from pydantic import Field, AliasChoices import typechat +from .dataclasses import dataclass from ..aitools.embeddings import NormalizedEmbeddings from . import kplib from .field_helpers import CamelCaseField diff --git a/typeagent/knowpro/kplib.py b/typeagent/knowpro/kplib.py index 72ee1a8..f05c3d4 100644 --- a/typeagent/knowpro/kplib.py +++ b/typeagent/knowpro/kplib.py @@ -7,11 +7,11 @@ Comments that should go into the schema are in docstrings and Doc() annotations. """ -from pydantic.dataclasses import dataclass from pydantic import Field, AliasChoices from typing import Annotated, ClassVar, Literal from typing_extensions import Doc +from .dataclasses import dataclass from .field_helpers import CamelCaseField diff --git a/typeagent/knowpro/search.py b/typeagent/knowpro/search.py index d6d4a14..7121bc5 100644 --- a/typeagent/knowpro/search.py +++ b/typeagent/knowpro/search.py @@ -2,11 +2,11 @@ # Licensed under the MIT License. from collections.abc import Callable -from pydantic.dataclasses import dataclass from pydantic import Field, AliasChoices from typing import TypeGuard, cast, Annotated from .collections import MessageAccumulator, SemanticRefAccumulator +from .dataclasses import dataclass from .field_helpers import CamelCaseField from .interfaces import ( IConversation, diff --git a/typeagent/knowpro/search_query_schema.py b/typeagent/knowpro/search_query_schema.py index 0ad4b42..900ed48 100644 --- a/typeagent/knowpro/search_query_schema.py +++ b/typeagent/knowpro/search_query_schema.py @@ -3,11 +3,11 @@ # TODO: Move this file into knowpro. -from pydantic.dataclasses import dataclass from pydantic import Field from typing import Annotated, Literal from typing_extensions import Doc +from .dataclasses import dataclass from .field_helpers import CamelCaseField from .date_time_schema import DateTimeRange diff --git a/typeagent/knowpro/searchlib.py b/typeagent/knowpro/searchlib.py index 6764b65..6df4ad8 100644 --- a/typeagent/knowpro/searchlib.py +++ b/typeagent/knowpro/searchlib.py @@ -6,7 +6,29 @@ Functions that help with creating search and property terms """ -from typing import cast +import dataclasses +from typing import Any, cast + + +def pydantic_dataclass_to_dict(obj: Any) -> Any: + """Recursively convert dataclass instances (including pydantic dataclasses) to dictionaries.""" + if dataclasses.is_dataclass(obj) and not isinstance(obj, type): + # dataclasses.asdict already recurses into nested dataclasses/lists + data = dataclasses.asdict(obj) + if data: + return data + # Fallback for dataclasses where asdict() returns empty (observed with some pydantic dataclasses) + result: dict[str, object] = {} + for field in dataclasses.fields(obj): + value = getattr(obj, field.name) + result[field.name] = pydantic_dataclass_to_dict(value) + return result + if isinstance(obj, list): + return [pydantic_dataclass_to_dict(item) for item in obj] + if isinstance(obj, dict): + return {key: pydantic_dataclass_to_dict(value) for key, value in obj.items()} + return obj + from .interfaces import ( ISemanticRefCollection, diff --git a/typeagent/knowpro/types.py b/typeagent/knowpro/types.py new file mode 100644 index 0000000..92aa742 --- /dev/null +++ b/typeagent/knowpro/types.py @@ -0,0 +1,31 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +"""Shared type helpers used to break circular imports in knowpro.""" + +from typing import Any, Generic, NotRequired, TypedDict, TypeVar + +TMessageData = TypeVar("TMessageData") + + +class ConversationDataWithIndexes(TypedDict, Generic[TMessageData]): + """Serializable conversation payload with index metadata.""" + + nameTag: str + messages: list[TMessageData] + tags: list[str] + semanticRefs: list[Any] | None + semanticIndexData: NotRequired[Any] + relatedTermsIndexData: NotRequired[Any] + threadData: NotRequired[Any] + messageIndexData: NotRequired[Any] + + +# When importing from modules that cannot depend on knowpro.interfaces, +# fall back to ``Any`` to avoid circular references while keeping type checkers +# satisfied. +SearchTermGroupTypes = Any + +__all__ = [ + "ConversationDataWithIndexes", + "SearchTermGroupTypes", +] diff --git a/typeagent/knowpro/universal_message.py b/typeagent/knowpro/universal_message.py index a2eb142..a01f3e2 100644 --- a/typeagent/knowpro/universal_message.py +++ b/typeagent/knowpro/universal_message.py @@ -7,9 +7,9 @@ from typing import TypedDict from pydantic import Field -from pydantic.dataclasses import dataclass as pydantic_dataclass from . import kplib +from .dataclasses import dataclass as pydantic_dataclass from .field_helpers import CamelCaseField from .interfaces import IKnowledgeSource, IMessage, IMessageMetadata