langbot-plugin 0.1.0__tar.gz

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 (36) hide show
  1. langbot_plugin-0.1.0/.gitignore +174 -0
  2. langbot_plugin-0.1.0/.python-version +1 -0
  3. langbot_plugin-0.1.0/PKG-INFO +30 -0
  4. langbot_plugin-0.1.0/README.md +17 -0
  5. langbot_plugin-0.1.0/pyproject.toml +23 -0
  6. langbot_plugin-0.1.0/src/langbot_plugin/__init__.py +0 -0
  7. langbot_plugin-0.1.0/src/langbot_plugin/api/__init__.py +0 -0
  8. langbot_plugin-0.1.0/src/langbot_plugin/cli/__init__.py +35 -0
  9. langbot_plugin-0.1.0/src/langbot_plugin/cli/command.py +9 -0
  10. langbot_plugin-0.1.0/src/langbot_plugin/cli/commands/__init__.py +0 -0
  11. langbot_plugin-0.1.0/src/langbot_plugin/entities/__init__.py +0 -0
  12. langbot_plugin-0.1.0/src/langbot_plugin/entities/io/__init__.py +0 -0
  13. langbot_plugin-0.1.0/src/langbot_plugin/entities/io/errors.py +31 -0
  14. langbot_plugin-0.1.0/src/langbot_plugin/entities/io/req.py +16 -0
  15. langbot_plugin-0.1.0/src/langbot_plugin/entities/io/resp.py +19 -0
  16. langbot_plugin-0.1.0/src/langbot_plugin/runtime/__init__.py +0 -0
  17. langbot_plugin-0.1.0/src/langbot_plugin/runtime/__main__.py +10 -0
  18. langbot_plugin-0.1.0/src/langbot_plugin/runtime/app.py +60 -0
  19. langbot_plugin-0.1.0/src/langbot_plugin/runtime/controller/__init__.py +0 -0
  20. langbot_plugin-0.1.0/src/langbot_plugin/runtime/controller/stdio/__init__.py +0 -0
  21. langbot_plugin-0.1.0/src/langbot_plugin/runtime/controller/stdio/server.py +18 -0
  22. langbot_plugin-0.1.0/src/langbot_plugin/runtime/controller/ws/__init__.py +0 -0
  23. langbot_plugin-0.1.0/src/langbot_plugin/runtime/controller/ws/server.py +74 -0
  24. langbot_plugin-0.1.0/src/langbot_plugin/runtime/core/__init__.py +0 -0
  25. langbot_plugin-0.1.0/src/langbot_plugin/runtime/io/connection.py +19 -0
  26. langbot_plugin-0.1.0/src/langbot_plugin/runtime/io/connections/__init__.py +0 -0
  27. langbot_plugin-0.1.0/src/langbot_plugin/runtime/io/connections/stdio.py +24 -0
  28. langbot_plugin-0.1.0/src/langbot_plugin/runtime/io/connections/ws.py +26 -0
  29. langbot_plugin-0.1.0/src/langbot_plugin/runtime/io/handler.py +129 -0
  30. langbot_plugin-0.1.0/src/langbot_plugin/runtime/io/handlers/__init__.py +0 -0
  31. langbot_plugin-0.1.0/src/langbot_plugin/runtime/io/handlers/control.py +23 -0
  32. langbot_plugin-0.1.0/src/langbot_plugin/runtime/service/__init__.py +0 -0
  33. langbot_plugin-0.1.0/src/langbot_plugin/utils/__init__.py +0 -0
  34. langbot_plugin-0.1.0/src/langbot_plugin/utils/importutil.py +38 -0
  35. langbot_plugin-0.1.0/src/langbot_plugin/version.py +1 -0
  36. langbot_plugin-0.1.0/uv.lock +444 -0
@@ -0,0 +1,174 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # PyInstaller
30
+ # Usually these files are written by a python script from a template
31
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
32
+ *.manifest
33
+ *.spec
34
+
35
+ # Installer logs
36
+ pip-log.txt
37
+ pip-delete-this-directory.txt
38
+
39
+ # Unit test / coverage reports
40
+ htmlcov/
41
+ .tox/
42
+ .nox/
43
+ .coverage
44
+ .coverage.*
45
+ .cache
46
+ nosetests.xml
47
+ coverage.xml
48
+ *.cover
49
+ *.py,cover
50
+ .hypothesis/
51
+ .pytest_cache/
52
+ cover/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+ db.sqlite3-journal
63
+
64
+ # Flask stuff:
65
+ instance/
66
+ .webassets-cache
67
+
68
+ # Scrapy stuff:
69
+ .scrapy
70
+
71
+ # Sphinx documentation
72
+ docs/_build/
73
+
74
+ # PyBuilder
75
+ .pybuilder/
76
+ target/
77
+
78
+ # Jupyter Notebook
79
+ .ipynb_checkpoints
80
+
81
+ # IPython
82
+ profile_default/
83
+ ipython_config.py
84
+
85
+ # pyenv
86
+ # For a library or package, you might want to ignore these files since the code is
87
+ # intended to run in multiple environments; otherwise, check them in:
88
+ # .python-version
89
+
90
+ # pipenv
91
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
93
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
94
+ # install all needed dependencies.
95
+ #Pipfile.lock
96
+
97
+ # UV
98
+ # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
99
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
100
+ # commonly ignored for libraries.
101
+ #uv.lock
102
+
103
+ # poetry
104
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
105
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
106
+ # commonly ignored for libraries.
107
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
108
+ #poetry.lock
109
+
110
+ # pdm
111
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
112
+ #pdm.lock
113
+ # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
114
+ # in version control.
115
+ # https://pdm.fming.dev/latest/usage/project/#working-with-version-control
116
+ .pdm.toml
117
+ .pdm-python
118
+ .pdm-build/
119
+
120
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
121
+ __pypackages__/
122
+
123
+ # Celery stuff
124
+ celerybeat-schedule
125
+ celerybeat.pid
126
+
127
+ # SageMath parsed files
128
+ *.sage.py
129
+
130
+ # Environments
131
+ .env
132
+ .venv
133
+ env/
134
+ venv/
135
+ ENV/
136
+ env.bak/
137
+ venv.bak/
138
+
139
+ # Spyder project settings
140
+ .spyderproject
141
+ .spyproject
142
+
143
+ # Rope project settings
144
+ .ropeproject
145
+
146
+ # mkdocs documentation
147
+ /site
148
+
149
+ # mypy
150
+ .mypy_cache/
151
+ .dmypy.json
152
+ dmypy.json
153
+
154
+ # Pyre type checker
155
+ .pyre/
156
+
157
+ # pytype static type analyzer
158
+ .pytype/
159
+
160
+ # Cython debug symbols
161
+ cython_debug/
162
+
163
+ # PyCharm
164
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
165
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
166
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
167
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
168
+ #.idea/
169
+
170
+ # Ruff stuff:
171
+ .ruff_cache/
172
+
173
+ # PyPI configuration file
174
+ .pypirc
@@ -0,0 +1 @@
1
+ 3.12
@@ -0,0 +1,30 @@
1
+ Metadata-Version: 2.4
2
+ Name: langbot-plugin
3
+ Version: 0.1.0
4
+ Summary: This package contains the SDK, CLI for building plugins for LangBot, plus the runtime for hosting LangBot plugins
5
+ Author-email: Junyan Qin <rockchinq@gmail.com>
6
+ Requires-Python: >=3.10
7
+ Requires-Dist: mypy>=1.16.0
8
+ Requires-Dist: pydantic>=2.11.5
9
+ Requires-Dist: ruff>=0.11.12
10
+ Requires-Dist: textual>=3.2.0
11
+ Requires-Dist: websockets>=15.0.1
12
+ Description-Content-Type: text/markdown
13
+
14
+ # langbot-plugin-sdk
15
+
16
+ ## Connection
17
+
18
+ ### with LangBot
19
+
20
+ - Stdio
21
+ - Passively
22
+ - WebSocket
23
+ - Server: `:5400 /control/ws`
24
+
25
+ ### with Plugins
26
+
27
+ - Stdio
28
+ - Actively
29
+ - WebSocket (Debug)
30
+ - Server: `:5401 /debug/ws`
@@ -0,0 +1,17 @@
1
+ # langbot-plugin-sdk
2
+
3
+ ## Connection
4
+
5
+ ### with LangBot
6
+
7
+ - Stdio
8
+ - Passively
9
+ - WebSocket
10
+ - Server: `:5400 /control/ws`
11
+
12
+ ### with Plugins
13
+
14
+ - Stdio
15
+ - Actively
16
+ - WebSocket (Debug)
17
+ - Server: `:5401 /debug/ws`
@@ -0,0 +1,23 @@
1
+ [project]
2
+ name = "langbot-plugin"
3
+ version = "0.1.0"
4
+ description = "This package contains the SDK, CLI for building plugins for LangBot, plus the runtime for hosting LangBot plugins"
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "Junyan Qin", email = "rockchinq@gmail.com" }
8
+ ]
9
+ requires-python = ">=3.10"
10
+ dependencies = [
11
+ "mypy>=1.16.0",
12
+ "pydantic>=2.11.5",
13
+ "ruff>=0.11.12",
14
+ "textual>=3.2.0",
15
+ "websockets>=15.0.1",
16
+ ]
17
+
18
+ [project.scripts]
19
+ lbp = "langbot_plugin.cli:main"
20
+
21
+ [build-system]
22
+ requires = ["hatchling"]
23
+ build-backend = "hatchling.build"
File without changes
@@ -0,0 +1,35 @@
1
+ import argparse
2
+ from langbot_plugin.version import __version__
3
+ from langbot_plugin.runtime import __main__ as runtime_main
4
+
5
+
6
+ def main():
7
+ parser = argparse.ArgumentParser(description="LangBot Plugin CLI")
8
+
9
+ subparsers = parser.add_subparsers(dest="command")
10
+
11
+ version_parser = subparsers.add_parser("ver", help="Show the version of the CLI")
12
+
13
+ init_parser = subparsers.add_parser("init", help="Initialize a new plugin")
14
+ init_parser.add_argument(
15
+ "--name", "-n", action="store", type=str, help="The name of the plugin"
16
+ )
17
+
18
+ runtime_parser = subparsers.add_parser("rt", help="Run the runtime")
19
+ runtime_parser.add_argument(
20
+ "--stdio-control", "-s", action="store_true", default=False
21
+ )
22
+ runtime_parser.add_argument("--ws-control-port", type=int, default=5400)
23
+ runtime_parser.add_argument("--ws-debug-port", type=int, default=5401)
24
+
25
+ args = parser.parse_args()
26
+
27
+ match args.command:
28
+ case "ver":
29
+ print(f"LangBot Plugin CLI v{__version__}")
30
+ case "init":
31
+ print(f"Initializing plugin {args.name}")
32
+ case "rt":
33
+ runtime_main.main(args)
34
+ case _:
35
+ print(f"Unknown command: {args.command}")
@@ -0,0 +1,9 @@
1
+ from __future__ import annotations
2
+
3
+ import abc
4
+
5
+
6
+ class CLICommand(abc.ABC):
7
+ @abc.abstractmethod
8
+ def run(self):
9
+ pass
@@ -0,0 +1,31 @@
1
+ from __future__ import annotations
2
+
3
+
4
+ class ConnectionClosedError(Exception):
5
+ """The connection is closed."""
6
+
7
+ def __init__(self, message: str):
8
+ self.message = message
9
+
10
+ def __str__(self):
11
+ return self.message
12
+
13
+
14
+ class ActionCallTimeoutError(Exception):
15
+ """The action call timed out."""
16
+
17
+ def __init__(self, message: str):
18
+ self.message = message
19
+
20
+ def __str__(self):
21
+ return self.message
22
+
23
+
24
+ class ActionCallError(Exception):
25
+ """The action call failed."""
26
+
27
+ def __init__(self, message: str):
28
+ self.message = message
29
+
30
+ def __str__(self):
31
+ return self.message
@@ -0,0 +1,16 @@
1
+ from __future__ import annotations
2
+
3
+ import pydantic
4
+ from typing import Any
5
+
6
+
7
+ class ActionRequest(pydantic.BaseModel):
8
+ seq_id: int = pydantic.Field(..., description="The sequence id of the request")
9
+ action: str
10
+ data: dict[str, Any]
11
+
12
+ @classmethod
13
+ def make_request(
14
+ cls, seq_id: int, action: str, data: dict[str, Any]
15
+ ) -> ActionRequest:
16
+ return cls(seq_id=seq_id, action=action, data=data)
@@ -0,0 +1,19 @@
1
+ from __future__ import annotations
2
+
3
+ import pydantic
4
+ from typing import Any, Optional
5
+
6
+
7
+ class ActionResponse(pydantic.BaseModel):
8
+ seq_id: Optional[int] = None
9
+ code: int = pydantic.Field(..., description="The code of the response")
10
+ message: str = pydantic.Field(..., description="The message of the response")
11
+ data: dict[str, Any] = pydantic.Field(..., description="The data of the response")
12
+
13
+ @classmethod
14
+ def success(cls, data: dict[str, Any]) -> ActionResponse:
15
+ return cls(seq_id=0, code=0, message="success", data=data)
16
+
17
+ @classmethod
18
+ def error(cls, message: str) -> ActionResponse:
19
+ return cls(code=1, message=message, data={})
@@ -0,0 +1,10 @@
1
+ # handler for command
2
+ import asyncio
3
+ import argparse
4
+
5
+ from langbot_plugin.runtime.app import Application
6
+
7
+
8
+ def main(args: argparse.Namespace):
9
+ app = Application(args)
10
+ asyncio.run(app.run())
@@ -0,0 +1,60 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ from enum import Enum
5
+
6
+ import asyncio
7
+
8
+ from langbot_plugin.runtime.controller.stdio import server as stdio_controller_server
9
+ from langbot_plugin.runtime.controller.ws import server as ws_controller_server
10
+ from langbot_plugin.runtime.io import handler
11
+
12
+
13
+ class ControlConnectionMode(Enum):
14
+ STDIO = "stdio"
15
+ WS = "ws"
16
+
17
+
18
+ class Application:
19
+ """Runtime application context."""
20
+
21
+ handler_manager: handler.HandlerManager
22
+
23
+ control_connection_mode: ControlConnectionMode
24
+
25
+ stdio_server: stdio_controller_server.StdioServer | None = (
26
+ None # stdio control server
27
+ )
28
+ ws_server: ws_controller_server.WebSocketServer | None = (
29
+ None # ws control/debug server
30
+ )
31
+
32
+ def __init__(self, args: argparse.Namespace):
33
+ self.args = args
34
+ self.handler_manager = handler.HandlerManager()
35
+
36
+ if args.stdio_control:
37
+ self.control_connection_mode = ControlConnectionMode.STDIO
38
+ else:
39
+ self.control_connection_mode = ControlConnectionMode.WS
40
+
41
+ # build controllers layer
42
+ if self.control_connection_mode == ControlConnectionMode.STDIO:
43
+ self.stdio_server = stdio_controller_server.StdioServer(
44
+ self.handler_manager
45
+ )
46
+
47
+ self.ws_server = ws_controller_server.WebSocketServer(
48
+ self.args.ws_control_port, self.args.ws_debug_port, self.handler_manager
49
+ )
50
+
51
+ async def run(self):
52
+ tasks = []
53
+
54
+ if self.stdio_server:
55
+ tasks.append(self.stdio_server.run())
56
+
57
+ if self.ws_server:
58
+ tasks.append(self.ws_server.run())
59
+
60
+ await asyncio.gather(*tasks)
@@ -0,0 +1,18 @@
1
+ # Stdio server for LangBot control connection
2
+ from __future__ import annotations
3
+
4
+ from langbot_plugin.runtime.io.connections import stdio as stdio_connection
5
+ from langbot_plugin.runtime.io.handlers import control as control_handler
6
+ from langbot_plugin.runtime.io import handler as io_handler
7
+
8
+
9
+ class StdioServer:
10
+ def __init__(self, handler_manager: io_handler.HandlerManager):
11
+ self.handler_manager = handler_manager
12
+
13
+ async def run(self):
14
+ print("Starting Stdio server")
15
+ connection = stdio_connection.StdioConnection()
16
+ handler = control_handler.ControlConnectionHandler(connection)
17
+ task = self.handler_manager.set_control_handler(handler)
18
+ await task
@@ -0,0 +1,74 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import websockets
5
+
6
+ from langbot_plugin.runtime.io.connections import ws as ws_connection
7
+ from langbot_plugin.runtime.io.handlers import control as control_handler
8
+ from langbot_plugin.runtime.io import handler as io_handler
9
+
10
+
11
+ class ControlConnectionWebSocketServer:
12
+ """The server for control connection WebSocket connections."""
13
+
14
+ def __init__(self, port: int, handler_manager: io_handler.HandlerManager):
15
+ self.port = port
16
+ self.handler_manager = handler_manager
17
+
18
+ async def run(self):
19
+ server = await websockets.serve(self.handle_connection, "0.0.0.0", self.port)
20
+ await server.wait_closed()
21
+
22
+ async def handle_connection(self, websocket: websockets.ServerConnection):
23
+ print(f"New control connection from {websocket.remote_address}")
24
+ connection = ws_connection.WebSocketConnection(websocket)
25
+ handler = control_handler.ControlConnectionHandler(connection)
26
+ task = self.handler_manager.set_control_handler(handler)
27
+ await task
28
+
29
+
30
+ class DebugConnectionWebSocketServer:
31
+ """The server for debug connection WebSocket connections."""
32
+
33
+ def __init__(self, port: int, handler_manager: io_handler.HandlerManager):
34
+ self.port = port
35
+ self.handler_manager = handler_manager
36
+
37
+ async def run(self):
38
+ server = await websockets.serve(self.handle_connection, "0.0.0.0", self.port)
39
+ await server.wait_closed()
40
+
41
+ async def handle_connection(self, websocket: websockets.ServerConnection):
42
+ print(f"New connection from {websocket.remote_address}")
43
+ await websocket.send("Hello, world!")
44
+ await websocket.close()
45
+
46
+
47
+ class WebSocketServer:
48
+ """The server for control connection WebSocket connections."""
49
+
50
+ def __init__(
51
+ self,
52
+ control_port: int,
53
+ debug_port: int,
54
+ handler_manager: io_handler.HandlerManager,
55
+ ):
56
+ self.control_port = control_port
57
+ self.debug_port = debug_port
58
+ self.handler_manager = handler_manager
59
+
60
+ async def run(self):
61
+ print(
62
+ f"Starting WebSocket server on port {self.control_port} for control connections"
63
+ )
64
+ print(
65
+ f"Starting WebSocket server on port {self.debug_port} for debug connections"
66
+ )
67
+ control_server = ControlConnectionWebSocketServer(
68
+ self.control_port, self.handler_manager
69
+ )
70
+ debug_server = DebugConnectionWebSocketServer(
71
+ self.debug_port, self.handler_manager
72
+ )
73
+
74
+ await asyncio.gather(control_server.run(), debug_server.run())
@@ -0,0 +1,19 @@
1
+ from __future__ import annotations
2
+
3
+ import abc
4
+
5
+
6
+ class Connection(abc.ABC):
7
+ """The abstract base class for all connections."""
8
+
9
+ @abc.abstractmethod
10
+ async def send(self, message: str) -> None:
11
+ pass
12
+
13
+ @abc.abstractmethod
14
+ async def receive(self) -> str:
15
+ pass
16
+
17
+ @abc.abstractmethod
18
+ async def close(self) -> None:
19
+ pass
@@ -0,0 +1,24 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+
5
+ from langbot_plugin.runtime.io import connection
6
+
7
+
8
+ class StdioConnection(connection.Connection):
9
+ """The connection for Stdio connections."""
10
+
11
+ def __init__(self):
12
+ pass
13
+
14
+ async def send(self, message: str) -> None:
15
+ print(message)
16
+
17
+ async def receive(self) -> str:
18
+ while True:
19
+ s = await asyncio.to_thread(input)
20
+ if s.startswith("{") and s.endswith("}"):
21
+ return s
22
+
23
+ async def close(self) -> None:
24
+ pass
@@ -0,0 +1,26 @@
1
+ from __future__ import annotations
2
+
3
+ import websockets
4
+
5
+ from langbot_plugin.runtime.io import connection as io_connection
6
+ from langbot_plugin.entities.io.errors import ConnectionClosedError
7
+
8
+
9
+ class WebSocketConnection(io_connection.Connection):
10
+ """The connection for WebSocket connections."""
11
+
12
+ def __init__(self, websocket: websockets.ServerConnection):
13
+ self.websocket = websocket
14
+
15
+ async def send(self, message: str) -> None:
16
+ await self.websocket.send(message, text=True)
17
+
18
+ async def receive(self) -> str:
19
+ try:
20
+ data = await self.websocket.recv(decode=True)
21
+ return data
22
+ except websockets.exceptions.ConnectionClosed:
23
+ raise ConnectionClosedError("Connection closed")
24
+
25
+ async def close(self) -> None:
26
+ await self.websocket.close()