pi-ai-python 1.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pi_agent/__init__.py +127 -0
- pi_agent/agent.py +617 -0
- pi_agent/agent_loop.py +820 -0
- pi_agent/proxy.py +269 -0
- pi_agent/py.typed +0 -0
- pi_agent/stream_fn.py +27 -0
- pi_agent/tools.py +327 -0
- pi_agent/types.py +394 -0
- pi_ai/__init__.py +263 -0
- pi_ai/abort.py +315 -0
- pi_ai/api/__init__.py +1 -0
- pi_ai/api/anthropic_messages.py +1782 -0
- pi_ai/api/azure_openai_config.py +138 -0
- pi_ai/api/azure_openai_responses.py +245 -0
- pi_ai/api/bedrock_converse_stream.py +1598 -0
- pi_ai/api/classifier_shared.py +211 -0
- pi_ai/api/cloudflare.py +36 -0
- pi_ai/api/cloudflare_workers_ai_system_one.py +80 -0
- pi_ai/api/constrained_sampling.py +279 -0
- pi_ai/api/github_copilot_headers.py +30 -0
- pi_ai/api/google_generative_ai.py +138 -0
- pi_ai/api/google_shared.py +927 -0
- pi_ai/api/google_vertex.py +234 -0
- pi_ai/api/lazy.py +156 -0
- pi_ai/api/llama_cpp_classify.py +449 -0
- pi_ai/api/mistral_conversations.py +756 -0
- pi_ai/api/openai_codex_responses.py +1545 -0
- pi_ai/api/openai_completions.py +1594 -0
- pi_ai/api/openai_decisions.py +210 -0
- pi_ai/api/openai_prompt_cache.py +19 -0
- pi_ai/api/openai_responses.py +418 -0
- pi_ai/api/openai_responses_shared.py +840 -0
- pi_ai/api/openai_sdk.py +204 -0
- pi_ai/api/openrouter_images.py +153 -0
- pi_ai/api/pi_messages.py +449 -0
- pi_ai/api/registry.py +103 -0
- pi_ai/api/simple_options.py +119 -0
- pi_ai/api/system_one_shared.py +148 -0
- pi_ai/api/transform_messages.py +211 -0
- pi_ai/api/typesafe_system_one.py +43 -0
- pi_ai/auth/__init__.py +1 -0
- pi_ai/auth/context.py +47 -0
- pi_ai/auth/credential_store.py +208 -0
- pi_ai/auth/helpers.py +123 -0
- pi_ai/auth/oauth/__init__.py +5 -0
- pi_ai/auth/oauth/_common.py +454 -0
- pi_ai/auth/oauth/anthropic.py +283 -0
- pi_ai/auth/oauth/callback_server.py +339 -0
- pi_ai/auth/oauth/device_code.py +127 -0
- pi_ai/auth/oauth/github_copilot.py +508 -0
- pi_ai/auth/oauth/kimi_coding.py +283 -0
- pi_ai/auth/oauth/load.py +64 -0
- pi_ai/auth/oauth/meta.py +238 -0
- pi_ai/auth/oauth/openai_chatgpt.py +321 -0
- pi_ai/auth/oauth/openai_codex.py +391 -0
- pi_ai/auth/oauth/openrouter.py +173 -0
- pi_ai/auth/oauth/pkce.py +23 -0
- pi_ai/auth/oauth/radius.py +311 -0
- pi_ai/auth/oauth/xai.py +220 -0
- pi_ai/auth/resolve.py +190 -0
- pi_ai/auth/types.py +292 -0
- pi_ai/cli.py +265 -0
- pi_ai/compat.py +651 -0
- pi_ai/env_api_keys.py +137 -0
- pi_ai/event_stream.py +329 -0
- pi_ai/models.py +1191 -0
- pi_ai/models_store.py +121 -0
- pi_ai/oauth.py +90 -0
- pi_ai/providers/__init__.py +1 -0
- pi_ai/providers/all.py +154 -0
- pi_ai/providers/amazon_bedrock.py +116 -0
- pi_ai/providers/ant_ling.py +23 -0
- pi_ai/providers/anthropic.py +105 -0
- pi_ai/providers/azure.py +103 -0
- pi_ai/providers/baseten.py +23 -0
- pi_ai/providers/catalog.py +167 -0
- pi_ai/providers/cerebras.py +23 -0
- pi_ai/providers/cloudflare_ai_gateway.py +30 -0
- pi_ai/providers/cloudflare_auth.py +112 -0
- pi_ai/providers/cloudflare_stream.py +84 -0
- pi_ai/providers/cloudflare_workers_ai.py +26 -0
- pi_ai/providers/data/amazon-bedrock.json +1 -0
- pi_ai/providers/data/ant-ling.json +1 -0
- pi_ai/providers/data/anthropic.json +1 -0
- pi_ai/providers/data/azure.json +1 -0
- pi_ai/providers/data/baseten.json +1 -0
- pi_ai/providers/data/cerebras.json +1 -0
- pi_ai/providers/data/cloudflare-ai-gateway.json +1 -0
- pi_ai/providers/data/cloudflare-workers-ai.json +1 -0
- pi_ai/providers/data/deepseek.json +1 -0
- pi_ai/providers/data/fireworks.json +1 -0
- pi_ai/providers/data/github-copilot.json +1 -0
- pi_ai/providers/data/google-vertex.json +1 -0
- pi_ai/providers/data/google.json +1 -0
- pi_ai/providers/data/groq.json +1 -0
- pi_ai/providers/data/huggingface.json +1 -0
- pi_ai/providers/data/kimi-coding.json +1 -0
- pi_ai/providers/data/manifest.json +1 -0
- pi_ai/providers/data/meta.json +1 -0
- pi_ai/providers/data/minimax-cn.json +1 -0
- pi_ai/providers/data/minimax.json +1 -0
- pi_ai/providers/data/mistral.json +1 -0
- pi_ai/providers/data/moonshotai-cn.json +1 -0
- pi_ai/providers/data/moonshotai.json +1 -0
- pi_ai/providers/data/nvidia.json +1 -0
- pi_ai/providers/data/openai-codex.json +1 -0
- pi_ai/providers/data/openai.json +1 -0
- pi_ai/providers/data/opencode-go.json +1 -0
- pi_ai/providers/data/opencode.json +1 -0
- pi_ai/providers/data/openrouter.json +1 -0
- pi_ai/providers/data/qwen-token-plan-cn.json +1 -0
- pi_ai/providers/data/qwen-token-plan-individual.json +1 -0
- pi_ai/providers/data/qwen-token-plan.json +1 -0
- pi_ai/providers/data/radius.json +1 -0
- pi_ai/providers/data/together.json +1 -0
- pi_ai/providers/data/typesafe.json +1 -0
- pi_ai/providers/data/vercel-ai-gateway.json +1 -0
- pi_ai/providers/data/xai.json +1 -0
- pi_ai/providers/data/xiaomi-token-plan-ams.json +1 -0
- pi_ai/providers/data/xiaomi-token-plan-cn.json +1 -0
- pi_ai/providers/data/xiaomi-token-plan-sgp.json +1 -0
- pi_ai/providers/data/xiaomi.json +1 -0
- pi_ai/providers/data/zai-coding-cn.json +1 -0
- pi_ai/providers/data/zai.json +1 -0
- pi_ai/providers/deepseek.py +23 -0
- pi_ai/providers/faux.py +680 -0
- pi_ai/providers/fireworks.py +26 -0
- pi_ai/providers/github_copilot.py +46 -0
- pi_ai/providers/google.py +23 -0
- pi_ai/providers/google_vertex.py +131 -0
- pi_ai/providers/groq.py +23 -0
- pi_ai/providers/huggingface.py +23 -0
- pi_ai/providers/kimi_coding.py +32 -0
- pi_ai/providers/meta.py +32 -0
- pi_ai/providers/minimax.py +23 -0
- pi_ai/providers/minimax_cn.py +23 -0
- pi_ai/providers/mistral.py +23 -0
- pi_ai/providers/moonshotai.py +23 -0
- pi_ai/providers/moonshotai_cn.py +23 -0
- pi_ai/providers/nvidia.py +23 -0
- pi_ai/providers/openai.py +44 -0
- pi_ai/providers/openai_codex.py +26 -0
- pi_ai/providers/opencode.py +36 -0
- pi_ai/providers/opencode_go.py +33 -0
- pi_ai/providers/opencode_headers.py +60 -0
- pi_ai/providers/openrouter.py +145 -0
- pi_ai/providers/openrouter_api.py +122 -0
- pi_ai/providers/qwen_token_plan.py +23 -0
- pi_ai/providers/qwen_token_plan_cn.py +23 -0
- pi_ai/providers/qwen_token_plan_individual.py +23 -0
- pi_ai/providers/radius.py +146 -0
- pi_ai/providers/radius_config.py +136 -0
- pi_ai/providers/together.py +23 -0
- pi_ai/providers/typesafe.py +22 -0
- pi_ai/providers/vercel_ai_gateway.py +25 -0
- pi_ai/providers/xai.py +32 -0
- pi_ai/providers/xiaomi.py +23 -0
- pi_ai/providers/xiaomi_token_plan_ams.py +23 -0
- pi_ai/providers/xiaomi_token_plan_cn.py +23 -0
- pi_ai/providers/xiaomi_token_plan_sgp.py +23 -0
- pi_ai/providers/zai.py +23 -0
- pi_ai/providers/zai_coding_cn.py +23 -0
- pi_ai/py.typed +0 -0
- pi_ai/serialization.py +477 -0
- pi_ai/session_resources.py +33 -0
- pi_ai/types.py +1003 -0
- pi_ai/utils/__init__.py +1 -0
- pi_ai/utils/callbacks.py +15 -0
- pi_ai/utils/diagnostics.py +70 -0
- pi_ai/utils/error_body.py +89 -0
- pi_ai/utils/estimate.py +125 -0
- pi_ai/utils/fastjson.py +63 -0
- pi_ai/utils/http.py +409 -0
- pi_ai/utils/json_parse.py +424 -0
- pi_ai/utils/misc.py +193 -0
- pi_ai/utils/model_operations.py +73 -0
- pi_ai/utils/models_error.py +35 -0
- pi_ai/utils/oauth_page.py +121 -0
- pi_ai/utils/overflow.py +87 -0
- pi_ai/utils/retry.py +193 -0
- pi_ai/utils/schema.py +283 -0
- pi_ai/utils/text.py +65 -0
- pi_ai/utils/transcript.py +271 -0
- pi_ai/utils/validation.py +813 -0
- pi_ai_python-1.3.0.dist-info/METADATA +167 -0
- pi_ai_python-1.3.0.dist-info/RECORD +189 -0
- pi_ai_python-1.3.0.dist-info/WHEEL +4 -0
- pi_ai_python-1.3.0.dist-info/entry_points.txt +2 -0
- pi_ai_python-1.3.0.dist-info/licenses/LICENSE +22 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Vercel AI Gateway provider."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from ..api.registry import anthropic_messages_api, typesafe_system_one_api
|
|
6
|
+
from ..auth.helpers import env_api_key_auth
|
|
7
|
+
from ..auth.types import ProviderAuth
|
|
8
|
+
from ..models import Provider, create_provider
|
|
9
|
+
from .catalog import catalog_models
|
|
10
|
+
|
|
11
|
+
__all__ = ["vercel_ai_gateway_provider"]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def vercel_ai_gateway_provider() -> Provider:
|
|
15
|
+
"""Vercel AI Gateway (Anthropic Messages API), authenticated with ``AI_GATEWAY_API_KEY``."""
|
|
16
|
+
return create_provider(
|
|
17
|
+
id="vercel-ai-gateway",
|
|
18
|
+
name="Vercel AI Gateway",
|
|
19
|
+
base_url="https://ai-gateway.vercel.sh",
|
|
20
|
+
auth=ProviderAuth(api_key=env_api_key_auth("Vercel AI Gateway API key", ["AI_GATEWAY_API_KEY"])),
|
|
21
|
+
models=lambda: catalog_models("vercel-ai-gateway"),
|
|
22
|
+
api=anthropic_messages_api(),
|
|
23
|
+
# AI Gateway serves TypeSafe's System One protocol at /typesafe/v1/systemone.
|
|
24
|
+
classifiers={"typesafe-system-one": typesafe_system_one_api()},
|
|
25
|
+
)
|
pi_ai/providers/xai.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""xAI provider."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from ..api.registry import openai_responses_api
|
|
6
|
+
from ..auth.helpers import env_api_key_auth, lazy_oauth
|
|
7
|
+
from ..auth.oauth.load import load_xai_oauth
|
|
8
|
+
from ..auth.types import ProviderAuth
|
|
9
|
+
from ..models import Provider, create_provider
|
|
10
|
+
from .catalog import catalog_models
|
|
11
|
+
|
|
12
|
+
__all__ = ["xai_provider"]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def xai_provider() -> Provider:
|
|
16
|
+
"""xAI (OpenAI Responses API): ``XAI_API_KEY`` or a SuperGrok / X Premium login."""
|
|
17
|
+
return create_provider(
|
|
18
|
+
id="xai",
|
|
19
|
+
name="xAI",
|
|
20
|
+
base_url="https://api.x.ai/v1",
|
|
21
|
+
auth=ProviderAuth(
|
|
22
|
+
api_key=env_api_key_auth("xAI API key", ["XAI_API_KEY"]),
|
|
23
|
+
oauth=lazy_oauth(
|
|
24
|
+
"xAI (Grok/X subscription)",
|
|
25
|
+
load_xai_oauth,
|
|
26
|
+
is_subscription=True,
|
|
27
|
+
login_label="Sign in with SuperGrok or X Premium",
|
|
28
|
+
),
|
|
29
|
+
),
|
|
30
|
+
models=lambda: catalog_models("xai"),
|
|
31
|
+
api=openai_responses_api(),
|
|
32
|
+
)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Xiaomi provider."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from ..api.registry import openai_completions_api
|
|
6
|
+
from ..auth.helpers import env_api_key_auth
|
|
7
|
+
from ..auth.types import ProviderAuth
|
|
8
|
+
from ..models import Provider, create_provider
|
|
9
|
+
from .catalog import catalog_models
|
|
10
|
+
|
|
11
|
+
__all__ = ["xiaomi_provider"]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def xiaomi_provider() -> Provider:
|
|
15
|
+
"""Xiaomi (OpenAI-compatible Chat Completions), authenticated with ``XIAOMI_API_KEY``."""
|
|
16
|
+
return create_provider(
|
|
17
|
+
id="xiaomi",
|
|
18
|
+
name="Xiaomi",
|
|
19
|
+
base_url="https://api.xiaomimimo.com/v1",
|
|
20
|
+
auth=ProviderAuth(api_key=env_api_key_auth("Xiaomi API key", ["XIAOMI_API_KEY"])),
|
|
21
|
+
models=lambda: catalog_models("xiaomi"),
|
|
22
|
+
api=openai_completions_api(),
|
|
23
|
+
)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Xiaomi Token Plan AMS provider."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from ..api.registry import openai_completions_api
|
|
6
|
+
from ..auth.helpers import env_api_key_auth
|
|
7
|
+
from ..auth.types import ProviderAuth
|
|
8
|
+
from ..models import Provider, create_provider
|
|
9
|
+
from .catalog import catalog_models
|
|
10
|
+
|
|
11
|
+
__all__ = ["xiaomi_token_plan_ams_provider"]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def xiaomi_token_plan_ams_provider() -> Provider:
|
|
15
|
+
"""Xiaomi Token Plan AMS (OpenAI-compatible Chat Completions), authenticated with ``XIAOMI_TOKEN_PLAN_AMS_API_KEY``."""
|
|
16
|
+
return create_provider(
|
|
17
|
+
id="xiaomi-token-plan-ams",
|
|
18
|
+
name="Xiaomi Token Plan AMS",
|
|
19
|
+
base_url="https://token-plan-ams.xiaomimimo.com/v1",
|
|
20
|
+
auth=ProviderAuth(api_key=env_api_key_auth("Xiaomi Token Plan AMS API key", ["XIAOMI_TOKEN_PLAN_AMS_API_KEY"])),
|
|
21
|
+
models=lambda: catalog_models("xiaomi-token-plan-ams"),
|
|
22
|
+
api=openai_completions_api(),
|
|
23
|
+
)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Xiaomi Token Plan CN provider."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from ..api.registry import openai_completions_api
|
|
6
|
+
from ..auth.helpers import env_api_key_auth
|
|
7
|
+
from ..auth.types import ProviderAuth
|
|
8
|
+
from ..models import Provider, create_provider
|
|
9
|
+
from .catalog import catalog_models
|
|
10
|
+
|
|
11
|
+
__all__ = ["xiaomi_token_plan_cn_provider"]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def xiaomi_token_plan_cn_provider() -> Provider:
|
|
15
|
+
"""Xiaomi Token Plan CN (OpenAI-compatible Chat Completions), authenticated with ``XIAOMI_TOKEN_PLAN_CN_API_KEY``."""
|
|
16
|
+
return create_provider(
|
|
17
|
+
id="xiaomi-token-plan-cn",
|
|
18
|
+
name="Xiaomi Token Plan CN",
|
|
19
|
+
base_url="https://token-plan-cn.xiaomimimo.com/v1",
|
|
20
|
+
auth=ProviderAuth(api_key=env_api_key_auth("Xiaomi Token Plan CN API key", ["XIAOMI_TOKEN_PLAN_CN_API_KEY"])),
|
|
21
|
+
models=lambda: catalog_models("xiaomi-token-plan-cn"),
|
|
22
|
+
api=openai_completions_api(),
|
|
23
|
+
)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Xiaomi Token Plan SGP provider."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from ..api.registry import openai_completions_api
|
|
6
|
+
from ..auth.helpers import env_api_key_auth
|
|
7
|
+
from ..auth.types import ProviderAuth
|
|
8
|
+
from ..models import Provider, create_provider
|
|
9
|
+
from .catalog import catalog_models
|
|
10
|
+
|
|
11
|
+
__all__ = ["xiaomi_token_plan_sgp_provider"]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def xiaomi_token_plan_sgp_provider() -> Provider:
|
|
15
|
+
"""Xiaomi Token Plan SGP (OpenAI-compatible Chat Completions), authenticated with ``XIAOMI_TOKEN_PLAN_SGP_API_KEY``."""
|
|
16
|
+
return create_provider(
|
|
17
|
+
id="xiaomi-token-plan-sgp",
|
|
18
|
+
name="Xiaomi Token Plan SGP",
|
|
19
|
+
base_url="https://token-plan-sgp.xiaomimimo.com/v1",
|
|
20
|
+
auth=ProviderAuth(api_key=env_api_key_auth("Xiaomi Token Plan SGP API key", ["XIAOMI_TOKEN_PLAN_SGP_API_KEY"])),
|
|
21
|
+
models=lambda: catalog_models("xiaomi-token-plan-sgp"),
|
|
22
|
+
api=openai_completions_api(),
|
|
23
|
+
)
|
pi_ai/providers/zai.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Z.AI provider."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from ..api.registry import openai_completions_api
|
|
6
|
+
from ..auth.helpers import env_api_key_auth
|
|
7
|
+
from ..auth.types import ProviderAuth
|
|
8
|
+
from ..models import Provider, create_provider
|
|
9
|
+
from .catalog import catalog_models
|
|
10
|
+
|
|
11
|
+
__all__ = ["zai_provider"]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def zai_provider() -> Provider:
|
|
15
|
+
"""Z.AI (OpenAI-compatible Chat Completions), authenticated with ``ZAI_API_KEY``."""
|
|
16
|
+
return create_provider(
|
|
17
|
+
id="zai",
|
|
18
|
+
name="Z.AI",
|
|
19
|
+
base_url="https://api.z.ai/api/coding/paas/v4",
|
|
20
|
+
auth=ProviderAuth(api_key=env_api_key_auth("Z.AI API key", ["ZAI_API_KEY"])),
|
|
21
|
+
models=lambda: catalog_models("zai"),
|
|
22
|
+
api=openai_completions_api(),
|
|
23
|
+
)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Z.AI Coding CN provider."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from ..api.registry import openai_completions_api
|
|
6
|
+
from ..auth.helpers import env_api_key_auth
|
|
7
|
+
from ..auth.types import ProviderAuth
|
|
8
|
+
from ..models import Provider, create_provider
|
|
9
|
+
from .catalog import catalog_models
|
|
10
|
+
|
|
11
|
+
__all__ = ["zai_coding_cn_provider"]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def zai_coding_cn_provider() -> Provider:
|
|
15
|
+
"""Z.AI Coding CN (OpenAI-compatible Chat Completions), authenticated with ``ZAI_CODING_CN_API_KEY``."""
|
|
16
|
+
return create_provider(
|
|
17
|
+
id="zai-coding-cn",
|
|
18
|
+
name="Z.AI Coding CN",
|
|
19
|
+
base_url="https://open.bigmodel.cn/api/coding/paas/v4",
|
|
20
|
+
auth=ProviderAuth(api_key=env_api_key_auth("Z.AI Coding CN API key", ["ZAI_CODING_CN_API_KEY"])),
|
|
21
|
+
models=lambda: catalog_models("zai-coding-cn"),
|
|
22
|
+
api=openai_completions_api(),
|
|
23
|
+
)
|
pi_ai/py.typed
ADDED
|
File without changes
|
pi_ai/serialization.py
ADDED
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
"""Conversion between pi-ai data classes and pi's JSON representation.
|
|
2
|
+
|
|
3
|
+
The TypeScript implementation stores contexts, messages and models as plain JSON
|
|
4
|
+
objects with camelCase keys. These helpers produce and read exactly that format,
|
|
5
|
+
so a session written by the Python port can be resumed by TypeScript pi and vice
|
|
6
|
+
versa::
|
|
7
|
+
|
|
8
|
+
from pi_ai.serialization import context_to_dict, context_from_dict, dumps, loads
|
|
9
|
+
|
|
10
|
+
text = dumps(context) # JSON string in pi's format
|
|
11
|
+
restored = loads(text, Context) # back to data classes
|
|
12
|
+
|
|
13
|
+
Free-form JSON values (tool arguments, tool result ``details``, headers, model
|
|
14
|
+
``compat`` settings, ``metadata``) are passed through untouched.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import dataclasses
|
|
20
|
+
import json
|
|
21
|
+
import re
|
|
22
|
+
from collections.abc import Callable, Mapping
|
|
23
|
+
from typing import Any, TypeVar
|
|
24
|
+
|
|
25
|
+
from . import types as t
|
|
26
|
+
from .utils.diagnostics import AssistantMessageDiagnostic, DiagnosticErrorInfo
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
"to_dict",
|
|
30
|
+
"to_json_value",
|
|
31
|
+
"from_dict",
|
|
32
|
+
"dumps",
|
|
33
|
+
"loads",
|
|
34
|
+
"message_from_dict",
|
|
35
|
+
"messages_from_list",
|
|
36
|
+
"content_from_dict",
|
|
37
|
+
"tool_from_dict",
|
|
38
|
+
"model_from_dict",
|
|
39
|
+
"event_from_dict",
|
|
40
|
+
"context_from_dict",
|
|
41
|
+
"context_to_dict",
|
|
42
|
+
"camel_case",
|
|
43
|
+
"snake_case",
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
T = TypeVar("T")
|
|
47
|
+
|
|
48
|
+
_CAMEL_RE = re.compile(r"_([a-z0-9])")
|
|
49
|
+
_SNAKE_RE = re.compile(r"(?<=[a-z0-9])([A-Z])")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def camel_case(name: str) -> str:
|
|
53
|
+
"""``tool_call_id`` -> ``toolCallId`` (``cache_write_1h`` -> ``cacheWrite1h``)."""
|
|
54
|
+
return _CAMEL_RE.sub(lambda match: match.group(1).upper(), name)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def snake_case(name: str) -> str:
|
|
58
|
+
"""``toolCallId`` -> ``tool_call_id`` (``cacheWrite1h`` -> ``cache_write_1h``)."""
|
|
59
|
+
converted = _SNAKE_RE.sub(lambda match: "_" + match.group(1).lower(), name)
|
|
60
|
+
return re.sub(r"(?<=[a-z])(?=\d)", "_", converted)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# Fields whose values are free-form JSON: copied verbatim, keys never converted.
|
|
64
|
+
_RAW_FIELDS = frozenset(
|
|
65
|
+
{
|
|
66
|
+
"arguments",
|
|
67
|
+
"details",
|
|
68
|
+
"headers",
|
|
69
|
+
"compat",
|
|
70
|
+
"metadata",
|
|
71
|
+
"sections",
|
|
72
|
+
"thinking_level_map",
|
|
73
|
+
"prompt_cache",
|
|
74
|
+
"sampling_params",
|
|
75
|
+
"sampling_params_by_thinking_level",
|
|
76
|
+
"parameters",
|
|
77
|
+
"constrained_sampling",
|
|
78
|
+
"data",
|
|
79
|
+
"state",
|
|
80
|
+
"criteria",
|
|
81
|
+
"probabilities",
|
|
82
|
+
"env",
|
|
83
|
+
}
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _decode_list(decoder: Callable[[Any], Any]) -> Callable[[Any], Any]:
|
|
88
|
+
return lambda values: [decoder(value) for value in values] if isinstance(values, list) else values
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
# ---------------------------------------------------------------------------
|
|
92
|
+
# Encoding
|
|
93
|
+
# ---------------------------------------------------------------------------
|
|
94
|
+
|
|
95
|
+
_field_cache: dict[type, tuple[tuple[str, str], ...]] = {}
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _fields(cls: type) -> tuple[tuple[str, str], ...]:
|
|
99
|
+
cached = _field_cache.get(cls)
|
|
100
|
+
if cached is None:
|
|
101
|
+
cached = tuple((f.name, camel_case(f.name)) for f in dataclasses.fields(cls) if not f.name.startswith("_"))
|
|
102
|
+
_field_cache[cls] = cached
|
|
103
|
+
return cached
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _discriminator(obj: Any) -> tuple[str, str] | None:
|
|
107
|
+
cls = type(obj)
|
|
108
|
+
for key in ("type", "role"):
|
|
109
|
+
value = cls.__dict__.get(key)
|
|
110
|
+
if value is None:
|
|
111
|
+
for base in cls.__mro__[1:]:
|
|
112
|
+
if key in base.__dict__:
|
|
113
|
+
value = base.__dict__[key]
|
|
114
|
+
break
|
|
115
|
+
if isinstance(value, str):
|
|
116
|
+
return (key, value)
|
|
117
|
+
return None
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def to_dict(obj: Any) -> Any:
|
|
121
|
+
"""Encode a pi-ai data class (or a list/dict of them) as pi JSON with camelCase keys.
|
|
122
|
+
|
|
123
|
+
``None`` fields are omitted, like ``undefined`` properties in TypeScript.
|
|
124
|
+
"""
|
|
125
|
+
return to_json_value(obj)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def to_json_value(obj: Any) -> Any:
|
|
129
|
+
if obj is None or isinstance(obj, (str, int, float, bool)):
|
|
130
|
+
return obj
|
|
131
|
+
if isinstance(obj, (list, tuple)):
|
|
132
|
+
return [to_json_value(item) for item in obj]
|
|
133
|
+
if isinstance(obj, Mapping):
|
|
134
|
+
return {key: to_json_value(value) for key, value in obj.items()}
|
|
135
|
+
if isinstance(obj, t.TranscriptContext):
|
|
136
|
+
return {"messages": to_json_value(obj.messages)}
|
|
137
|
+
if dataclasses.is_dataclass(obj) and not isinstance(obj, type):
|
|
138
|
+
result: dict[str, Any] = {}
|
|
139
|
+
discriminator = _discriminator(obj)
|
|
140
|
+
if discriminator is not None:
|
|
141
|
+
result[discriminator[0]] = discriminator[1]
|
|
142
|
+
for name, key in _fields(type(obj)):
|
|
143
|
+
value = getattr(obj, name)
|
|
144
|
+
if value is None:
|
|
145
|
+
continue
|
|
146
|
+
if name in _RAW_FIELDS:
|
|
147
|
+
if name == "parameters":
|
|
148
|
+
from .utils.schema import parameters_schema
|
|
149
|
+
|
|
150
|
+
value = parameters_schema(value)
|
|
151
|
+
result[key] = _raw_json(value)
|
|
152
|
+
else:
|
|
153
|
+
result[key] = to_json_value(value)
|
|
154
|
+
return result
|
|
155
|
+
if isinstance(obj, set | frozenset):
|
|
156
|
+
return [to_json_value(item) for item in obj]
|
|
157
|
+
if isinstance(obj, BaseException):
|
|
158
|
+
return str(obj)
|
|
159
|
+
model_dump = getattr(obj, "model_dump", None)
|
|
160
|
+
if callable(model_dump): # pydantic instances in tool details
|
|
161
|
+
return model_dump(mode="json")
|
|
162
|
+
raise TypeError(f"Object of type {type(obj).__name__} is not JSON serializable")
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _raw_json(value: Any) -> Any:
|
|
166
|
+
if isinstance(value, (dict, list, str, int, float, bool)) or value is None:
|
|
167
|
+
if isinstance(value, dict):
|
|
168
|
+
return {key: _raw_json(item) for key, item in value.items()}
|
|
169
|
+
if isinstance(value, list):
|
|
170
|
+
return [_raw_json(item) for item in value]
|
|
171
|
+
return value
|
|
172
|
+
return to_json_value(value)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def dumps(obj: Any, *, indent: int | None = None) -> str:
|
|
176
|
+
"""Serialize a data class (context, message, model, ...) to a pi-compatible JSON string."""
|
|
177
|
+
return json.dumps(to_json_value(obj), ensure_ascii=False, indent=indent, separators=None if indent else (",", ":"))
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
# ---------------------------------------------------------------------------
|
|
181
|
+
# Decoding
|
|
182
|
+
# ---------------------------------------------------------------------------
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _build(cls: type[T], data: Mapping[str, Any], *, nested: Mapping[str, Callable[[Any], Any]] | None = None) -> T:
|
|
186
|
+
kwargs: dict[str, Any] = {}
|
|
187
|
+
for name, key in _fields(cls):
|
|
188
|
+
if key in data:
|
|
189
|
+
value = data[key]
|
|
190
|
+
elif name in data:
|
|
191
|
+
value = data[name]
|
|
192
|
+
else:
|
|
193
|
+
continue
|
|
194
|
+
if value is None:
|
|
195
|
+
continue
|
|
196
|
+
decoder = nested.get(name) if nested else None
|
|
197
|
+
if decoder is not None:
|
|
198
|
+
value = decoder(value)
|
|
199
|
+
kwargs[name] = value
|
|
200
|
+
return cls(**kwargs)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _usage_cost(data: Any) -> t.UsageCost:
|
|
204
|
+
return data if isinstance(data, t.UsageCost) else _build(t.UsageCost, data)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _usage(data: Any) -> t.Usage:
|
|
208
|
+
if isinstance(data, t.Usage):
|
|
209
|
+
return data
|
|
210
|
+
return _build(t.Usage, data, nested={"cost": _usage_cost})
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
_CONTENT_TYPES: dict[str, type] = {
|
|
214
|
+
"text": t.TextContent,
|
|
215
|
+
"thinking": t.ThinkingContent,
|
|
216
|
+
"image": t.ImageContent,
|
|
217
|
+
"toolCall": t.ToolCall,
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def content_from_dict(data: Any) -> Any:
|
|
222
|
+
"""Decode one content block (``{"type": "text", "text": ...}`` etc.)."""
|
|
223
|
+
if not isinstance(data, Mapping):
|
|
224
|
+
return data
|
|
225
|
+
cls = _CONTENT_TYPES.get(data.get("type", ""))
|
|
226
|
+
if cls is None:
|
|
227
|
+
return dict(data)
|
|
228
|
+
return _build(cls, data)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _content_list(data: Any) -> Any:
|
|
232
|
+
if isinstance(data, str) or data is None:
|
|
233
|
+
return data if data is not None else []
|
|
234
|
+
return [content_from_dict(item) for item in data]
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def tool_from_dict(data: Any) -> t.Tool:
|
|
238
|
+
if isinstance(data, t.Tool):
|
|
239
|
+
return data
|
|
240
|
+
return _build(t.Tool, data)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _tool_reference(data: Any) -> t.ToolReference:
|
|
244
|
+
return data if isinstance(data, t.ToolReference) else t.ToolReference(data["name"])
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _diagnostic(data: Any) -> Any:
|
|
248
|
+
if not isinstance(data, Mapping):
|
|
249
|
+
return data
|
|
250
|
+
return _build(
|
|
251
|
+
AssistantMessageDiagnostic,
|
|
252
|
+
data,
|
|
253
|
+
nested={"error": lambda value: _build(DiagnosticErrorInfo, value) if isinstance(value, Mapping) else value},
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def _deferred(data: Any) -> Any:
|
|
258
|
+
return _build(t.DeferredHandle, data) if isinstance(data, Mapping) else data
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _nested_calls(data: Any) -> Any:
|
|
262
|
+
if not isinstance(data, Mapping):
|
|
263
|
+
return data
|
|
264
|
+
return _build(
|
|
265
|
+
t.NestedToolCalls,
|
|
266
|
+
data,
|
|
267
|
+
nested={"calls": _decode_list(lambda item: _build(t.NestedToolCallRecord, item))},
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
_MESSAGE_NESTED: dict[str, dict[str, Callable[[Any], Any]]] = {
|
|
272
|
+
"system": {
|
|
273
|
+
"content": lambda value: value if isinstance(value, str) else _content_list(value),
|
|
274
|
+
"tools_added": _decode_list(tool_from_dict),
|
|
275
|
+
"tools_removed": _decode_list(_tool_reference),
|
|
276
|
+
},
|
|
277
|
+
"user": {"content": _content_list},
|
|
278
|
+
"assistant": {
|
|
279
|
+
"content": _content_list,
|
|
280
|
+
"usage": _usage,
|
|
281
|
+
"diagnostics": _decode_list(_diagnostic),
|
|
282
|
+
"deferred": _deferred,
|
|
283
|
+
},
|
|
284
|
+
"toolResult": {
|
|
285
|
+
"content": _content_list,
|
|
286
|
+
"usage": _usage,
|
|
287
|
+
"nested_calls": _nested_calls,
|
|
288
|
+
},
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
_MESSAGE_TYPES: dict[str, type] = {
|
|
292
|
+
"system": t.SystemMessage,
|
|
293
|
+
"user": t.UserMessage,
|
|
294
|
+
"assistant": t.AssistantMessage,
|
|
295
|
+
"toolResult": t.ToolResultMessage,
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def message_from_dict(data: Any) -> Any:
|
|
300
|
+
"""Decode a message by its ``role``. Unknown roles (custom agent messages) are returned unchanged."""
|
|
301
|
+
if not isinstance(data, Mapping):
|
|
302
|
+
return data
|
|
303
|
+
role = data.get("role")
|
|
304
|
+
cls = _MESSAGE_TYPES.get(role) if isinstance(role, str) else None
|
|
305
|
+
if cls is None:
|
|
306
|
+
return data
|
|
307
|
+
return _build(cls, data, nested=_MESSAGE_NESTED[role]) # type: ignore[index]
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def messages_from_list(data: list[Any]) -> list[Any]:
|
|
311
|
+
return [message_from_dict(item) for item in data]
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def _cost(data: Any) -> t.ModelCost:
|
|
315
|
+
if isinstance(data, t.ModelCost):
|
|
316
|
+
return data
|
|
317
|
+
return _build(t.ModelCost, data, nested={"tiers": _decode_list(lambda tier: _build(t.ModelCostTier, tier))})
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def _input_limits(data: Any) -> Any:
|
|
321
|
+
if not isinstance(data, Mapping):
|
|
322
|
+
return data
|
|
323
|
+
|
|
324
|
+
def images(value: Any) -> Any:
|
|
325
|
+
if not isinstance(value, Mapping):
|
|
326
|
+
return value
|
|
327
|
+
return _build(
|
|
328
|
+
t.ModelImageInputLimits,
|
|
329
|
+
value,
|
|
330
|
+
nested={"resize": lambda r: _build(t.ModelImageResizeOptions, r) if isinstance(r, Mapping) else r},
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
return _build(t.ModelInputLimits, data, nested={"images": images})
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
_MODEL_NESTED: dict[str, Callable[[Any], Any]] = {"cost": _cost, "input_limits": _input_limits}
|
|
337
|
+
_MODEL_TYPES: dict[str, type] = {"chat": t.Model, "image": t.ImageModel, "classifier": t.ClassifierModel}
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def model_from_dict(data: Any) -> Any:
|
|
341
|
+
"""Decode a catalog entry. A missing ``type`` means a chat model."""
|
|
342
|
+
if not isinstance(data, Mapping):
|
|
343
|
+
return data
|
|
344
|
+
cls = _MODEL_TYPES.get(data.get("type") or "chat")
|
|
345
|
+
if cls is None:
|
|
346
|
+
raise ValueError(f"Unknown model type: {data.get('type')!r}")
|
|
347
|
+
return _build(cls, data, nested=_MODEL_NESTED)
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
_EVENT_TYPES: dict[str, type] = {
|
|
351
|
+
cls.type: cls # type: ignore[attr-defined]
|
|
352
|
+
for cls in (
|
|
353
|
+
t.StartEvent,
|
|
354
|
+
t.TextStartEvent,
|
|
355
|
+
t.TextDeltaEvent,
|
|
356
|
+
t.TextEndEvent,
|
|
357
|
+
t.ThinkingStartEvent,
|
|
358
|
+
t.ThinkingDeltaEvent,
|
|
359
|
+
t.ThinkingEndEvent,
|
|
360
|
+
t.ToolCallStartEvent,
|
|
361
|
+
t.ToolCallDeltaEvent,
|
|
362
|
+
t.ToolCallEndEvent,
|
|
363
|
+
t.DoneEvent,
|
|
364
|
+
t.ErrorEvent,
|
|
365
|
+
)
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def event_from_dict(data: Mapping[str, Any]) -> Any:
|
|
370
|
+
cls = _EVENT_TYPES[data["type"]]
|
|
371
|
+
return _build(
|
|
372
|
+
cls,
|
|
373
|
+
data,
|
|
374
|
+
nested={
|
|
375
|
+
"partial": message_from_dict,
|
|
376
|
+
"message": message_from_dict,
|
|
377
|
+
"error": message_from_dict,
|
|
378
|
+
"tool_call": content_from_dict,
|
|
379
|
+
},
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def context_from_dict(data: Mapping[str, Any]) -> t.Context:
|
|
384
|
+
tools = data.get("tools")
|
|
385
|
+
return t.Context(
|
|
386
|
+
messages=messages_from_list(list(data.get("messages") or [])),
|
|
387
|
+
system_prompt=data.get("systemPrompt", data.get("system_prompt")),
|
|
388
|
+
tools=[tool_from_dict(tool) for tool in tools] if tools else None,
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def context_to_dict(context: t.Context | t.TranscriptContext) -> dict[str, Any]:
|
|
393
|
+
return to_json_value(context)
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
_QUESTION_TYPES = {
|
|
397
|
+
"choice": t.ClassifierChoiceQuestion,
|
|
398
|
+
"score": t.ClassifierScoreQuestion,
|
|
399
|
+
"bool": t.ClassifierBoolQuestion,
|
|
400
|
+
}
|
|
401
|
+
_ANSWER_TYPES = {"choice": t.ClassifierChoiceAnswer, "score": t.ClassifierScoreAnswer, "bool": t.ClassifierBoolAnswer}
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def classifier_question_from_dict(data: Any) -> Any:
|
|
405
|
+
if not isinstance(data, Mapping):
|
|
406
|
+
return data
|
|
407
|
+
return _build(_QUESTION_TYPES[data["type"]], data)
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def classifier_answer_from_dict(data: Any) -> Any:
|
|
411
|
+
if not isinstance(data, Mapping):
|
|
412
|
+
return data
|
|
413
|
+
return _build(_ANSWER_TYPES[data["type"]], data)
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
_DECODERS: dict[type, Callable[[Any], Any]] = {
|
|
417
|
+
t.Context: context_from_dict,
|
|
418
|
+
t.Model: model_from_dict,
|
|
419
|
+
t.ImageModel: model_from_dict,
|
|
420
|
+
t.ClassifierModel: model_from_dict,
|
|
421
|
+
t.Tool: tool_from_dict,
|
|
422
|
+
t.Usage: _usage,
|
|
423
|
+
t.ModelCost: _cost,
|
|
424
|
+
t.DeferredHandle: _deferred,
|
|
425
|
+
t.SystemMessage: message_from_dict,
|
|
426
|
+
t.UserMessage: message_from_dict,
|
|
427
|
+
t.AssistantMessage: message_from_dict,
|
|
428
|
+
t.ToolResultMessage: message_from_dict,
|
|
429
|
+
t.TextContent: content_from_dict,
|
|
430
|
+
t.ThinkingContent: content_from_dict,
|
|
431
|
+
t.ImageContent: content_from_dict,
|
|
432
|
+
t.ToolCall: content_from_dict,
|
|
433
|
+
t.ImagesContext: lambda data: t.ImagesContext([content_from_dict(item) for item in data.get("input", [])]),
|
|
434
|
+
t.ClassifierContext: lambda data: t.ClassifierContext(
|
|
435
|
+
state=data["state"],
|
|
436
|
+
questions={name: classifier_question_from_dict(q) for name, q in data.get("questions", {}).items()},
|
|
437
|
+
images=[content_from_dict(image) for image in data["images"]] if data.get("images") else None,
|
|
438
|
+
),
|
|
439
|
+
t.ClassifierResult: lambda data: _build(
|
|
440
|
+
t.ClassifierResult,
|
|
441
|
+
data,
|
|
442
|
+
nested={
|
|
443
|
+
"answers": lambda answers: {name: classifier_answer_from_dict(a) for name, a in answers.items()},
|
|
444
|
+
"usage": _usage,
|
|
445
|
+
},
|
|
446
|
+
),
|
|
447
|
+
t.AssistantImages: lambda data: _build(t.AssistantImages, data, nested={"output": _content_list, "usage": _usage}),
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def from_dict(cls: type[T] | str, data: Any) -> T:
|
|
452
|
+
"""Decode pi JSON into data class ``cls``.
|
|
453
|
+
|
|
454
|
+
``cls`` may also be ``"message"``, ``"content"``, ``"model"``, ``"event"`` or
|
|
455
|
+
``"context"`` to decode by discriminator.
|
|
456
|
+
"""
|
|
457
|
+
if isinstance(cls, str):
|
|
458
|
+
decoder = {
|
|
459
|
+
"message": message_from_dict,
|
|
460
|
+
"content": content_from_dict,
|
|
461
|
+
"model": model_from_dict,
|
|
462
|
+
"event": event_from_dict,
|
|
463
|
+
"context": context_from_dict,
|
|
464
|
+
"messages": messages_from_list,
|
|
465
|
+
}[cls]
|
|
466
|
+
return decoder(data)
|
|
467
|
+
decoder = _DECODERS.get(cls)
|
|
468
|
+
if decoder is not None:
|
|
469
|
+
return decoder(data)
|
|
470
|
+
if dataclasses.is_dataclass(cls):
|
|
471
|
+
return _build(cls, data) # type: ignore[return-value]
|
|
472
|
+
raise TypeError(f"Cannot decode {cls!r}")
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
def loads(text: str | bytes, cls: type[T] | str = "context") -> T:
|
|
476
|
+
"""Parse a JSON string produced by :func:`dumps` (or TypeScript pi) into ``cls``."""
|
|
477
|
+
return from_dict(cls, json.loads(text))
|