diff --git a/README.md b/README.md index 9215e47..8e8abe0 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,8 @@ Python-based Telnet helper for connecting to MUD servers, handling login flows, Add guidance text after the type and optional modifiers separated by `|`, e.g. `#agent intelligent explore carefully|model=mistral/mistral-large-2407|delay=2`. The agent only calls built-in tools (`look`, `move`, `movement`, `explore`, `communication`, `intelligentcommunication`) and refuses unknown names. +9. Prefer a TUI? Run `python textual_ui.py` to open a two-pane interface (MUD output on the left, agent output on the right) with an input box at the bottom. It accepts the same commands as the CLI (`#execute …`, `#agent …`, or raw MUD commands). + ## Environment Variables All variables can be placed in the `.env` file (one `KEY=value` per line) or provided through the shell environment. diff --git a/app.py b/app.py index 7980265..b61e674 100644 --- a/app.py +++ b/app.py @@ -7,7 +7,7 @@ from pathlib import Path from threading import Event, Lock, Thread from typing import Callable, Optional, Type -from tools import Tool, LookTool +from tools import Tool from agents import Agent, build_agent from agent_runtime import run_agent @@ -18,6 +18,12 @@ TOOL_REGISTRY = { "kwargs": {}, "description": "Sends the 'schau' look command to refresh the room description.", }, + "simple": { + "module": "tools", + "class": "LookTool", + "kwargs": {}, + "description": "Alias of 'look'. Sends the 'schau' look command to refresh the room description.", + }, "move": { "module": "movement_tool", "class": "MovementTool", @@ -63,6 +69,7 @@ TOOL_REGISTRY = { TOOL_DESCRIPTIONS = { name: meta["description"] for name, meta in TOOL_REGISTRY.items() } + from telnetclient import TelnetClient @@ -208,13 +215,8 @@ def require_env(key: str) -> str: def build_tool(spec: str) -> Tool: """Instantiate a tool based on configuration.""" - normalized = spec.strip() - if not normalized: - return LookTool() - + normalized = spec.strip() or "look" key = normalized.lower() - if key in {"simple", "look"}: - return LookTool() if key in TOOL_REGISTRY: meta = TOOL_REGISTRY[key] diff --git a/pyproject.toml b/pyproject.toml index 012b106..ce2ec6a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,4 +6,5 @@ readme = "README.md" requires-python = ">=3.9" dependencies = [ "litellm>=1.77.4", + "textual>=0.48.1", ] diff --git a/textual_ui.py b/textual_ui.py new file mode 100644 index 0000000..0757491 --- /dev/null +++ b/textual_ui.py @@ -0,0 +1,260 @@ +from __future__ import annotations + +import io +import os +from contextlib import redirect_stdout, redirect_stderr +from threading import Event, Thread, current_thread +from typing import Callable + +from textual.app import App, ComposeResult +from textual.containers import Horizontal, Vertical +from textual.widgets import Footer, Header, Input, Label, Log + +from app import ( + TOOL_DESCRIPTIONS, + SessionState, + build_tool, + load_env_file, + login, + require_env, + run_tool_loop, +) +from agent_runtime import run_agent +from agents import build_agent +from telnetclient import TelnetClient + + +class _QueueWriter(io.TextIOBase): + def __init__(self, emit: Callable[[str], None]) -> None: + super().__init__() + self._emit = emit + self._buffer: str = "" + + def write(self, s: str) -> int: # type: ignore[override] + self._buffer += s + while "\n" in self._buffer: + line, self._buffer = self._buffer.split("\n", 1) + if line: + self._emit(line) + return len(s) + + def flush(self) -> None: # type: ignore[override] + if self._buffer: + self._emit(self._buffer) + self._buffer = "" + + +class MudUI(App): + CSS = """ + Screen { + layout: vertical; + } + #logs { + height: 1fr; + } + #input-row { + padding: 1 2; + } + Log { + border: round #888881; + padding: 1 1; + } + """ + + BINDINGS = [ + ("ctrl+c", "quit", "Quit"), + ] + + def compose(self) -> ComposeResult: + yield Header(show_clock=True) + with Vertical(id="logs"): + yield Label("MUD Output") + self.mud_log = Log(classes="mud") + yield self.mud_log + yield Label("Agent Output") + self.agent_log = Log(classes="agent") + yield self.agent_log + with Horizontal(id="input-row"): + self.input = Input(placeholder="Type command or #execute/#agent ...", id="command") + yield self.input + yield Footer() + + def on_mount(self) -> None: + self._stop_event = Event() + self._ui_thread = current_thread() + load_env_file() + host = require_env("MISTLE_HOST") + port = int(require_env("MISTLE_PORT")) + timeout = float(os.environ.get("MISTLE_TIMEOUT", "10")) + + self.state = SessionState() + self.client = TelnetClient(host=host, port=port, timeout=timeout) + try: + self.client.connect() + except Exception as exc: # pragma: no cover - network specific + self.log_mud(f"[error] Failed to connect: {exc}") + return + + writer = _QueueWriter(lambda line: self._emit_to_log(self.mud_log, line)) + with redirect_stdout(writer), redirect_stderr(writer): + login( + self.client, + user=os.environ.get("MISTLE_USER", ""), + password=os.environ.get("MISTLE_PASSWORD", ""), + login_prompt=os.environ.get("MISTLE_LOGIN_PROMPT", ""), + state=self.state, + ) + writer.flush() + + self.reader_thread = Thread(target=self._reader_loop, daemon=True) + self.reader_thread.start() + + self.input.focus() + self.log_mud(f"Connected to {host}:{port}") + + def on_input_submitted(self, event: Input.Submitted) -> None: + command = event.value.strip() + event.input.value = "" + if not command: + return + if command.startswith("#execute"): + parts = command.split(maxsplit=1) + if len(parts) == 1: + self.log_agent("Usage: #execute ") + else: + self._start_tool(parts[1]) + return + if command.startswith("#agent"): + parts = command.split(maxsplit=1) + if len(parts) == 1: + self.log_agent("Usage: #agent ") + else: + self._start_agent(parts[1]) + return + self.state.send(self.client, command) + self.log_agent(f"> {command}") + + def on_unmount(self) -> None: + self._stop_event.set() + try: + self.client.close() + except Exception: + pass + + def _emit_to_log(self, log: Log, message: str) -> None: + if current_thread() is self._ui_thread: + log.write(message) + else: + log.write(message) + + def log_mud(self, message: str) -> None: + self._emit_to_log(self.mud_log, message) + + def log_agent(self, message: str) -> None: + self._emit_to_log(self.agent_log, message) + + def _reader_loop(self) -> None: + while not self._stop_event.is_set(): + data = self.client.receive(timeout=0.3) + if data: + self.state.update_output(data) + self._emit_to_log(self.mud_log, data) + + def _wrap_run(self, func: Callable[[], None]) -> Thread: + def runner() -> None: + writer = _QueueWriter(lambda line: self._emit_to_log(self.agent_log, line)) + with redirect_stdout(writer), redirect_stderr(writer): + func() + writer.flush() + + thread = Thread(target=runner, daemon=True) + thread.start() + return thread + + def _start_tool(self, raw_spec: str) -> None: + spec = raw_spec.strip() + if not spec: + self.log_agent("Usage: #execute ") + return + self.log_agent(f"[Tool] Executing {spec!r}") + + def worker() -> None: + try: + tool = build_tool(spec) + except RuntimeError as exc: + print(f"[Agent] Failed to load tool {spec}: {exc}") + return + + run_tool_loop( + self.client, + self.state, + tool, + self._stop_event, + min_send_interval=1.0, + auto_stop=True, + auto_stop_idle=2.0, + ) + + self._wrap_run(worker) + + def _start_agent(self, raw_spec: str) -> None: + spec = raw_spec.strip() + if not spec: + self.log_agent("Usage: #agent ") + return + self.log_agent(f"[Agent] Executing {spec!r}") + + def build(spec_str: str) -> Tool: + return build_tool(spec_str) + + def run_tool_instance(tool: Tool) -> bool: + run_tool_loop( + self.client, + self.state, + tool, + self._stop_event, + min_send_interval=1.0, + auto_stop=True, + auto_stop_idle=2.0, + ) + output_after = self.state.snapshot_output() + if output_after: + observe = getattr(agent, "observe", None) + if callable(observe): + try: + observe(output_after) + except Exception as exc: # pragma: no cover + print(f"[Agent] observe failed: {exc}") + return True + + def send_command(command: str) -> None: + self.state.send(self.client, command) + self._emit_to_log(self.agent_log, f"[Agent] command: {command}") + + try: + agent = build_agent(spec, allowed_tools=TOOL_DESCRIPTIONS) + except RuntimeError as exc: + self.log_agent(f"[Agent] Failed to configure '{spec}': {exc}") + return + + last_output = self.state.snapshot_output() + observe = getattr(agent, "observe", None) + if last_output and callable(observe): + try: + observe(last_output) + except Exception as exc: # pragma: no cover + self.log_agent(f"[Agent] observe failed: {exc}") + + self._wrap_run( + lambda: run_agent( + agent, + build_tool=build, + run_tool=run_tool_instance, + send_command=send_command, + stop_event=self._stop_event, + ) + ) + + +if __name__ == "__main__": + MudUI().run() diff --git a/uv.lock b/uv.lock index 6e51a10..a90b20d 100644 --- a/uv.lock +++ b/uv.lock @@ -725,6 +725,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, ] +[[package]] +name = "linkify-it-py" +version = "2.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "uc-micro-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2a/ae/bb56c6828e4797ba5a4821eec7c43b8bf40f69cda4d4f5f8c8a2810ec96a/linkify-it-py-2.0.3.tar.gz", hash = "sha256:68cda27e162e9215c17d786649d1da0021a451bdc436ef9e0fa0ba5234b9b048", size = 27946, upload-time = "2024-02-04T14:48:04.179Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/1e/b832de447dee8b582cac175871d2f6c3d5077cc56d5575cadba1fd1cccfa/linkify_it_py-2.0.3-py3-none-any.whl", hash = "sha256:6bcbc417b0ac14323382aef5c5192c0075bf8a9d6b41820a2b66371eac6b6d79", size = 19820, upload-time = "2024-02-04T14:48:02.496Z" }, +] + [[package]] name = "litellm" version = "1.77.4" @@ -756,6 +768,52 @@ version = "0.7.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/da/eb/95288b1c4aa541eb296a6271e3f8c7ece03b78923ac47dbe95d2287d9f5e/madoka-0.7.1.tar.gz", hash = "sha256:e258baa84fc0a3764365993b8bf5e1b065383a6ca8c9f862fb3e3e709843fae7", size = 81413, upload-time = "2019-02-10T18:38:01.382Z" } +[[package]] +name = "markdown-it-py" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "mdurl", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596, upload-time = "2023-06-03T06:41:14.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload-time = "2023-06-03T06:41:11.019Z" }, +] + +[package.optional-dependencies] +linkify = [ + { name = "linkify-it-py", marker = "python_full_version < '3.10'" }, +] +plugins = [ + { name = "mdit-py-plugins", version = "0.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +dependencies = [ + { name = "mdurl", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, +] + +[package.optional-dependencies] +linkify = [ + { name = "linkify-it-py", marker = "python_full_version >= '3.10'" }, +] +plugins = [ + { name = "mdit-py-plugins", version = "0.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] + [[package]] name = "markupsafe" version = "3.0.2" @@ -824,16 +882,59 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b3/73/085399401383ce949f727afec55ec3abd76648d04b9f22e1c0e99cb4bec3/MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a", size = 15506, upload-time = "2024-10-18T15:21:52.974Z" }, ] +[[package]] +name = "mdit-py-plugins" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "markdown-it-py", version = "3.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/03/a2ecab526543b152300717cf232bb4bb8605b6edb946c845016fa9c9c9fd/mdit_py_plugins-0.4.2.tar.gz", hash = "sha256:5f2cd1fdb606ddf152d37ec30e46101a60512bc0e5fa1a7002c36647b09e26b5", size = 43542, upload-time = "2024-09-09T20:27:49.564Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/f7/7782a043553ee469c1ff49cfa1cdace2d6bf99a1f333cf38676b3ddf30da/mdit_py_plugins-0.4.2-py3-none-any.whl", hash = "sha256:0c673c3f889399a33b95e88d2f0d111b4447bdfea7f237dab2d488f459835636", size = 55316, upload-time = "2024-09-09T20:27:48.397Z" }, +] + +[[package]] +name = "mdit-py-plugins" +version = "0.5.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +dependencies = [ + { name = "markdown-it-py", version = "4.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b2/fd/a756d36c0bfba5f6e39a1cdbdbfdd448dc02692467d83816dff4592a1ebc/mdit_py_plugins-0.5.0.tar.gz", hash = "sha256:f4918cb50119f50446560513a8e311d574ff6aaed72606ddae6d35716fe809c6", size = 44655, upload-time = "2025-08-11T07:25:49.083Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/86/dd6e5db36df29e76c7a7699123569a4a18c1623ce68d826ed96c62643cae/mdit_py_plugins-0.5.0-py3-none-any.whl", hash = "sha256:07a08422fc1936a5d26d146759e9155ea466e842f5ab2f7d2266dd084c8dab1f", size = 57205, upload-time = "2025-08-11T07:25:47.597Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + [[package]] name = "mistle-mudbot" version = "0.1.0" source = { virtual = "." } dependencies = [ { name = "litellm" }, + { name = "textual" }, ] [package.metadata] -requires-dist = [{ name = "litellm", specifier = ">=1.77.4" }] +requires-dist = [ + { name = "litellm", specifier = ">=1.77.4" }, + { name = "textual", specifier = ">=0.48.1" }, +] [[package]] name = "multidict" @@ -983,6 +1084,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, ] +[[package]] +name = "platformdirs" +version = "4.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/23/e8/21db9c9987b0e728855bd57bff6984f67952bea55d6f75e055c46b5383e8/platformdirs-4.4.0.tar.gz", hash = "sha256:ca753cf4d81dc309bc67b0ea38fd15dc97bc30ce419a7f58d13eb3bf14c4febf", size = 21634, upload-time = "2025-08-26T14:32:04.268Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/4b/2028861e724d3bd36227adfa20d3fd24c3fc6d52032f4a93c133be5d17ce/platformdirs-4.4.0-py3-none-any.whl", hash = "sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85", size = 18654, upload-time = "2025-08-26T14:32:02.735Z" }, +] + [[package]] name = "pondpond" version = "1.4.1" @@ -1224,6 +1334,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d4/29/3cade8a924a61f60ccfa10842f75eb12787e1440e2b8660ceffeb26685e7/pydantic_core-2.33.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2807668ba86cb38c6817ad9bc66215ab8584d1d304030ce4f0887336f28a5e27", size = 2066661, upload-time = "2025-04-23T18:33:49.995Z" }, ] +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + [[package]] name = "python-dotenv" version = "1.1.1" @@ -1457,6 +1576,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, ] +[[package]] +name = "rich" +version = "14.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py", version = "3.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "markdown-it-py", version = "4.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fe/75/af448d8e52bf1d8fa6a9d089ca6c07ff4453d86c65c145d0a300bb073b9b/rich-14.1.0.tar.gz", hash = "sha256:e497a48b844b0320d45007cdebfeaeed8db2a4f4bcf49f15e455cfc4af11eaa8", size = 224441, upload-time = "2025-07-25T07:32:58.125Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e3/30/3c4d035596d3cf444529e0b2953ad0466f6049528a879d27534700580395/rich-14.1.0-py3-none-any.whl", hash = "sha256:536f5f1785986d6dbdea3c75205c473f970777b4a0d6c6dd1b696aa05a3fa04f", size = 243368, upload-time = "2025-07-25T07:32:56.73Z" }, +] + [[package]] name = "rpds-py" version = "0.27.1" @@ -1628,6 +1761,23 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, ] +[[package]] +name = "textual" +version = "6.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py", version = "3.0.0", source = { registry = "https://pypi.org/simple" }, extra = ["linkify", "plugins"], marker = "python_full_version < '3.10'" }, + { name = "markdown-it-py", version = "4.0.0", source = { registry = "https://pypi.org/simple" }, extra = ["linkify", "plugins"], marker = "python_full_version >= '3.10'" }, + { name = "platformdirs" }, + { name = "pygments" }, + { name = "rich" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/da/44/4b524b2f06e0fa6c4ede56a4e9af5edd5f3f83cf2eea5cb4fd0ce5bbe063/textual-6.1.0.tar.gz", hash = "sha256:cc89826ca2146c645563259320ca4ddc75d183c77afb7d58acdd46849df9144d", size = 1564786, upload-time = "2025-09-02T11:42:34.655Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/43/f91e041f239b54399310a99041faf33beae9a6e628671471d0fcd6276af4/textual-6.1.0-py3-none-any.whl", hash = "sha256:a3f5e6710404fcdc6385385db894699282dccf2ad50103cebc677403c1baadd5", size = 707840, upload-time = "2025-09-02T11:42:32.746Z" }, +] + [[package]] name = "tiktoken" version = "0.11.0" @@ -1728,6 +1878,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552, upload-time = "2025-05-21T18:55:22.152Z" }, ] +[[package]] +name = "uc-micro-py" +version = "1.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/91/7a/146a99696aee0609e3712f2b44c6274566bc368dfe8375191278045186b8/uc-micro-py-1.0.3.tar.gz", hash = "sha256:d321b92cff673ec58027c04015fcaa8bb1e005478643ff4a500882eaab88c48a", size = 6043, upload-time = "2024-02-09T16:52:01.654Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/37/87/1f677586e8ac487e29672e4b17455758fce261de06a0d086167bb760361a/uc_micro_py-1.0.3-py3-none-any.whl", hash = "sha256:db1dffff340817673d7b466ec86114a9dc0e9d4d9b5ba229d9d60e5c12600cd5", size = 6229, upload-time = "2024-02-09T16:52:00.371Z" }, +] + [[package]] name = "urllib3" version = "2.5.0"