Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ Preliminary CLI Script #555

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions kai/jsonrpc/core.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import re
import threading
from typing import Any, Callable, Literal, Optional, overload

Expand Down Expand Up @@ -72,9 +73,18 @@ def handle_request(self, request: JsonRpcRequest, server: "JsonRpcServer") -> No

log.log(TRACE, "Calling method: %s", request.method)

self.notify_callbacks[request.method](
request=request, server=server, app=self
)
any_executed = False
for method, callback in self.notify_callbacks.items():
# method is a regex, check if request.method matches it
if re.match(method, request.method):
log.log(TRACE, "Calling method: %s", request.method)

callback(request=request, server=server, app=self)
any_executed = True

if not any_executed:
log.error(f"Notify method not found: {request.method}")
log.error(f"Notify methods: {self.notify_callbacks.keys()}")

@overload
def add(
Expand Down
123 changes: 123 additions & 0 deletions kai/rpc_server/cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import json
import os
import subprocess # trunk-ignore(bandit/B404)
import sys
from io import BufferedReader, BufferedWriter
from pathlib import Path
from typing import Any, Literal, cast

import yaml
from pydantic import BaseModel, Field, FilePath
from pydantic_settings import BaseSettings, CliApp, CliSubCommand

from kai.jsonrpc.core import JsonRpcApplication, JsonRpcServer
from kai.jsonrpc.models import JsonRpcError, JsonRpcRequest
from kai.jsonrpc.streams import BareJsonStream


class RpcConfig(BaseModel):
process: str
request_timeout: float | None = 240


class RunFile(BaseSettings):
rpc_config: RpcConfig | FilePath

input: FilePath = Field(
..., description="Sequence of requests to make from a YAML or JSON file."
)
output: Path | None = Field(
None, description="Output file to write the results to."
)
output_format: Literal["yaml", "json"] = Field(
"yaml", description="Output format that the results should be written in."
)

def cli_cmd(self) -> None:
if isinstance(self.rpc_config, Path):
with open(self.rpc_config, "r") as f:
self.rpc_config = RpcConfig.model_validate(yaml.safe_load(f.read()))

input_data_raw = yaml.safe_load(open(self.input, "r"))
if not isinstance(input_data_raw, list):
raise Exception(
"Input file must be a list of dicts with keys 'method' and 'params'"
)

input_data: list[JsonRpcRequest] = []
for request in input_data_raw:
input_data.append(JsonRpcRequest.model_validate(request))

output: list[dict[str, Any] | None] = []

# trunk-ignore-begin(bandit/B603)
rpc_subprocess = subprocess.Popen(
self.rpc_config.process.split(),
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
# stderr=subprocess.PIPE,
env=os.environ,
)
# trunk-ignore-end(bandit/B603)

app = JsonRpcApplication()

@app.add_notify(method="*")
def recv_notify(
app: JsonRpcApplication,
server: JsonRpcServer,
id: Any,
params: dict[str, Any],
) -> None:
output.append(params)

rpc_server = JsonRpcServer(
json_rpc_stream=BareJsonStream(
cast(BufferedReader, rpc_subprocess.stdout),
cast(BufferedWriter, rpc_subprocess.stdin),
),
app=app,
request_timeout=self.rpc_config.request_timeout,
)

rpc_server.start()

try:
for request in input_data:
response = rpc_server.send_request(
method=request.method, params=request.params
)

if isinstance(response, JsonRpcError):
raise Exception(
f"Failed to get response for {request['method']} - {response.code} {response.message}"
)
elif response is None:
output.append(response)
else:
output.append(response.model_dump())

if self.output is not None:
with open(self.output, "w") as f:
if self.output_format == "json":
f.write(json.dumps(output))
else:
f.write(yaml.dump(output))

finally:
rpc_subprocess.terminate()
rpc_subprocess.wait()
rpc_server.stop()


class KaiCli(BaseSettings):
run_file: CliSubCommand[RunFile] = Field(
..., description="Run a sequence of JSON-RPC requests from a file."
)

def cli_cmd(self) -> None:
CliApp.run_subcommand(self)


if __name__ == "__main__":
_cmd = CliApp.run(KaiCli, sys.argv[1:])
4 changes: 1 addition & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ dependencies = [
"aiohttp==3.9.3; python_version >= '3.12'",
"gitpython==3.1.43",
"pydantic==2.8.2",
"pydantic-settings==2.4.0",
"pydantic-settings==2.6.1",
"requests==2.32.3",
"pygments==2.18.0",
"python-dateutil==2.8.2",
Expand Down Expand Up @@ -58,8 +58,6 @@ dependencies = [
# --- Possibly can be removed ---
"async-timeout==4.0.3",
"asgiref==3.7.2",
"click==8.1.7", # For potential CLI stuff
"typer==0.9.0", # For potential CLI stuff
"loguru==0.7.2", # For potential logging improvements
"unidiff==0.7.5",
]
Expand Down
Loading