Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion test/test_conversation_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
14 changes: 13 additions & 1 deletion test/test_mcp_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import os
import sys
from typing import Any
from typing import Any, TYPE_CHECKING

import pytest
from mcp import StdioServerParameters
Expand All @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion test/test_sqlitestore.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion test/test_storage_providers_unified.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion typeagent/emails/email_message.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
3 changes: 2 additions & 1 deletion typeagent/knowpro/answer_response_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
37 changes: 37 additions & 0 deletions typeagent/knowpro/dataclasses.py
Original file line number Diff line number Diff line change
@@ -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)
3 changes: 2 additions & 1 deletion typeagent/knowpro/date_time_schema.py
Original file line number Diff line number Diff line change
@@ -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:
Expand Down
2 changes: 1 addition & 1 deletion typeagent/knowpro/interfaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion typeagent/knowpro/kplib.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down
2 changes: 1 addition & 1 deletion typeagent/knowpro/search.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion typeagent/knowpro/search_query_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
24 changes: 23 additions & 1 deletion typeagent/knowpro/searchlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
31 changes: 31 additions & 0 deletions typeagent/knowpro/types.py
Original file line number Diff line number Diff line change
@@ -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",
]
2 changes: 1 addition & 1 deletion typeagent/knowpro/universal_message.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Loading