@elizaos/python 2.0.0-alpha.10
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.
- package/LICENSE +26 -0
- package/README.md +239 -0
- package/elizaos/__init__.py +280 -0
- package/elizaos/action_docs.py +149 -0
- package/elizaos/advanced_capabilities/__init__.py +85 -0
- package/elizaos/advanced_capabilities/actions/__init__.py +54 -0
- package/elizaos/advanced_capabilities/actions/add_contact.py +139 -0
- package/elizaos/advanced_capabilities/actions/follow_room.py +151 -0
- package/elizaos/advanced_capabilities/actions/image_generation.py +148 -0
- package/elizaos/advanced_capabilities/actions/mute_room.py +164 -0
- package/elizaos/advanced_capabilities/actions/remove_contact.py +145 -0
- package/elizaos/advanced_capabilities/actions/roles.py +207 -0
- package/elizaos/advanced_capabilities/actions/schedule_follow_up.py +154 -0
- package/elizaos/advanced_capabilities/actions/search_contacts.py +145 -0
- package/elizaos/advanced_capabilities/actions/send_message.py +187 -0
- package/elizaos/advanced_capabilities/actions/settings.py +151 -0
- package/elizaos/advanced_capabilities/actions/unfollow_room.py +164 -0
- package/elizaos/advanced_capabilities/actions/unmute_room.py +164 -0
- package/elizaos/advanced_capabilities/actions/update_contact.py +164 -0
- package/elizaos/advanced_capabilities/actions/update_entity.py +161 -0
- package/elizaos/advanced_capabilities/evaluators/__init__.py +18 -0
- package/elizaos/advanced_capabilities/evaluators/reflection.py +134 -0
- package/elizaos/advanced_capabilities/evaluators/relationship_extraction.py +203 -0
- package/elizaos/advanced_capabilities/providers/__init__.py +36 -0
- package/elizaos/advanced_capabilities/providers/agent_settings.py +60 -0
- package/elizaos/advanced_capabilities/providers/contacts.py +77 -0
- package/elizaos/advanced_capabilities/providers/facts.py +82 -0
- package/elizaos/advanced_capabilities/providers/follow_ups.py +113 -0
- package/elizaos/advanced_capabilities/providers/knowledge.py +83 -0
- package/elizaos/advanced_capabilities/providers/relationships.py +112 -0
- package/elizaos/advanced_capabilities/providers/roles.py +97 -0
- package/elizaos/advanced_capabilities/providers/settings.py +51 -0
- package/elizaos/advanced_capabilities/services/__init__.py +18 -0
- package/elizaos/advanced_capabilities/services/follow_up.py +138 -0
- package/elizaos/advanced_capabilities/services/rolodex.py +244 -0
- package/elizaos/advanced_memory/__init__.py +3 -0
- package/elizaos/advanced_memory/evaluators.py +97 -0
- package/elizaos/advanced_memory/memory_service.py +556 -0
- package/elizaos/advanced_memory/plugin.py +30 -0
- package/elizaos/advanced_memory/prompts.py +12 -0
- package/elizaos/advanced_memory/providers.py +90 -0
- package/elizaos/advanced_memory/types.py +65 -0
- package/elizaos/advanced_planning/__init__.py +10 -0
- package/elizaos/advanced_planning/actions.py +145 -0
- package/elizaos/advanced_planning/message_classifier.py +127 -0
- package/elizaos/advanced_planning/planning_service.py +712 -0
- package/elizaos/advanced_planning/plugin.py +40 -0
- package/elizaos/advanced_planning/prompts.py +4 -0
- package/elizaos/basic_capabilities/__init__.py +66 -0
- package/elizaos/basic_capabilities/actions/__init__.py +24 -0
- package/elizaos/basic_capabilities/actions/choice.py +140 -0
- package/elizaos/basic_capabilities/actions/ignore.py +66 -0
- package/elizaos/basic_capabilities/actions/none.py +56 -0
- package/elizaos/basic_capabilities/actions/reply.py +120 -0
- package/elizaos/basic_capabilities/providers/__init__.py +54 -0
- package/elizaos/basic_capabilities/providers/action_state.py +113 -0
- package/elizaos/basic_capabilities/providers/actions.py +263 -0
- package/elizaos/basic_capabilities/providers/attachments.py +76 -0
- package/elizaos/basic_capabilities/providers/capabilities.py +62 -0
- package/elizaos/basic_capabilities/providers/character.py +113 -0
- package/elizaos/basic_capabilities/providers/choice.py +73 -0
- package/elizaos/basic_capabilities/providers/context_bench.py +44 -0
- package/elizaos/basic_capabilities/providers/current_time.py +58 -0
- package/elizaos/basic_capabilities/providers/entities.py +99 -0
- package/elizaos/basic_capabilities/providers/evaluators.py +54 -0
- package/elizaos/basic_capabilities/providers/providers_list.py +55 -0
- package/elizaos/basic_capabilities/providers/recent_messages.py +85 -0
- package/elizaos/basic_capabilities/providers/time.py +45 -0
- package/elizaos/basic_capabilities/providers/world.py +93 -0
- package/elizaos/basic_capabilities/services/__init__.py +18 -0
- package/elizaos/basic_capabilities/services/embedding.py +122 -0
- package/elizaos/basic_capabilities/services/task.py +178 -0
- package/elizaos/bootstrap/__init__.py +12 -0
- package/elizaos/bootstrap/actions/__init__.py +68 -0
- package/elizaos/bootstrap/actions/add_contact.py +149 -0
- package/elizaos/bootstrap/actions/choice.py +147 -0
- package/elizaos/bootstrap/actions/follow_room.py +151 -0
- package/elizaos/bootstrap/actions/ignore.py +80 -0
- package/elizaos/bootstrap/actions/image_generation.py +135 -0
- package/elizaos/bootstrap/actions/mute_room.py +151 -0
- package/elizaos/bootstrap/actions/none.py +71 -0
- package/elizaos/bootstrap/actions/remove_contact.py +159 -0
- package/elizaos/bootstrap/actions/reply.py +140 -0
- package/elizaos/bootstrap/actions/roles.py +193 -0
- package/elizaos/bootstrap/actions/schedule_follow_up.py +164 -0
- package/elizaos/bootstrap/actions/search_contacts.py +159 -0
- package/elizaos/bootstrap/actions/send_message.py +173 -0
- package/elizaos/bootstrap/actions/settings.py +165 -0
- package/elizaos/bootstrap/actions/unfollow_room.py +151 -0
- package/elizaos/bootstrap/actions/unmute_room.py +151 -0
- package/elizaos/bootstrap/actions/update_contact.py +178 -0
- package/elizaos/bootstrap/actions/update_entity.py +175 -0
- package/elizaos/bootstrap/autonomy/__init__.py +18 -0
- package/elizaos/bootstrap/autonomy/action.py +197 -0
- package/elizaos/bootstrap/autonomy/providers.py +165 -0
- package/elizaos/bootstrap/autonomy/routes.py +171 -0
- package/elizaos/bootstrap/autonomy/service.py +562 -0
- package/elizaos/bootstrap/autonomy/types.py +18 -0
- package/elizaos/bootstrap/evaluators/__init__.py +19 -0
- package/elizaos/bootstrap/evaluators/reflection.py +118 -0
- package/elizaos/bootstrap/evaluators/relationship_extraction.py +192 -0
- package/elizaos/bootstrap/plugin.py +140 -0
- package/elizaos/bootstrap/providers/__init__.py +80 -0
- package/elizaos/bootstrap/providers/action_state.py +71 -0
- package/elizaos/bootstrap/providers/actions.py +256 -0
- package/elizaos/bootstrap/providers/agent_settings.py +63 -0
- package/elizaos/bootstrap/providers/attachments.py +76 -0
- package/elizaos/bootstrap/providers/capabilities.py +66 -0
- package/elizaos/bootstrap/providers/character.py +128 -0
- package/elizaos/bootstrap/providers/choice.py +77 -0
- package/elizaos/bootstrap/providers/contacts.py +78 -0
- package/elizaos/bootstrap/providers/context_bench.py +49 -0
- package/elizaos/bootstrap/providers/current_time.py +56 -0
- package/elizaos/bootstrap/providers/entities.py +99 -0
- package/elizaos/bootstrap/providers/evaluators.py +58 -0
- package/elizaos/bootstrap/providers/facts.py +86 -0
- package/elizaos/bootstrap/providers/follow_ups.py +116 -0
- package/elizaos/bootstrap/providers/knowledge.py +73 -0
- package/elizaos/bootstrap/providers/providers_list.py +59 -0
- package/elizaos/bootstrap/providers/recent_messages.py +85 -0
- package/elizaos/bootstrap/providers/relationships.py +106 -0
- package/elizaos/bootstrap/providers/roles.py +95 -0
- package/elizaos/bootstrap/providers/settings.py +55 -0
- package/elizaos/bootstrap/providers/time.py +45 -0
- package/elizaos/bootstrap/providers/world.py +97 -0
- package/elizaos/bootstrap/services/__init__.py +26 -0
- package/elizaos/bootstrap/services/embedding.py +122 -0
- package/elizaos/bootstrap/services/follow_up.py +138 -0
- package/elizaos/bootstrap/services/rolodex.py +244 -0
- package/elizaos/bootstrap/services/task.py +585 -0
- package/elizaos/bootstrap/types.py +54 -0
- package/elizaos/bootstrap/utils/__init__.py +7 -0
- package/elizaos/bootstrap/utils/xml.py +69 -0
- package/elizaos/character.py +149 -0
- package/elizaos/logger.py +179 -0
- package/elizaos/media/__init__.py +45 -0
- package/elizaos/media/mime.py +315 -0
- package/elizaos/media/search.py +161 -0
- package/elizaos/media/tests/__init__.py +1 -0
- package/elizaos/media/tests/test_mime.py +117 -0
- package/elizaos/media/tests/test_search.py +156 -0
- package/elizaos/plugin.py +191 -0
- package/elizaos/prompts.py +1071 -0
- package/elizaos/py.typed +0 -0
- package/elizaos/runtime.py +2572 -0
- package/elizaos/services/__init__.py +49 -0
- package/elizaos/services/hook_service.py +511 -0
- package/elizaos/services/message_service.py +1248 -0
- package/elizaos/settings.py +182 -0
- package/elizaos/streaming_context.py +159 -0
- package/elizaos/trajectory_context.py +18 -0
- package/elizaos/types/__init__.py +512 -0
- package/elizaos/types/agent.py +31 -0
- package/elizaos/types/components.py +208 -0
- package/elizaos/types/database.py +64 -0
- package/elizaos/types/environment.py +46 -0
- package/elizaos/types/events.py +47 -0
- package/elizaos/types/memory.py +45 -0
- package/elizaos/types/model.py +393 -0
- package/elizaos/types/plugin.py +188 -0
- package/elizaos/types/primitives.py +100 -0
- package/elizaos/types/runtime.py +460 -0
- package/elizaos/types/service.py +113 -0
- package/elizaos/types/service_interfaces.py +244 -0
- package/elizaos/types/state.py +188 -0
- package/elizaos/types/task.py +29 -0
- package/elizaos/utils/__init__.py +108 -0
- package/elizaos/utils/spec_examples.py +48 -0
- package/elizaos/utils/streaming.py +426 -0
- package/elizaos_atropos_shared/__init__.py +1 -0
- package/elizaos_atropos_shared/canonical_eliza.py +282 -0
- package/package.json +19 -0
- package/pyproject.toml +143 -0
- package/requirements-dev.in +11 -0
- package/requirements-dev.lock +134 -0
- package/requirements.in +9 -0
- package/requirements.lock +64 -0
- package/tests/__init__.py +0 -0
- package/tests/test_action_parameters.py +154 -0
- package/tests/test_actions_provider_examples.py +39 -0
- package/tests/test_advanced_memory_behavior.py +96 -0
- package/tests/test_advanced_memory_flag.py +30 -0
- package/tests/test_advanced_planning_behavior.py +225 -0
- package/tests/test_advanced_planning_flag.py +26 -0
- package/tests/test_autonomy.py +445 -0
- package/tests/test_bootstrap_initialize.py +37 -0
- package/tests/test_character.py +163 -0
- package/tests/test_character_provider.py +231 -0
- package/tests/test_dynamic_prompt_exec.py +561 -0
- package/tests/test_logger_redaction.py +43 -0
- package/tests/test_plugin.py +117 -0
- package/tests/test_runtime.py +422 -0
- package/tests/test_salt_production_enforcement.py +22 -0
- package/tests/test_settings_crypto.py +118 -0
- package/tests/test_streaming.py +295 -0
- package/tests/test_types.py +221 -0
- package/tests/test_uuid_parity.py +46 -0
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import os
|
|
5
|
+
import secrets
|
|
6
|
+
from collections.abc import Mapping
|
|
7
|
+
|
|
8
|
+
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
|
9
|
+
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
|
10
|
+
from cryptography.hazmat.primitives.padding import PKCS7
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def get_salt() -> str:
|
|
14
|
+
salt = os.environ.get("SECRET_SALT", "secretsalt")
|
|
15
|
+
node_env = os.environ.get("NODE_ENV", "").strip().lower()
|
|
16
|
+
allow_default = os.environ.get("ELIZA_ALLOW_DEFAULT_SECRET_SALT", "").strip().lower() == "true"
|
|
17
|
+
if node_env == "production" and salt == "secretsalt" and not allow_default:
|
|
18
|
+
raise RuntimeError(
|
|
19
|
+
"SECRET_SALT must be set to a non-default value in production. "
|
|
20
|
+
"Set ELIZA_ALLOW_DEFAULT_SECRET_SALT=true to override (not recommended)."
|
|
21
|
+
)
|
|
22
|
+
return salt
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _derive_key(salt: str) -> bytes:
|
|
26
|
+
return hashlib.sha256(salt.encode("utf-8")).digest()[:32]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _looks_encrypted(value: str) -> bool:
|
|
30
|
+
parts = value.split(":")
|
|
31
|
+
# v2: v2:ivHex:ciphertextHex:tagHex
|
|
32
|
+
if len(parts) == 4 and parts[0] == "v2":
|
|
33
|
+
try:
|
|
34
|
+
iv = bytes.fromhex(parts[1])
|
|
35
|
+
tag = bytes.fromhex(parts[3])
|
|
36
|
+
except ValueError:
|
|
37
|
+
return False
|
|
38
|
+
return len(iv) == 12 and len(tag) == 16
|
|
39
|
+
|
|
40
|
+
# v1 legacy: ivHex:ciphertextHex
|
|
41
|
+
if len(parts) != 2:
|
|
42
|
+
return False
|
|
43
|
+
try:
|
|
44
|
+
iv = bytes.fromhex(parts[0])
|
|
45
|
+
except ValueError:
|
|
46
|
+
return False
|
|
47
|
+
return len(iv) == 16
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def encrypt_string_value(value: object, salt: str) -> object:
|
|
51
|
+
if value is None or isinstance(value, (bool, int, float)):
|
|
52
|
+
return value
|
|
53
|
+
if not isinstance(value, str):
|
|
54
|
+
return value
|
|
55
|
+
|
|
56
|
+
if _looks_encrypted(value):
|
|
57
|
+
return value
|
|
58
|
+
|
|
59
|
+
# v2: AES-256-GCM with integrity tag
|
|
60
|
+
key = _derive_key(salt)
|
|
61
|
+
iv = secrets.token_bytes(12)
|
|
62
|
+
aad = b"elizaos:settings:v2"
|
|
63
|
+
aesgcm = AESGCM(key)
|
|
64
|
+
ciphertext_and_tag = aesgcm.encrypt(iv, value.encode("utf-8"), aad)
|
|
65
|
+
ciphertext = ciphertext_and_tag[:-16]
|
|
66
|
+
tag = ciphertext_and_tag[-16:]
|
|
67
|
+
return f"v2:{iv.hex()}:{ciphertext.hex()}:{tag.hex()}"
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def decrypt_string_value(value: object, salt: str) -> object:
|
|
71
|
+
if not isinstance(value, str):
|
|
72
|
+
return value
|
|
73
|
+
|
|
74
|
+
parts = value.split(":")
|
|
75
|
+
if len(parts) == 4 and parts[0] == "v2":
|
|
76
|
+
try:
|
|
77
|
+
iv = bytes.fromhex(parts[1])
|
|
78
|
+
ciphertext = bytes.fromhex(parts[2])
|
|
79
|
+
tag = bytes.fromhex(parts[3])
|
|
80
|
+
except ValueError:
|
|
81
|
+
return value
|
|
82
|
+
if len(iv) != 12 or len(tag) != 16:
|
|
83
|
+
return value
|
|
84
|
+
|
|
85
|
+
key = _derive_key(salt)
|
|
86
|
+
aad = b"elizaos:settings:v2"
|
|
87
|
+
aesgcm = AESGCM(key)
|
|
88
|
+
try:
|
|
89
|
+
plaintext_bytes = aesgcm.decrypt(iv, ciphertext + tag, aad)
|
|
90
|
+
except Exception:
|
|
91
|
+
return value
|
|
92
|
+
try:
|
|
93
|
+
return plaintext_bytes.decode("utf-8")
|
|
94
|
+
except UnicodeDecodeError:
|
|
95
|
+
return value
|
|
96
|
+
|
|
97
|
+
if len(parts) != 2:
|
|
98
|
+
return value
|
|
99
|
+
|
|
100
|
+
iv_hex, ciphertext_hex = parts
|
|
101
|
+
try:
|
|
102
|
+
iv = bytes.fromhex(iv_hex)
|
|
103
|
+
except ValueError:
|
|
104
|
+
return value
|
|
105
|
+
if len(iv) != 16:
|
|
106
|
+
return value
|
|
107
|
+
|
|
108
|
+
try:
|
|
109
|
+
ciphertext = bytes.fromhex(ciphertext_hex)
|
|
110
|
+
except ValueError:
|
|
111
|
+
return value
|
|
112
|
+
|
|
113
|
+
key = _derive_key(salt)
|
|
114
|
+
cipher = Cipher(algorithms.AES(key), modes.CBC(iv))
|
|
115
|
+
decryptor = cipher.decryptor()
|
|
116
|
+
|
|
117
|
+
try:
|
|
118
|
+
padded = decryptor.update(ciphertext) + decryptor.finalize()
|
|
119
|
+
except Exception:
|
|
120
|
+
return value
|
|
121
|
+
|
|
122
|
+
try:
|
|
123
|
+
unpadder = PKCS7(128).unpadder()
|
|
124
|
+
plaintext_bytes = unpadder.update(padded) + unpadder.finalize()
|
|
125
|
+
except ValueError:
|
|
126
|
+
return value
|
|
127
|
+
|
|
128
|
+
try:
|
|
129
|
+
return plaintext_bytes.decode("utf-8")
|
|
130
|
+
except UnicodeDecodeError:
|
|
131
|
+
return value
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def encrypt_object_values(obj: Mapping[str, object], salt: str) -> dict[str, object]:
|
|
135
|
+
result: dict[str, object] = {}
|
|
136
|
+
for key, value in obj.items():
|
|
137
|
+
if isinstance(value, str) and value:
|
|
138
|
+
result[key] = encrypt_string_value(value, salt)
|
|
139
|
+
else:
|
|
140
|
+
result[key] = value
|
|
141
|
+
return result
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def decrypt_object_values(obj: Mapping[str, object], salt: str) -> dict[str, object]:
|
|
145
|
+
result: dict[str, object] = {}
|
|
146
|
+
for key, value in obj.items():
|
|
147
|
+
if isinstance(value, str) and value:
|
|
148
|
+
result[key] = decrypt_string_value(value, salt)
|
|
149
|
+
else:
|
|
150
|
+
result[key] = value
|
|
151
|
+
return result
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def migrate_encrypted_string_value(value: object, salt: str) -> object:
|
|
155
|
+
"""
|
|
156
|
+
Migrate a legacy v1 encrypted string (AES-CBC) to v2 (AES-GCM).
|
|
157
|
+
|
|
158
|
+
- v2 values are returned unchanged
|
|
159
|
+
- v1 values are decrypted then re-encrypted as v2
|
|
160
|
+
- non-encrypted values are returned unchanged
|
|
161
|
+
"""
|
|
162
|
+
if not isinstance(value, str):
|
|
163
|
+
return value
|
|
164
|
+
if value.startswith("v2:"):
|
|
165
|
+
return value
|
|
166
|
+
if not _looks_encrypted(value):
|
|
167
|
+
return value
|
|
168
|
+
|
|
169
|
+
decrypted = decrypt_string_value(value, salt)
|
|
170
|
+
if decrypted == value:
|
|
171
|
+
return value
|
|
172
|
+
return encrypt_string_value(decrypted, salt)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def migrate_object_values(obj: Mapping[str, object], salt: str) -> dict[str, object]:
|
|
176
|
+
result: dict[str, object] = {}
|
|
177
|
+
for key, value in obj.items():
|
|
178
|
+
result[key] = migrate_encrypted_string_value(value, salt)
|
|
179
|
+
return result
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
decrypt_secret = decrypt_string_value
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""Streaming context management for automatic streaming in use_model calls.
|
|
2
|
+
|
|
3
|
+
Follows the OpenTelemetry ContextManager pattern (matching TypeScript implementation):
|
|
4
|
+
- Interface for context management
|
|
5
|
+
- Uses contextvars for async-safe context propagation
|
|
6
|
+
- Automatic streaming in nested async calls
|
|
7
|
+
|
|
8
|
+
This provides parity with TypeScript's streaming-context.ts.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from collections.abc import Callable
|
|
12
|
+
from contextvars import ContextVar
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from typing import Any, TypeVar
|
|
15
|
+
|
|
16
|
+
# Type for generic return values
|
|
17
|
+
T = TypeVar("T")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class StreamingContext:
|
|
22
|
+
"""Streaming context containing callbacks for streaming lifecycle.
|
|
23
|
+
|
|
24
|
+
Attributes:
|
|
25
|
+
on_stream_chunk: Called for each chunk of streamed content.
|
|
26
|
+
on_stream_end: Called when a use_model streaming call completes.
|
|
27
|
+
message_id: Optional message ID for tracking.
|
|
28
|
+
abort_signal: Optional callable returning True if cancelled.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
on_stream_chunk: Callable[[str, str | None], Any]
|
|
32
|
+
"""Called for each chunk of streamed content (chunk, message_id) -> None"""
|
|
33
|
+
|
|
34
|
+
on_stream_end: Callable[[], None] | None = None
|
|
35
|
+
"""Called when a use_model streaming call completes (allows reset between calls)"""
|
|
36
|
+
|
|
37
|
+
message_id: str | None = None
|
|
38
|
+
"""Optional message ID for tracking"""
|
|
39
|
+
|
|
40
|
+
abort_signal: Callable[[], bool] | None = None
|
|
41
|
+
"""Optional abort signal - callable returning True if cancelled"""
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# Context variable for async-safe streaming context propagation
|
|
45
|
+
_streaming_context: ContextVar[StreamingContext | None] = ContextVar(
|
|
46
|
+
"streaming_context", default=None
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def run_with_streaming_context(
|
|
51
|
+
context: StreamingContext | None,
|
|
52
|
+
fn: Callable[[], T],
|
|
53
|
+
) -> T:
|
|
54
|
+
"""Run a function with a streaming context.
|
|
55
|
+
|
|
56
|
+
All use_model calls within this function will automatically use streaming
|
|
57
|
+
if a context with on_stream_chunk is provided.
|
|
58
|
+
|
|
59
|
+
Example:
|
|
60
|
+
async def handle_request(message: Memory) -> None:
|
|
61
|
+
async def on_chunk(chunk: str, msg_id: str | None) -> None:
|
|
62
|
+
await send_sse(chunk)
|
|
63
|
+
|
|
64
|
+
ctx = StreamingContext(on_stream_chunk=on_chunk)
|
|
65
|
+
await run_with_streaming_context(
|
|
66
|
+
ctx,
|
|
67
|
+
lambda: runtime.process_message(message)
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
context: The streaming context with on_stream_chunk callback.
|
|
72
|
+
fn: The function to run with streaming context.
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
The result of the function.
|
|
76
|
+
"""
|
|
77
|
+
token = _streaming_context.set(context)
|
|
78
|
+
try:
|
|
79
|
+
return fn()
|
|
80
|
+
finally:
|
|
81
|
+
_streaming_context.reset(token)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
async def run_with_streaming_context_async(
|
|
85
|
+
context: StreamingContext | None,
|
|
86
|
+
fn: Callable[[], Any],
|
|
87
|
+
) -> Any:
|
|
88
|
+
"""Run an async function with a streaming context.
|
|
89
|
+
|
|
90
|
+
Same as run_with_streaming_context but for async functions.
|
|
91
|
+
|
|
92
|
+
Example:
|
|
93
|
+
async def handle_request(message: Memory) -> None:
|
|
94
|
+
async def on_chunk(chunk: str, msg_id: str | None) -> None:
|
|
95
|
+
await send_sse(chunk)
|
|
96
|
+
|
|
97
|
+
ctx = StreamingContext(on_stream_chunk=on_chunk)
|
|
98
|
+
await run_with_streaming_context_async(
|
|
99
|
+
ctx,
|
|
100
|
+
lambda: runtime.process_message(message)
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
context: The streaming context with on_stream_chunk callback.
|
|
105
|
+
fn: The async function to run with streaming context.
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
The result of the async function.
|
|
109
|
+
"""
|
|
110
|
+
token = _streaming_context.set(context)
|
|
111
|
+
try:
|
|
112
|
+
result = fn()
|
|
113
|
+
# Handle both sync and async functions
|
|
114
|
+
if hasattr(result, "__await__"):
|
|
115
|
+
return await result
|
|
116
|
+
return result
|
|
117
|
+
finally:
|
|
118
|
+
_streaming_context.reset(token)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def get_streaming_context() -> StreamingContext | None:
|
|
122
|
+
"""Get the currently active streaming context.
|
|
123
|
+
|
|
124
|
+
Called by use_model to check if automatic streaming should be enabled.
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
The current streaming context or None.
|
|
128
|
+
"""
|
|
129
|
+
return _streaming_context.get()
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def set_streaming_context(context: StreamingContext | None) -> None:
|
|
133
|
+
"""Set the streaming context directly.
|
|
134
|
+
|
|
135
|
+
This is useful for frameworks that manage their own context lifecycle.
|
|
136
|
+
Prefer using run_with_streaming_context when possible.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
context: The streaming context to set, or None to clear.
|
|
140
|
+
"""
|
|
141
|
+
_streaming_context.set(context)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def clear_streaming_context() -> None:
|
|
145
|
+
"""Clear the streaming context.
|
|
146
|
+
|
|
147
|
+
This is useful for cleanup after streaming operations.
|
|
148
|
+
"""
|
|
149
|
+
_streaming_context.set(None)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
__all__ = [
|
|
153
|
+
"StreamingContext",
|
|
154
|
+
"run_with_streaming_context",
|
|
155
|
+
"run_with_streaming_context_async",
|
|
156
|
+
"get_streaming_context",
|
|
157
|
+
"set_streaming_context",
|
|
158
|
+
"clear_streaming_context",
|
|
159
|
+
]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from contextlib import contextmanager
|
|
4
|
+
from contextvars import ContextVar
|
|
5
|
+
|
|
6
|
+
# Async-safe context for associating runtime/model calls with a trajectory step.
|
|
7
|
+
CURRENT_TRAJECTORY_STEP_ID: ContextVar[str | None] = ContextVar(
|
|
8
|
+
"CURRENT_TRAJECTORY_STEP_ID", default=None
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@contextmanager
|
|
13
|
+
def bind_trajectory_step(step_id: str | None):
|
|
14
|
+
token = CURRENT_TRAJECTORY_STEP_ID.set(step_id)
|
|
15
|
+
try:
|
|
16
|
+
yield
|
|
17
|
+
finally:
|
|
18
|
+
CURRENT_TRAJECTORY_STEP_ID.reset(token)
|