python-codex 0.3.2__py3-none-any.whl → 0.4.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.
Files changed (53) hide show
  1. pycodex/__init__.py +3 -27
  2. pycodex/bootstrap.py +5 -45
  3. pycodex/cli.py +1 -14
  4. pycodex/context.py +1 -1
  5. pycodex/events.py +1 -1
  6. pycodex/model.py +2 -2
  7. pycodex/portable/__init__.py +45 -0
  8. pycodex/{portable.py → portable/client.py} +4 -7
  9. pycodex/{portable_server.py → portable/server.py} +3 -3
  10. pycodex/protocol.py +1 -1
  11. pycodex/runtime.py +1 -1
  12. pycodex/tools/base_tool.py +1 -1
  13. pycodex/tools/close_agent_tool.py +1 -1
  14. pycodex/tools/code_mode_manager.py +1 -1
  15. pycodex/tools/request_permissions_tool.py +1 -1
  16. pycodex/tools/resume_agent_tool.py +1 -1
  17. pycodex/{runtime_services.py → tools/runtime_services.py} +5 -5
  18. pycodex/tools/send_input_tool.py +1 -1
  19. pycodex/tools/spawn_agent_tool.py +1 -1
  20. pycodex/tools/unified_exec_manager.py +1 -1
  21. pycodex/tools/update_plan_tool.py +1 -1
  22. pycodex/tools/view_image_tool.py +5 -0
  23. pycodex/tools/wait_agent_tool.py +1 -1
  24. pycodex/utils/__init__.py +0 -4
  25. pycodex/utils/_aes_gcm/LICENSE +56 -0
  26. pycodex/utils/_aes_gcm/README.md +61 -0
  27. pycodex/utils/_aes_gcm/__init__.py +1 -0
  28. pycodex/utils/_aes_gcm/aes.py +175 -0
  29. pycodex/utils/_aes_gcm/gcm.py +73 -0
  30. pycodex/utils/_aes_gcm/source-sha256.json +4 -0
  31. pycodex/{compat.py → utils/compat.py} +2 -0
  32. pycodex/utils/crypto.py +35 -0
  33. pycodex/utils/debug.py +24 -0
  34. pycodex/{doctor.py → utils/doctor.py} +3 -3
  35. pycodex/utils/image_utils.py +3 -2
  36. pycodex/{model_metadata.py → utils/model_metadata.py} +2 -2
  37. {python_codex-0.3.2.dist-info → python_codex-0.4.0.dist-info}/METADATA +165 -42
  38. {python_codex-0.3.2.dist-info → python_codex-0.4.0.dist-info}/RECORD +43 -45
  39. {python_codex-0.3.2.dist-info → python_codex-0.4.0.dist-info}/entry_points.txt +0 -1
  40. responses_server/app.py +28 -15
  41. responses_server/stream_router.py +17 -13
  42. pycodex/feishu_card.py +0 -793
  43. pycodex/feishu_link.py +0 -440
  44. pycodex/tools/ipython_tool.py +0 -142
  45. workspace_server/__init__.py +0 -39
  46. workspace_server/__main__.py +0 -4
  47. workspace_server/app.py +0 -1713
  48. workspace_server/owned_process.py +0 -27
  49. workspace_server/workspace.html +0 -2211
  50. workspace_server/workspaces.html +0 -550
  51. workspace_server/workspaces.py +0 -628
  52. {python_codex-0.3.2.dist-info → python_codex-0.4.0.dist-info}/WHEEL +0 -0
  53. {python_codex-0.3.2.dist-info → python_codex-0.4.0.dist-info}/licenses/LICENSE +0 -0
pycodex/feishu_link.py DELETED
@@ -1,440 +0,0 @@
1
- import asyncio
2
- import threading
3
- import time
4
- import typing
5
-
6
- from .events import SessionStateEvent
7
- from .feishu_card import PycodexCard
8
- from .runtime import AgentRuntime
9
-
10
- CARD_UPDATE_FLUSH_INTERVAL_SECONDS = 0.5
11
- _LISTENER = None
12
- _LISTENER_LOCK = threading.Lock()
13
-
14
-
15
- class PycodexRuntimeLink:
16
- def __init__(
17
- self,
18
- runtime: "AgentRuntime",
19
- target: str,
20
- loop=None,
21
- card: "typing.Union[PycodexCard, None]" = None,
22
- ) -> None:
23
- self.runtime = runtime
24
- self.target = target
25
- self.loop = loop
26
- self.card = card or PycodexCard.from_env()
27
- self.session_key = None
28
- self.message_id = None
29
- self._listener = None
30
- self._frontend_id = None
31
- self._detached = False
32
- self._update_thread = None
33
- self._update_stop = threading.Event()
34
- self._update_lock = threading.Lock()
35
- self._update_pending = False
36
-
37
- async def start_async(self) -> "PycodexRuntimeLink":
38
- self.loop = asyncio.get_event_loop()
39
- self.card.apply_event(SessionStateEvent("attach", self.runtime.snapshot()))
40
- try:
41
- await self.loop.run_in_executor(None, self.start)
42
- except Exception:
43
- await self.loop.run_in_executor(None, self.stop)
44
- raise
45
- self._frontend_id = self.runtime.attach(self._handle_runtime_event)
46
- return self
47
-
48
- def start(self) -> "PycodexRuntimeLink":
49
- if self.loop is None:
50
- raise RuntimeError("PycodexRuntimeLink.start_async() is required")
51
- self.card.send(self.target)
52
- self.session_key = self.card.session_key
53
- self.message_id = self.card.message_id
54
- self._listener = _feishu_listener(self.card)
55
- self._listener.register(self)
56
- self._listener.start()
57
- self._start_update_thread()
58
- return self
59
-
60
- def stop(self) -> None:
61
- self._detach_frontend()
62
- if self._listener is not None:
63
- _release_feishu_listener(self._listener, self)
64
- self._listener = None
65
- self._stop_update_thread()
66
-
67
- def detach(self) -> None:
68
- self._detached = True
69
- self._detach_frontend()
70
- if self._listener is not None:
71
- _release_feishu_listener(self._listener, self)
72
- self._listener = None
73
- self.card.detach()
74
- self._safe_update_card()
75
- self._stop_update_thread()
76
-
77
- def _detach_frontend(self) -> None:
78
- if self._frontend_id is not None:
79
- self.runtime.detach(self._frontend_id)
80
- self._frontend_id = None
81
-
82
- def _handle_runtime_event(self, event) -> None:
83
- if self._detached:
84
- return
85
- self.card.apply_event(event)
86
- self._update_card()
87
-
88
- async def _submit(
89
- self, action: "typing.Dict[str, object]"
90
- ) -> "typing.Dict[str, object]":
91
- if self._detached:
92
- return {"toast": {"type": "warning", "content": "pycodex is detached."}}
93
- if action.get("action") == "history":
94
- try:
95
- self.card.show_history(action.get("history_index"))
96
- except ValueError as exc:
97
- return {"toast": {"type": "warning", "content": str(exc)}}
98
- self._update_card()
99
- return {}
100
- if action.get("action") != "send":
101
- return {
102
- "toast": {
103
- "type": "info",
104
- "content": "Unsupported action: {0}".format(action.get("action")),
105
- }
106
- }
107
- prompt = str(action.get("prompt") or "").strip()
108
- future = asyncio.run_coroutine_threadsafe(
109
- self.runtime.submit_input(
110
- prompt, sender=str(action.get("sender") or "feishu")
111
- ),
112
- self.loop,
113
- )
114
- try:
115
- await asyncio.wrap_future(future)
116
- except (ValueError, RuntimeError) as exc:
117
- return {"toast": {"type": "warning", "content": str(exc)}}
118
- return {"toast": {"type": "info", "content": "Input submitted."}}
119
-
120
- def _update_card(self) -> None:
121
- with self._update_lock:
122
- self._update_pending = True
123
-
124
- def _safe_update_card(self) -> None:
125
- try:
126
- self.card.update()
127
- except Exception as e:
128
- print(e)
129
-
130
- def _start_update_thread(self) -> None:
131
- if self._update_thread is not None and self._update_thread.is_alive():
132
- return
133
- self._update_stop.clear()
134
- self._update_thread = threading.Thread(
135
- target=self._run_update_loop,
136
- name="pycodex-feishu-card",
137
- )
138
- self._update_thread.daemon = True
139
- self._update_thread.start()
140
-
141
- def _stop_update_thread(self) -> None:
142
- self._update_stop.set()
143
- thread = self._update_thread
144
- if thread is not None and thread.is_alive():
145
- thread.join(timeout=2.0)
146
- self._update_thread = None
147
-
148
- def _run_update_loop(self) -> None:
149
- while not self._update_stop.wait(CARD_UPDATE_FLUSH_INTERVAL_SECONDS):
150
- with self._update_lock:
151
- if not self._update_pending:
152
- continue
153
- self._update_pending = False
154
- self._safe_update_card()
155
-
156
-
157
- def _feishu_listener(card: PycodexCard) -> "_FeishuCardActionListener":
158
- global _LISTENER
159
- with _LISTENER_LOCK:
160
- if _LISTENER is None:
161
- _LISTENER = _FeishuCardActionListener(card)
162
- else:
163
- _LISTENER.assert_compatible(card)
164
- return _LISTENER
165
-
166
-
167
- def _release_feishu_listener(
168
- listener: "_FeishuCardActionListener",
169
- link: PycodexRuntimeLink,
170
- ) -> None:
171
- global _LISTENER
172
- with _LISTENER_LOCK:
173
- listener.unregister(link)
174
- if listener.empty():
175
- listener.stop()
176
- if _LISTENER is listener:
177
- _LISTENER = None
178
-
179
-
180
- class _FeishuCardActionListener:
181
- def __init__(self, card: PycodexCard) -> None:
182
- self.app_id = card.app_id
183
- self.app_secret = card.app_secret
184
- self.domain = card.domain
185
- self.encrypt_key = card.encrypt_key
186
- self.verification_token = card.verification_token
187
- self._links = {}
188
- self._links_lock = threading.Lock()
189
- self._async_thread = _AsyncLoopThread()
190
- self._thread = None
191
- self._client = None
192
- self._client_loop = None
193
- self._stop_requested = threading.Event()
194
-
195
- def assert_compatible(self, card: PycodexCard) -> None:
196
- if self.app_id != card.app_id or self.domain != card.domain:
197
- raise RuntimeError("Feishu listener is already running for another app")
198
-
199
- def start(self) -> None:
200
- if not self.app_id or not self.app_secret:
201
- raise RuntimeError("FEISHU_APP_ID and FEISHU_APP_SECRET are required")
202
- if self._thread is not None and self._thread.is_alive():
203
- return
204
- self._stop_requested.clear()
205
- self._async_thread.start()
206
- self._thread = threading.Thread(
207
- target=self._listen,
208
- name="pycodex-feishu-link",
209
- )
210
- self._thread.daemon = True
211
- self._thread.start()
212
-
213
- def register(self, link: PycodexRuntimeLink) -> None:
214
- message_id = str(getattr(link, "message_id", "") or "").strip()
215
- if not message_id:
216
- raise RuntimeError("Feishu linked card message_id is required")
217
- with self._links_lock:
218
- self._links[message_id] = link
219
-
220
- def unregister(self, link: PycodexRuntimeLink) -> None:
221
- message_id = str(getattr(link, "message_id", "") or "").strip()
222
- with self._links_lock:
223
- if message_id and self._links.get(message_id) is link:
224
- del self._links[message_id]
225
- return
226
- stale_ids = [
227
- registered_id
228
- for registered_id, registered_link in self._links.items()
229
- if registered_link is link
230
- ]
231
- for registered_id in stale_ids:
232
- del self._links[registered_id]
233
-
234
- def empty(self) -> bool:
235
- with self._links_lock:
236
- return not self._links
237
-
238
- def stop(self) -> None:
239
- self._stop_requested.set()
240
- client = self._client
241
- loop = self._client_loop
242
- if loop is not None:
243
- _stop_lark_client(client, loop)
244
- thread = self._thread
245
- if thread is not None and thread.is_alive():
246
- thread.join(timeout=2.0)
247
- self._thread = None
248
- self._async_thread.stop()
249
-
250
- def _listen(self) -> None:
251
- try:
252
- import lark_oapi as lark
253
- import lark_oapi.ws as lark_ws
254
- from lark_oapi.event.callback.model.p2_card_action_trigger import (
255
- P2CardActionTriggerResponse,
256
- )
257
- except ImportError as exc:
258
- raise RuntimeError(
259
- "lark-oapi is required for Feishu long-connection mode"
260
- ) from exc
261
-
262
- def on_card_action(event):
263
- return P2CardActionTriggerResponse(self._handle_card_action(event))
264
-
265
- handler = (
266
- lark.EventDispatcherHandler.builder(
267
- self.encrypt_key or "",
268
- self.verification_token or "",
269
- lark.LogLevel.INFO,
270
- )
271
- .register_p2_card_action_trigger(on_card_action)
272
- .build()
273
- )
274
- import lark_oapi.ws.client as lark_ws_client
275
-
276
- client = None
277
- loop = asyncio.new_event_loop()
278
- self._client_loop = loop
279
- asyncio.set_event_loop(loop)
280
- lark_ws_client.loop = loop
281
- try:
282
- if self._stop_requested.is_set():
283
- return
284
- client = lark_ws.Client(
285
- self.app_id,
286
- self.app_secret,
287
- event_handler=handler,
288
- domain=self.domain,
289
- )
290
- self._client = client
291
- if self._stop_requested.is_set():
292
- return
293
- client.start()
294
- except RuntimeError as exc:
295
- stopped = "Event loop stopped before Future completed" in str(exc)
296
- if not self._stop_requested.is_set() or not stopped:
297
- raise
298
- finally:
299
- if self._client is client:
300
- self._client = None
301
- if self._client_loop is loop:
302
- self._client_loop = None
303
- _close_loop(loop)
304
- asyncio.set_event_loop(None)
305
-
306
- def _handle_card_action(self, event) -> "typing.Dict[str, object]":
307
- try:
308
- message_id = _event_message_id(event)
309
- link = self._resolve_link(message_id)
310
- if link is None or link._detached:
311
- return {
312
- "toast": {
313
- "type": "warning",
314
- "content": "pycodex is detached.",
315
- }
316
- }
317
- action = link.card.parse_action(event)
318
- return self._async_thread.run(link._submit(action))
319
- except Exception as exc:
320
- return {
321
- "toast": {
322
- "type": "error",
323
- "content": "pycodex error: {0}".format(exc),
324
- }
325
- }
326
-
327
- def _resolve_link(self, message_id):
328
- message_id = str(message_id or "").strip()
329
- with self._links_lock:
330
- if message_id:
331
- return self._links.get(message_id)
332
- if len(self._links) == 1:
333
- return next(iter(self._links.values()))
334
- return None
335
-
336
-
337
- def _event_message_id(event) -> "typing.Union[str, None]":
338
- event_data = getattr(event, "event", None)
339
- context = getattr(event_data, "context", None)
340
- for name in ("open_message_id", "message_id"):
341
- value = str(getattr(context, name, None) or "").strip()
342
- if value:
343
- return value
344
- return None
345
-
346
-
347
- def _stop_lark_client(client, loop) -> None:
348
- if loop.is_closed():
349
- return
350
- disconnect = getattr(client, "_disconnect", None) if client is not None else None
351
- if loop.is_running():
352
- future = asyncio.run_coroutine_threadsafe(
353
- _disconnect_then_stop(disconnect, loop),
354
- loop,
355
- )
356
- try:
357
- future.result(timeout=2.0)
358
- except Exception as exc:
359
- future.cancel()
360
- print(exc)
361
- loop.call_soon_threadsafe(loop.stop)
362
- return
363
- if callable(disconnect):
364
- try:
365
- loop.run_until_complete(disconnect())
366
- except Exception as exc:
367
- print(exc)
368
-
369
-
370
- async def _disconnect_then_stop(disconnect, loop) -> None:
371
- try:
372
- if callable(disconnect):
373
- await disconnect()
374
- except Exception as exc:
375
- print(exc)
376
- finally:
377
- loop.stop()
378
-
379
-
380
- def _close_loop(loop) -> None:
381
- if loop.is_closed():
382
- return
383
- tasks = list(_all_loop_tasks(loop))
384
- for task in tasks:
385
- task.cancel()
386
- if tasks:
387
- loop.run_until_complete(asyncio.gather(*tasks, return_exceptions=True))
388
- shutdown_asyncgens = getattr(loop, "shutdown_asyncgens", None)
389
- if callable(shutdown_asyncgens):
390
- loop.run_until_complete(shutdown_asyncgens())
391
- loop.close()
392
-
393
-
394
- def _all_loop_tasks(loop):
395
- all_tasks = getattr(asyncio, "all_tasks", None)
396
- if callable(all_tasks):
397
- return all_tasks(loop)
398
- return asyncio.Task.all_tasks(loop)
399
-
400
-
401
- class _AsyncLoopThread:
402
- def __init__(self) -> None:
403
- self.loop = None
404
- self.thread = None
405
- self._started = threading.Event()
406
-
407
- def start(self) -> None:
408
- if self.loop is not None and self.loop.is_running():
409
- return
410
- self.loop = asyncio.new_event_loop()
411
- self._started.clear()
412
- self.thread = threading.Thread(
413
- target=self._run_loop,
414
- name="pycodex-feishu-asyncio",
415
- )
416
- self.thread.daemon = True
417
- self.thread.start()
418
- self._started.wait()
419
-
420
- def run(self, coro):
421
- if self.loop is None or not self.loop.is_running():
422
- raise RuntimeError("Feishu asyncio loop is not running")
423
- return asyncio.run_coroutine_threadsafe(coro, self.loop).result()
424
-
425
- def stop(self) -> None:
426
- loop = self.loop
427
- if loop is None:
428
- return
429
- if loop.is_running():
430
- loop.call_soon_threadsafe(loop.stop)
431
- if self.thread is not None:
432
- self.thread.join()
433
- loop.close()
434
- self.loop = None
435
- self.thread = None
436
-
437
- def _run_loop(self) -> None:
438
- asyncio.set_event_loop(self.loop)
439
- self._started.set()
440
- self.loop.run_forever()
@@ -1,142 +0,0 @@
1
- """Optional tool that executes code in the current IPython kernel."""
2
-
3
- from ..events import ToolCompletedEvent
4
- from ..utils.event_helpers import colorize_tool_message
5
- from .base_tool import BaseTool, ToolContext
6
-
7
-
8
- class IPythonTool(BaseTool):
9
- name = "ipython"
10
- description = (
11
- "Execute Python code in the current IPython kernel namespace. Use this "
12
- "only when running inside IPython and live notebook/session variables "
13
- "need to be inspected."
14
- )
15
- input_schema = {
16
- "type": "object",
17
- "properties": {
18
- "code": {
19
- "type": "string",
20
- "description": "Python code to execute in the current IPython session.",
21
- },
22
- },
23
- "required": ["code"],
24
- "additionalProperties": False,
25
- }
26
- supports_parallel = False
27
-
28
- def __init__(self, shell, name="ipython"):
29
- self._shell = shell
30
- self.name = name
31
-
32
- async def run(self, context: "ToolContext", args):
33
- del context
34
- if not isinstance(args, dict):
35
- return {"error": "arguments must be an object"}
36
-
37
- code = args.get("code", "")
38
- if not isinstance(code, str) or not code.strip():
39
- return {"error": "`code` must be a non-empty string"}
40
-
41
- from IPython.display import Code, display
42
- from IPython.utils.capture import capture_output
43
-
44
- display(Code(code, language="python"))
45
- with capture_output() as captured:
46
- result = self._shell.run_cell(code, store_history=False)
47
- captured.show()
48
-
49
- output = {
50
- "stdout": captured.stdout,
51
- "stderr": captured.stderr,
52
- }
53
-
54
- display_outputs = []
55
- for item in captured.outputs:
56
- data = getattr(item, "data", None)
57
- if isinstance(data, dict):
58
- display_outputs.append(
59
- data.get("text/plain") or data.get("text/html") or repr(data)
60
- )
61
- else:
62
- display_outputs.append(repr(item))
63
- if display_outputs:
64
- output["display"] = display_outputs
65
-
66
- if getattr(result, "result", None) is not None:
67
- output["result"] = repr(result.result)
68
-
69
- error = result.error_before_exec or result.error_in_exec
70
- if error is not None:
71
- output["error"] = f"{type(error).__name__}: {error}"
72
-
73
- return output
74
-
75
-
76
- def _install_agent_shortcut(shell):
77
- existing = shell.user_ns.get("_pycodex_agent_shortcut_transform")
78
- if existing in shell.input_transformers_cleanup:
79
- return existing
80
-
81
- def transform(lines):
82
- if len(lines) != 1:
83
- return lines
84
-
85
- line = lines[0]
86
- stripped = line.lstrip()
87
- indent = line[: len(line) - len(stripped)]
88
- if not stripped.startswith("@"):
89
- return lines
90
-
91
- body = stripped[1:].rstrip("\n")
92
- parts = body.split(None, 1)
93
- if not parts:
94
- return lines
95
-
96
- agent_name = parts[0]
97
- if not agent_name.isidentifier():
98
- return lines
99
-
100
- agent = shell.user_ns.get(agent_name)
101
- if agent is None or not callable(getattr(agent, "run_turn", None)):
102
- return lines
103
-
104
- prompt = "" if len(parts) == 1 else parts[1]
105
- if not prompt.strip():
106
- return [f"{indent}print('Usage: @{agent_name} <prompt>')\n"]
107
-
108
- return [f"{indent}print({agent_name}.ask({prompt!r}).output_text)\n"]
109
-
110
- shell.input_transformers_cleanup.append(transform)
111
- shell.user_ns["_pycodex_agent_shortcut_transform"] = transform
112
- return transform
113
-
114
-
115
- def attach_ipython_event_printer(agent, color=True):
116
- def handle_event(event):
117
- if isinstance(event, ToolCompletedEvent):
118
- tool_name = event.call.name
119
- message = event.visualize()
120
- for line in message.splitlines() or [""]:
121
- print(colorize_tool_message(line, color, tool_name), flush=True)
122
-
123
- agent.event_handler = handle_event
124
- return handle_event
125
-
126
-
127
- def attach_ipython_tool(
128
- agent, name="current_ipython", shortcut=True, print_tool_events=True, color=True
129
- ):
130
- from IPython import get_ipython
131
-
132
- shell = get_ipython()
133
- if shell is None:
134
- raise RuntimeError("not running inside IPython")
135
-
136
- tool = IPythonTool(shell, name=name)
137
- agent.tool_registry.register(tool)
138
- if shortcut:
139
- _install_agent_shortcut(shell)
140
- if print_tool_events:
141
- attach_ipython_event_printer(agent, color=color)
142
- return tool
@@ -1,39 +0,0 @@
1
- from .app import (
2
- ThreadedWorkspaceInteractiveSession,
3
- WebSessionView,
4
- WorkspaceInteractiveSession,
5
- build_parser,
6
- create_app,
7
- create_multi_workspace_app,
8
- main,
9
- parse_listen,
10
- run_serve_cli,
11
- )
12
- from .workspaces import (
13
- WorkspaceDefinition,
14
- WorkspaceEntry,
15
- WorkspaceRegistry,
16
- WorkspaceSessionManager,
17
- WorkspaceStateStore,
18
- default_board_path,
19
- load_workspace_definitions,
20
- )
21
-
22
- __all__ = [
23
- "ThreadedWorkspaceInteractiveSession",
24
- "WebSessionView",
25
- "WorkspaceInteractiveSession",
26
- "WorkspaceDefinition",
27
- "WorkspaceEntry",
28
- "WorkspaceRegistry",
29
- "WorkspaceSessionManager",
30
- "WorkspaceStateStore",
31
- "build_parser",
32
- "create_app",
33
- "create_multi_workspace_app",
34
- "default_board_path",
35
- "load_workspace_definitions",
36
- "main",
37
- "parse_listen",
38
- "run_serve_cli",
39
- ]
@@ -1,4 +0,0 @@
1
- from .app import main
2
-
3
- if __name__ == "__main__":
4
- main()