pi-python-core 0.8.1__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.
@@ -0,0 +1,167 @@
1
+ """System replay adapted from Pi a13d35a (MIT); see NOTICE."""
2
+
3
+ from copy import deepcopy
4
+ from .messages import Message, SystemMessage, TextContent, ToolDeclaration
5
+
6
+
7
+ def system_text(content: str | list[TextContent]) -> str:
8
+ """Pi's contentText: text blocks of one message joined by a single newline."""
9
+ return content if isinstance(content, str) else "\n".join(b.text for b in content)
10
+
11
+
12
+ def system_message_text(message: SystemMessage) -> str:
13
+ """A complete prompt: content followed by the sections that are set."""
14
+ parts = [system_text(message.content), *(v for v in message.sections.values() if v is not None)]
15
+ return "\n\n".join(p for p in parts if p)
16
+
17
+
18
+ def render_system_update(message: SystemMessage) -> str:
19
+ """A later system message as sent in place; section changes are framed by name."""
20
+ parts = [system_text(message.content)] if system_text(message.content) else []
21
+ for name, value in message.sections.items():
22
+ parts.append(
23
+ f'Removed system prompt section "{name}".'
24
+ if value is None
25
+ else f'Updated system prompt section "{name}":\n\n{value}'
26
+ )
27
+ return "\n\n".join(parts)
28
+
29
+
30
+ def current_tools(messages: list[Message]) -> list[ToolDeclaration]:
31
+ tools: dict[str, ToolDeclaration] = {}
32
+ for m in messages:
33
+ if isinstance(m, SystemMessage):
34
+ for name in m.tools_removed:
35
+ tools.pop(name, None)
36
+ for tool in m.tools_added:
37
+ tools[tool.name] = tool
38
+ return deepcopy(list(tools.values()))
39
+
40
+
41
+ def current_system_message(messages: list[Message]) -> SystemMessage | None:
42
+ systems = [m for m in messages if isinstance(m, SystemMessage)]
43
+ if not systems:
44
+ return None
45
+ sections: dict[str, str | None] = {}
46
+ for m in systems:
47
+ for name, text in m.sections.items():
48
+ if text is None:
49
+ sections.pop(name, None)
50
+ else:
51
+ sections[name] = text
52
+ return SystemMessage(
53
+ "\n\n".join(system_text(m.content) for m in systems if system_text(m.content)),
54
+ sections,
55
+ current_tools(messages),
56
+ timestamp=systems[0].timestamp,
57
+ )
58
+
59
+
60
+ def current_system_prompt(messages: list[Message]) -> str:
61
+ m = current_system_message(messages)
62
+ return system_message_text(m) if m else ""
63
+
64
+
65
+ def initial_system_message(messages: list[Message]) -> SystemMessage | None:
66
+ return messages[0] if messages and isinstance(messages[0], SystemMessage) else None
67
+
68
+
69
+ def collapse_system_messages(messages: list[Message]) -> list[Message]:
70
+ """For APIs without mid-conversation system messages: one replayed leading message."""
71
+ head = current_system_message(messages)
72
+ rest: list[Message] = [deepcopy(m) for m in messages if not isinstance(m, SystemMessage)]
73
+ return [head, *rest] if head else rest
74
+
75
+
76
+ def resolve_transcript(messages: list[Message], mid_conversation: bool) -> list[Message]:
77
+ return deepcopy(messages) if mid_conversation else collapse_system_messages(messages)
78
+
79
+
80
+ def declared_tools(messages: list[Message]) -> list[ToolDeclaration]:
81
+ """Every definition referenced by transcript tool state, in first-declaration order."""
82
+ tools: dict[str, ToolDeclaration] = {}
83
+ for m in messages:
84
+ if isinstance(m, SystemMessage):
85
+ for tool in m.tools_added:
86
+ tools[tool.name] = tool
87
+ return deepcopy(list(tools.values()))
88
+
89
+
90
+ def has_tool_redefinitions(messages: list[Message]) -> bool:
91
+ """A name declared twice with different definitions cannot be referenced by name."""
92
+ seen: dict[str, ToolDeclaration] = {}
93
+ for m in messages:
94
+ if isinstance(m, SystemMessage):
95
+ for tool in m.tools_added:
96
+ if tool.name in seen and seen[tool.name] != tool:
97
+ return True
98
+ seen[tool.name] = tool
99
+ return False
100
+
101
+
102
+ def has_non_additive_tool_changes(messages: list[Message]) -> bool:
103
+ """A removal or same-name redeclaration that an addition-only transport cannot replay."""
104
+ seen: set[str] = set()
105
+ for m in messages:
106
+ if isinstance(m, SystemMessage):
107
+ if m.tools_removed:
108
+ return True
109
+ for tool in m.tools_added:
110
+ if tool.name in seen:
111
+ return True
112
+ seen.add(tool.name)
113
+ return False
114
+
115
+
116
+ def resolve_transcript_tools(
117
+ messages: list[Message], supports_additions: bool
118
+ ) -> tuple[list[ToolDeclaration], bool]:
119
+ """Top-level request tools, and whether later additions are anchored in place."""
120
+ anchors = supports_additions and not has_non_additive_tool_changes(messages)
121
+ initial = initial_system_message(messages)
122
+ tools = (
123
+ (deepcopy(initial.tools_added) if initial else []) if anchors else current_tools(messages)
124
+ )
125
+ return tools, anchors
126
+
127
+
128
+ def with_request_tools(messages: list[Message], tools: list[ToolDeclaration]) -> list[Message]:
129
+ """Pi's normalizeContext: fold request-level tools into the leading system message.
130
+
131
+ Requests built by Agent already carry their tools in the transcript; this covers a
132
+ direct Provider call that passes `tools` without declaring them in system messages.
133
+ """
134
+ if not tools or any(isinstance(m, SystemMessage) and m.tools_added for m in messages):
135
+ return deepcopy(messages)
136
+ messages = deepcopy(messages)
137
+ head = initial_system_message(messages)
138
+ if head is None:
139
+ return [SystemMessage(tools_added=deepcopy(tools), timestamp=0), *messages]
140
+ head.tools_added = deepcopy(tools)
141
+ return messages
142
+
143
+
144
+ def declare_tool_changes(
145
+ history: list[Message], pending: list[Message], tools: list[ToolDeclaration]
146
+ ) -> list[Message]:
147
+ pending = deepcopy(pending)
148
+ systems = [i for i, m in enumerate(pending) if isinstance(m, SystemMessage)]
149
+ index = systems[-1] if systems else None
150
+ if index is not None:
151
+ m = pending[index]
152
+ assert isinstance(m, SystemMessage)
153
+ m.tools_added, m.tools_removed = [], []
154
+ previous = {t.name: t for t in current_tools(history + pending)}
155
+ current = {t.name: t for t in tools}
156
+ added = [t for t in tools if previous.get(t.name) != t]
157
+ removed = [name for name, t in previous.items() if current.get(name) != t]
158
+ if index is not None:
159
+ m = pending[index]
160
+ assert isinstance(m, SystemMessage)
161
+ m.tools_added, m.tools_removed = deepcopy(added), removed
162
+ elif added or removed:
163
+ index = next(
164
+ (i for i, m in enumerate(pending) if not isinstance(m, SystemMessage)), len(pending)
165
+ )
166
+ pending.insert(index, SystemMessage(tools_added=deepcopy(added), tools_removed=removed))
167
+ return pending
@@ -0,0 +1,119 @@
1
+ Metadata-Version: 2.4
2
+ Name: pi-python-core
3
+ Version: 0.8.1
4
+ Summary: An embeddable asyncio agent core with opt-in Claude and OpenAI providers
5
+ License-Expression: MIT
6
+ License-File: LICENSE
7
+ License-File: NOTICE
8
+ Classifier: Framework :: AsyncIO
9
+ Classifier: Operating System :: OS Independent
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3 :: Only
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Programming Language :: Python :: 3.14
16
+ Classifier: Programming Language :: Python :: Free Threading :: 2 - Beta
17
+ Classifier: Programming Language :: Python :: Implementation :: CPython
18
+ Classifier: Programming Language :: Python :: Implementation :: PyPy
19
+ Classifier: Typing :: Typed
20
+ Requires-Python: >=3.11
21
+ Requires-Dist: jsonschema<5,>=4.18
22
+ Provides-Extra: mcp
23
+ Requires-Dist: mcp>=1.10; extra == 'mcp'
24
+ Provides-Extra: providers
25
+ Requires-Dist: httpx<1,>=0.27; extra == 'providers'
26
+ Requires-Dist: pyjwt[crypto]<3,>=2.8; extra == 'providers'
27
+ Requires-Dist: websockets>=14.2; extra == 'providers'
28
+ Description-Content-Type: text/markdown
29
+
30
+ # pi-python-core
31
+
32
+ 一个可嵌入的 Python agent 核心,移植自 [Pi](https://github.com/earendil-works/pi)(固定参照 `v1.0.0`)。它负责一件事:把对话发给模型,执行模型要求的工具,把结果交回模型,直到得到回答。工具就是普通的 Python 函数;模型可以是 Claude、GPT、DeepSeek,也可以是本机或集群上的开源模型。
33
+
34
+ PyPI 上的 `pi-agent-core` 是另一个独立项目,移植的是 2026 年初 pi-mono 中的旧版本。本库对照 Pi v1.0.0 的行为,与上游的实际运行结果逐组比较;并自带 Claude、OpenAI、DeepSeek 和本地模型的接入,不需要安装任何模型 SDK。
35
+
36
+ ## 安装
37
+
38
+ 支持 Python 3.11–3.14(包括无 GIL 的 3.14t)和 PyPy 3.11,不需要 Node 或任何模型 SDK。
39
+
40
+ ```bash
41
+ pip install 'pi-python-core[providers]' # 或 uv add 'pi-python-core[providers]'
42
+ ```
43
+
44
+ 安装名是 `pi-python-core`,导入名是 `pi_python`。
45
+
46
+ | 安装选项 | 带来什么 |
47
+ |---|---|
48
+ | 不加选项 | 执行核心,只依赖 `jsonschema` |
49
+ | `[providers]` | 真实模型接入:Claude、OpenAI、Codex、DeepSeek,以及任何 OpenAI 兼容服务(Ollama、vLLM、llama.cpp 等) |
50
+ | `[mcp]` | 把 MCP 服务器的工具交给 agent |
51
+
52
+ 依赖写的是版本范围而不是固定版本,能和大多数已有环境共存。
53
+
54
+ ## 五分钟上手
55
+
56
+ ```python
57
+ from pi_python import Agent, tool
58
+ from pi_python.providers import AnthropicProvider
59
+
60
+ @tool
61
+ def word_count(text: str) -> int:
62
+ """Count the words in a text."""
63
+ return len(text.split())
64
+
65
+ agent = Agent(provider=AnthropicProvider(api_key="..."), model="claude-sonnet-4-5", tools=[word_count])
66
+ result = agent.prompt_sync("How many words are in 'to be or not to be'?")
67
+ print(result.messages[-1].content[0].text)
68
+ ```
69
+
70
+ `@tool` 从函数签名和文档字符串生成工具;普通函数和 `async` 函数都可以。在异步程序里用 `await agent.prompt(...)`。换成本地模型只改两行:
71
+
72
+ ```python
73
+ from pi_python.providers import OpenAICompletionsProvider
74
+
75
+ llm = OpenAICompletionsProvider(base_url="http://localhost:11434/v1", name="ollama")
76
+ agent = Agent(provider=llm, model=llm.model("qwen3:8b", context_window=40960), tools=[word_count])
77
+ ```
78
+
79
+ 不联网也能先跑起来:`python examples/quickstart.py`。
80
+
81
+ ## 能做什么
82
+
83
+ | 需要 | 怎么做 | 示例 |
84
+ |---|---|---|
85
+ | 写工具 | `@tool` 装饰普通函数;pydantic 模型、dataclass、枚举、日期等参数自动转换;也接受手写或 MCP 生成的 JSON Schema | [quickstart](examples/quickstart.py) |
86
+ | 接模型 | Claude 与 GPT 的 API key 或订阅登录;DeepSeek;任何 OpenAI 兼容服务 | [local_model](examples/local_model.py),[provider_chat](examples/provider_chat.py) |
87
+ | 用 MCP 工具 | `async with connect_stdio(...) as tools` | [mcp_tools](examples/mcp_tools.py) |
88
+ | 让一个 agent 调用另一个 | 把子 agent 包成工具,取消会一路传下去 | [subagent](examples/subagent.py) |
89
+ | 中途干预 | `steer` 插入指导,`follow_up` 排队后续任务,`abort` 随时取消(任何线程都可以调用) | |
90
+ | 上下文满了、服务出错 | `is_context_overflow`、`is_retryable_error` 判断原因,`continue_run()` 重试,`transform_context` 压缩 | [recovery](examples/recovery.py) |
91
+ | 保存与恢复对话 | `encode_messages` / `decode_messages`,应用决定存在哪里 | [save_restore](examples/save_restore.py) |
92
+ | 观察与审计 | 订阅事件;执行前后的钩子可以阻止、改写工具调用 | |
93
+
94
+ 除 `provider_chat` 需要真实凭据外,示例都能离线运行,不需要 API key;测试会逐个运行它们。
95
+
96
+ ## 与 Pi 的关系
97
+
98
+ 执行循环、事件顺序、钩子、队列、出错与取消的处理都与 Pi 一致,并用差分验证:同一组输入分别交给固定版本的上游代码和本库运行,逐项比较模型请求、工具调用、事件和最终记录。目前核心循环 25 组、模型接入 50 组、WebSocket 多轮 2 组、出错判断 44 条样例全部一致。
99
+
100
+ 有几处是有意的差异,例如工具参数严格校验、不自动转换类型,返回给调用者的状态是副本;另一些是为 Python 用户加的,例如 `@tool`、同步调用、失败后直接 `continue_run()`。逐项记录见[验收映射](compat/COVERAGE.md)。Pi 放在应用层的功能(终端界面、会话文件格式、上下文压缩)不在本库核心里;压缩可以用钩子实现,示例里有完整做法。本项目使用自己的版本号,并非 Pi 官方发行版。
101
+
102
+ ## 文档
103
+
104
+ - [一页看懂:五个概念和一轮的流程](docs/CONCEPTS.md)
105
+ - [公开 API](docs/API.md)
106
+ - [模型接入、订阅登录与本地模型](docs/PROVIDERS.md)
107
+ - [实施与验证结果](docs/IMPLEMENTATION.md)
108
+ - [与 Pi 的逐项对照和有意差异](compat/COVERAGE.md)
109
+ - [参照重建与候选版本验证](reference/README.md)
110
+
111
+ ## 开发
112
+
113
+ ```bash
114
+ uv sync --locked --extra providers --extra mcp
115
+ uv run pytest -q
116
+ uv run python scripts/verify.py # 全部检查,含与上游的差分(需要 Node)
117
+ ```
118
+
119
+ CI 配置在 `.github/workflows/ci.yml`,每次推送都在 GitHub Actions 上运行,覆盖 Linux 上的各个 Python 版本(含 3.14t 和 PyPy)以及 macOS 和 Windows。
@@ -0,0 +1,39 @@
1
+ pi_python/__init__.py,sha256=jmQw2LTIjdt9Sn3lTAaAUAvgDrNqfZ1rDGvccAAuPOY,3751
2
+ pi_python/_version.py,sha256=Ocl79hbbH8_jdr5dGC90VR1cAvZc05Rc0tkZttUnMjo,22
3
+ pi_python/agent.py,sha256=eFbmkO5prBbWssaIycgTRWd4VRJ6I3FJMDU414kpL1w,15264
4
+ pi_python/cancellation.py,sha256=DoQVdX7DcQdVLkl4OvhcpMhKZJEW0Kscl2-xxF6i6Y0,608
5
+ pi_python/errors.py,sha256=65lT0nYfzqxRfUlFG-Y9vgYxfDHEuFiBfgqmFnX9_O8,675
6
+ pi_python/estimate.py,sha256=dQQJ9UEGp57F3wO_CcU2oubgPe841ttgpUseK3Fu_0w,5048
7
+ pi_python/events.py,sha256=t05VE-km2ZcsdIXCviVzNFQCob4xBghDfNeBRWpLrCA,4594
8
+ pi_python/function_tools.py,sha256=MQgNwz6PgT2gnbXRe0-SNVLnedkui4jaclL9dGOZa1s,17633
9
+ pi_python/hooks.py,sha256=jm6ep78NjdHxIPZvcOXTZIPxkfBMkffVTNzLvnHS9mM,1321
10
+ pi_python/limits.py,sha256=K1aP1cFH4fVhtWIXqpWMmpkB9AI0wK-bz3Xd2hxnOs4,1157
11
+ pi_python/loop.py,sha256=H9UK8XqvnTFC8JgKoziqqD4s7xOZy-xPciTqaSiP_9c,17622
12
+ pi_python/lowlevel.py,sha256=56wtwqD0Ur1AGAXGa7zclb1IpmRPFpSEss_nspkAdrI,5514
13
+ pi_python/mcp.py,sha256=Nn0cDdNswsCFP-HXvDXiudMYtQvWEyt9bh6kFR028EQ,8084
14
+ pi_python/messages.py,sha256=w5e-lc9DLW_4vYZ73Wj32RXfNYzDHdp4HCYsF5qkfHY,15665
15
+ pi_python/models.py,sha256=IejtaWvbURM9vv_2kkoDzfto2TR0OSLjuQeOh3IJZ1k,6094
16
+ pi_python/provider.py,sha256=k_VfJu5WOQla_LkglTDAq5s-77ejCj4YRTyhJ8onZBo,4259
17
+ pi_python/proxy.py,sha256=kpEUoE1oDkJW7VVaV72slN3uJUFgYR4Q6jtYOEe8LmQ,12159
18
+ pi_python/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
19
+ pi_python/queues.py,sha256=nQ5xRSA3MMOHMs_ZJnG9sT3-d_yKn72oSnxXqiKPrcI,3032
20
+ pi_python/recovery.py,sha256=z5ht-6FoKtC5BPEaSgXiRE2KVf94mZDiKMSxV0mkZ-4,8138
21
+ pi_python/run.py,sha256=gR4udFeg1oFSh8CNkS5O-DCoOHrn6vTnaMsGH3nWbSs,17702
22
+ pi_python/stream.py,sha256=R3v-qjCHFQJhHQk83qATz7lC3yr8paJguaHwk8RoHow,10729
23
+ pi_python/sync.py,sha256=HXr6sh9fkom6SD3rSfBzM-jg2IKkim-QifCUP9nJMW0,2557
24
+ pi_python/testing.py,sha256=XfTSOkEv0CenqrBILI_T205jOhyitW53YQZ9yiMXir4,1046
25
+ pi_python/tools.py,sha256=7hpquuOoVeRAhVxlYOXsooFvTcXEgfVkVBrTjvMbqa8,20503
26
+ pi_python/transcript.py,sha256=g6Tg8eiyMCAK43iLpcBv220MHBq8kO7Ty-6OYLCzw2A,6685
27
+ pi_python/data/models.json,sha256=oa_LGv7vaRrS-l7_Z7iu8f7WTyoJJLPhUQyptUz7OmE,77549
28
+ pi_python/providers/__init__.py,sha256=6oB-gHkWn5o_01sLRKUnss-uTMcuyyzie-uSF1cPRdk,673
29
+ pi_python/providers/anthropic.py,sha256=35ZwPGh2VUg-86iNROyHz5MAv1bYZB2XGLR-DHYELAY,30204
30
+ pi_python/providers/common.py,sha256=z8dB2pIc-dRscPAppnA3ZwF_wJygunQb9pI9Wl4CvaA,8051
31
+ pi_python/providers/completions.py,sha256=7tFrS_j2u6mV90mSE8uZYZDfj6DqRVxJXbIrUDay3mg,50289
32
+ pi_python/providers/oauth.py,sha256=_xRiqxG3PIhxgrUYDoI9iKDEnXA12DQS6fPFBwSn8rQ,22822
33
+ pi_python/providers/openai.py,sha256=36KBHaFUgM_QGKBSTxVxAfroCzmcoh5DPKuX667WmLs,30112
34
+ pi_python/providers/transport.py,sha256=R9AbUmlTXwuGXJEv9Q1joZrTJAd1shxnz7Uxwuzl0nA,21990
35
+ pi_python_core-0.8.1.dist-info/METADATA,sha256=SEo2GGTCg7wTUWqDc95QtzmQQqEKKwlNNuLE76ujxow,6437
36
+ pi_python_core-0.8.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
37
+ pi_python_core-0.8.1.dist-info/licenses/LICENSE,sha256=BFf1vOw7OyEWBd-10aSQQv1jjzaGpBD-CZwkolrxPEg,1069
38
+ pi_python_core-0.8.1.dist-info/licenses/NOTICE,sha256=fJna16fm1ECHN7zVZhtyNmjLw8KGxGp8mL9blISF1K0,633
39
+ pi_python_core-0.8.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Mario Zechner
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,8 @@
1
+ pi-python-core is an independent Python implementation, not an official Pi release.
2
+ Agent execution and transcript behavior are adapted from earendil-works/pi:
3
+ https://github.com/earendil-works/pi/tree/a13d35a742c6ef8462812a28fbe1d8c8b7431c32
4
+ Copyright (c) 2025 Mario Zechner. MIT license; see LICENSE.
5
+ The complete upstream source is fetched only for development under reference/pi.
6
+ Provider request conversion (Claude Messages, OpenAI and Codex Responses), the cached
7
+ WebSocket continuation, transcript replay helpers and the context estimate/shortHash in
8
+ src/pi_python/estimate.py are adapted from packages/ai of the same commit.