Stay Hungry,Stay Foolish!

typing.Annotated

typing.Annotated

https://docs.python.org/3/library/typing.html

Special typing form to add context-specific metadata to an annotation.

Add metadata x to a given type T by using the annotation Annotated[T, x]. Metadata added using Annotated can be used by static analysis tools or at runtime. At runtime, the metadata is stored in a __metadata__ attribute.

If a library or tool encounters an annotation Annotated[T, x] and has no special logic for the metadata, it should ignore the metadata and simply treat the annotation as T. As such, Annotated can be useful for code that wants to use annotations for purposes outside Python’s static typing system.

Using Annotated[T, x] as an annotation still allows for static typechecking of T, as type checkers will simply ignore the metadata x. In this way, Annotated differs from the @no_type_check decorator, which can also be used for adding annotations outside the scope of the typing system, but completely disables typechecking for a function or class.

The responsibility of how to interpret the metadata lies with the tool or library encountering an Annotated annotation. A tool or library encountering an Annotated type can scan through the metadata elements to determine if they are of interest (e.g., using isinstance()).

Annotated[<type>, <metadata>]

Here is an example of how you might use Annotated to add metadata to type annotations if you were doing range analysis:

@dataclass
class ValueRange:
    lo: int
    hi: int

T1 = Annotated[int, ValueRange(-10, 5)]
T2 = Annotated[T1, ValueRange(-20, 3)]

The first argument to Annotated must be a valid type. Multiple metadata elements can be supplied as Annotated supports variadic arguments. The order of the metadata elements is preserved and matters for equality checks:

@dataclass
class ctype:
     kind: str

a1 = Annotated[int, ValueRange(3, 10), ctype("char")]
a2 = Annotated[int, ctype("char"), ValueRange(3, 10)]

assert a1 != a2  # Order matters

It is up to the tool consuming the annotations to decide whether the client is allowed to add multiple metadata elements to one annotation and how to merge those annotations.

Nested Annotated types are flattened. The order of the metadata elements starts with the innermost annotation:

assert Annotated[Annotated[int, ValueRange(3, 10)], ctype("char")] == Annotated[
    int, ValueRange(3, 10), ctype("char")
]

Duplicated metadata elements are not removed:

assert Annotated[int, ValueRange(3, 10)] != Annotated[
    int, ValueRange(3, 10), ValueRange(3, 10)
]

Annotated can be used with nested and generic aliases:

@dataclass
class MaxLen:
    value: int

type Vec[T] = Annotated[list[tuple[T, T]], MaxLen(10)]

# When used in a type annotation, a type checker will treat "V" the same as
# ``Annotated[list[tuple[int, int]], MaxLen(10)]``:
type V = Vec[int]

Annotated cannot be used with an unpacked TypeVarTuple:

type Variadic[*Ts] = Annotated[*Ts, Ann1] = Annotated[T1, T2, T3, ..., Ann1]  # NOT valid

where T1, T2, … are TypeVars. This is invalid as only one type should be passed to Annotated.

By default, get_type_hints() strips the metadata from annotations. Pass include_extras=True to have the metadata preserved:

>>>

from typing import Annotated, get_type_hints
def func(x: Annotated[int, "metadata"]) -> None: pass

get_type_hints(func)
{'x': <class 'int'>, 'return': <class 'NoneType'>}
get_type_hints(func, include_extras=True)
{'x': typing.Annotated[int, 'metadata'], 'return': <class 'NoneType'>}

At runtime, the metadata associated with an Annotated type can be retrieved via the __metadata__ attribute:

>>>

from typing import Annotated
X = Annotated[int, "very", "important", "metadata"]
X
typing.Annotated[int, 'very', 'important', 'metadata']
X.__metadata__
('very', 'important', 'metadata')

If you want to retrieve the original type wrapped by Annotated, use the __origin__ attribute:

>>>

from typing import Annotated, get_origin
Password = Annotated[str, "secret"]
Password.__origin__
<class 'str'>

Note that using get_origin() will return Annotated itself:

>>>

get_origin(Password)
typing.Annotated

See also

PEP 593 - Flexible function and variable annotations

The PEP introducing Annotated to the standard library.

Added in version 3.9.

 

https://github.com/fanqingsong/fastapi-mcp-langgraph-template

from typing import AsyncGenerator

import psycopg.errors
from fastapi import APIRouter
from langchain_core.messages import HumanMessage
from langgraph.checkpoint.postgres.aio import AsyncPostgresSaver
from sse_starlette.sse import EventSourceResponse
from starlette.responses import Response

from api.core.agent.orchestration import get_config, get_graph
from api.core.dependencies import LLMDep, setup_graph
from api.core.logs import print, uvicorn

router = APIRouter(tags=["chat"])


@router.get("/chat/completions")
async def completions(query: str, llm: LLMDep) -> Response:
    """
    Stream model completions as Server-Sent Events (SSE).

    This endpoint sends the model's responses in real-time as they are generated,
    allowing for a continuous stream of data to the client.
    """
    return EventSourceResponse(stream_completions(query, llm))


@router.get("/chat/agent")
async def agent(query: str, llm: LLMDep) -> Response:
    """Stream LangGraph completions as Server-Sent Events (SSE).

    This endpoint streams LangGraph-generated events in real-time, allowing the client
    to receive responses as they are processed, useful for agent-based workflows.
    """
    return EventSourceResponse(stream_graph(query, llm))


async def stream_completions(
    query: str, llm: LLMDep
) -> AsyncGenerator[dict[str, str], None]:
    async for chunk in llm.astream_events(query):
        yield dict(data=chunk)


async def checkpointer_setup(pool):
    checkpointer = AsyncPostgresSaver(pool)
    try:
        await checkpointer.setup()
    except (
        psycopg.errors.DuplicateColumn,
        psycopg.errors.ActiveSqlTransaction,
    ):
        uvicorn.warning("Skipping checkpointer setup — already configured.")
    return checkpointer


async def stream_graph(
    query: str,
    llm: LLMDep,
) -> AsyncGenerator[dict[str, str], None]:
    async with setup_graph() as resource:
        graph = get_graph(
            llm,
            tools=resource.tools,
            checkpointer=resource.checkpointer,
        )
        config = get_config()
        events = dict(messages=[HumanMessage(content=query)])

        async for event in graph.astream_events(events, config, version="v2"):
            if event.get("event").endswith("end"):
                print(event)
            yield dict(data=event)

 

from contextlib import asynccontextmanager
from typing import Annotated, AsyncGenerator

from fastapi import Depends
from langchain_mcp_adapters.tools import load_mcp_tools
from langchain_openai import ChatOpenAI
from sqlalchemy.ext.asyncio import AsyncEngine, create_async_engine

from api.core.agent.persistence import checkpointer_context
from api.core.config import settings
from api.core.mcps import mcp_sse_client
from api.core.models import Resource


def get_llm() -> ChatOpenAI:
    return ChatOpenAI(
        streaming=True,
        model=settings.model,
        temperature=0,
        api_key=settings.openai_api_key,
        stream_usage=True,
    )


LLMDep = Annotated[ChatOpenAI, Depends(get_llm)]


engine: AsyncEngine = create_async_engine(settings.orm_conn_str)


def get_engine() -> AsyncEngine:
    return engine


EngineDep = Annotated[AsyncEngine, Depends(get_engine)]


@asynccontextmanager
async def setup_graph() -> AsyncGenerator[Resource]:
    async with checkpointer_context(
        settings.checkpoint_conn_str
    ) as checkpointer:
        async with mcp_sse_client() as session:
            tools = await load_mcp_tools(session)
            yield Resource(
                checkpointer=checkpointer,
                tools=tools,
                session=session,
            )

 

posted @ 2025-04-28 10:19  lightsong  阅读(69)  评论(0)    收藏  举报
千山鸟飞绝,万径人踪灭