Skip to content
Merged
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
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,16 @@
Code importing the renamed `knowpro.interfaces_*` names from the
`typeagent.knowpro.interfaces` aggregator is unaffected.

#### Core typeagent package
- Replace `CamelCaseField` (and its module `typeagent.knowpro.field_helpers`)
with a pydantic alias generator: the `typeagent.knowpro.dataclasses.dataclass`
decorator now gives every field a camelCase alias. As a result, the
`source_id` field of messages and the `src_url` field of `EmailMessage` are
now saved as `sourceId` and `srcUrl`; data saved under the old names still
loads. Other dataclasses using this decorator also serialize previously
unaliased multi-word fields as camelCase when `by_alias=True`; their
snake_case input names remain accepted unless explicitly overridden.

### 0.5.0 (May 26)

This release focuses on ingestion throughput, robustness, and developer tooling,
Expand Down
14 changes: 7 additions & 7 deletions src/typeagent/emails/email_message.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,8 @@
from typing import Any

from pydantic import Field
from pydantic.dataclasses import dataclass as pydantic_dataclass

from ..knowpro.field_helpers import CamelCaseField
from ..knowpro.dataclasses import dataclass as pydantic_dataclass
from ..knowpro.interfaces import IKnowledgeSource, IMessage, IMessageMetadata
from ..knowpro.knowledge_schema import Action, ConcreteEntity, Facet, KnowledgeResponse

Expand Down Expand Up @@ -148,12 +147,13 @@ class EmailMessage(IMessage):
def __init__(self, **data: Any) -> None:
super().__init__(**data)

text_chunks: list[str] = CamelCaseField("The text chunks of the email message")
metadata: EmailMessageMeta = CamelCaseField(
"Metadata associated with the email message"
text_chunks: list[str] = Field(description="The text chunks of the email message")
# Without Field(), this would inherit the None default from IMessage.
metadata: EmailMessageMeta = Field(
description="Metadata associated with the email message"
)
tags: list[str] = CamelCaseField(
"Tags associated with the message", default_factory=list
tags: list[str] = Field(
default_factory=list, description="Tags associated with the message"
)
timestamp: str | None = None # Use metadata.sent_on for the actual sent time
src_url: str | None = None # Source file or uri for this email
Expand Down
26 changes: 13 additions & 13 deletions src/typeagent/knowpro/core_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@
TypedDict,
)

from pydantic.dataclasses import dataclass
from pydantic import Field
import typechat

from .field_helpers import CamelCaseField
from .dataclasses import dataclass
from .knowledge_schema import Action, ConcreteEntity, KnowledgeResponse

__all__ = [
Expand Down Expand Up @@ -144,10 +144,10 @@ class IMessage[TMetadata: IMessageMetadata](IKnowledgeSource, Protocol):

@dataclass
class ScoredSemanticRefOrdinal:
semantic_ref_ordinal: SemanticRefOrdinal = CamelCaseField(
"The ordinal of the semantic reference"
semantic_ref_ordinal: SemanticRefOrdinal = Field(
description="The ordinal of the semantic reference"
)
score: float = CamelCaseField("The relevance score")
score: float = Field(description="The relevance score")

def __repr__(self) -> str:
return f"{self.__class__.__name__}({self.semantic_ref_ordinal}, {self.score})"
Expand Down Expand Up @@ -223,12 +223,12 @@ class TextLocationData(TypedDict):
@dataclass(order=True)
class TextLocation:
# The ordinal of the message.
message_ordinal: MessageOrdinal = CamelCaseField("The ordinal of the message")
message_ordinal: MessageOrdinal = Field(description="The ordinal of the message")
# The ordinal of the chunk.
# In the end of a TextRange, 1 + ordinal of the last chunk in the range.
chunk_ordinal: int = CamelCaseField(
"The ordinal of the chunk; in the end of a TextRange, 1 + ordinal of the last chunk in the range",
chunk_ordinal: int = Field(
default=0,
description="The ordinal of the chunk; in the end of a TextRange, 1 + ordinal of the last chunk in the range",
)

def __repr__(self) -> str:
Expand Down Expand Up @@ -320,12 +320,12 @@ class SemanticRefData(TypedDict):

@dataclass
class SemanticRef:
semantic_ref_ordinal: SemanticRefOrdinal = CamelCaseField(
"The ordinal of the semantic reference"
semantic_ref_ordinal: SemanticRefOrdinal = Field(
description="The ordinal of the semantic reference"
)
range: TextRange = CamelCaseField("The text range of the semantic reference")
knowledge: Knowledge = CamelCaseField(
"The knowledge associated with this semantic reference"
range: TextRange = Field(description="The text range of the semantic reference")
knowledge: Knowledge = Field(
description="The knowledge associated with this semantic reference"
)

def __repr__(self) -> str:
Expand Down
30 changes: 25 additions & 5 deletions src/typeagent/knowpro/dataclasses.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,33 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
"""Compatibility helpers for pydantic dataclasses."""
"""Pydantic dataclass decorator that adds camelCase aliases to every field.

Fields are declared in snake_case. By default, both the snake_case name and
its generated camelCase form are accepted on input; output uses camelCase
when ``by_alias=True``. Explicit field aliases override the corresponding
generated aliases. A custom serialization alias needs a matching validation
alias if its output must round-trip; setting it alone does not change the
accepted input names.
"""

from collections.abc import Callable
import dataclasses
from typing import Any, cast, overload

from typing_extensions import dataclass_transform

from pydantic import AliasChoices, AliasGenerator, ConfigDict, Field
from pydantic.alias_generators import to_camel
from pydantic.dataclasses import dataclass as _pydantic_dataclass

from .field_helpers import CamelCaseField
# Accept Python and camelCase names through generated validation aliases while
# preserving explicit field validation aliases. Unlike populate_by_name=True,
# this does not globally allow Python names to bypass explicit validation aliases.
# This works on pydantic 2.10 without validate_by_name, which requires 2.11.
_ALIAS_GENERATOR = AliasGenerator(
validation_alias=lambda name: AliasChoices(name, to_camel(name)),
serialization_alias=to_camel,
)


@overload
Expand All @@ -20,14 +38,16 @@ def dataclass[T](__cls: type[T], /, **kwargs: Any) -> type[T]: ...
def dataclass[T](**kwargs: Any) -> Callable[[type[T]], type[T]]: ...


@dataclass_transform(field_specifiers=(CamelCaseField,))
@dataclass_transform(field_specifiers=(dataclasses.field, Field))
def dataclass[T](
__cls: type[T] | None = None, /, **kwargs: Any
) -> Callable[[type[T]], type[T]] | type[T]:
"""Wrapper that preserves pydantic behavior while informing type-checkers."""
"""Like ``pydantic.dataclasses.dataclass``, plus camelCase aliases."""
config = ConfigDict(alias_generator=_ALIAS_GENERATOR)
config.update(kwargs.pop("config", None) or {})

def wrap(cls: type[T]) -> type[T]:
return cast(type[T], _pydantic_dataclass(cls, **kwargs))
return cast(type[T], _pydantic_dataclass(cls, config=config, **kwargs))

if __cls is None:
return wrap
Expand Down
85 changes: 0 additions & 85 deletions src/typeagent/knowpro/field_helpers.py

This file was deleted.

3 changes: 1 addition & 2 deletions src/typeagent/knowpro/index_protocols.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@
from collections.abc import Iterable, Sequence
from typing import Protocol, runtime_checkable

from pydantic.dataclasses import dataclass

from ..aitools.embeddings import NormalizedEmbedding
from .core_types import (
DateRange,
Expand All @@ -20,6 +18,7 @@
Term,
TextRange,
)
from .dataclasses import dataclass
from .serialization_data import (
ConversationThreadData,
MessageTextIndexData,
Expand Down
25 changes: 10 additions & 15 deletions src/typeagent/knowpro/knowledge_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
from typing_extensions import Doc

from .dataclasses import dataclass
from .field_helpers import CamelCaseField


@dataclass
Expand Down Expand Up @@ -100,27 +99,23 @@ class Action:
verbs: Annotated[list[str], Doc("Each verb is typically a word.")]
# NOTE: Declared via Annotated so that there is no class-level default;
# otherwise TypeChat would render this required field as optional.
verb_tense: Annotated[
VerbTense,
CamelCaseField("The tense of the verb", field_name="verb_tense"),
]
subject_entity_name: str | Literal["none"] = CamelCaseField(
"The name of the subject entity", default="none"
)
object_entity_name: str | Literal["none"] = CamelCaseField(
"The name of the object entity", default="none"
)
indirect_object_entity_name: str | Literal["none"] = CamelCaseField(
"The name of the indirect object entity", default="none"
)
verb_tense: Annotated[VerbTense, Doc("The tense of the verb")]
subject_entity_name: Annotated[
str | Literal["none"], Doc("The name of the subject entity")
] = "none"
object_entity_name: Annotated[
str | Literal["none"], Doc("The name of the object entity")
] = "none"
indirect_object_entity_name: Annotated[
str | Literal["none"], Doc("The name of the indirect object entity")
] = "none"
params: list[str | ActionParam] | None = None
subject_entity_facet: Annotated[
Facet | None,
Doc(
"If the action implies this additional facet or property of the subject entity, "
"such as hobbies, activities, interests, personality"
),
CamelCaseField(field_name="subject_entity_facet"),
] = None


Expand Down
7 changes: 4 additions & 3 deletions src/typeagent/knowpro/search.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@
from collections.abc import Callable
from typing import cast, TypeGuard

from pydantic import Field

from ..storage.memory import related_terms_index
from ..storage.memory.message_index import IMessageTextEmbeddingIndex
from .collections import MessageAccumulator, SemanticRefAccumulator
from .dataclasses import dataclass
from .field_helpers import CamelCaseField
from .interfaces import (
IConversation,
IConversationSecondaryIndexes,
Expand Down Expand Up @@ -71,8 +72,8 @@

@dataclass
class SearchQueryExpr:
select_expressions: list[SearchSelectExpr] = CamelCaseField(
"List of selection expressions for the search"
select_expressions: list[SearchSelectExpr] = Field(
description="List of selection expressions for the search"
)
raw_query: str | None = None

Expand Down
8 changes: 1 addition & 7 deletions src/typeagent/knowpro/search_query_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@

from .dataclasses import dataclass
from .date_time_schema import DateTimeRange
from .field_helpers import CamelCaseField


@dataclass
Expand Down Expand Up @@ -146,11 +145,7 @@ class SearchFilter:

@dataclass
class SearchExpr:
rewritten_query: Annotated[
str,
Doc("The rewritten search query"),
CamelCaseField(field_name="rewritten_query"),
]
rewritten_query: Annotated[str, Doc("The rewritten search query")]
filters: Annotated[list[SearchFilter], Doc("List of search filters")]


Expand All @@ -161,5 +156,4 @@ class SearchQuery:
Doc(
"One expression for each search required by user request. Each SearchExpr runs independently, so make them standalone by resolving references like 'it', 'that', 'them' etc."
),
CamelCaseField(field_name="search_expressions"),
]
Loading
Loading