jingyi 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.
jingyi-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,28 @@
1
+ Metadata-Version: 2.4
2
+ Name: jingyi
3
+ Version: 0.1.0
4
+ Summary: OpenClaw-inspired Python demo agent
5
+ Requires-Python: >=3.11
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: typer<1.0,>=0.12
8
+ Provides-Extra: dev
9
+ Requires-Dist: pytest<9.0,>=8.0; extra == "dev"
10
+
11
+ # jingyi
12
+
13
+ An OpenClaw-inspired Python demo agent package.
14
+
15
+ ## Install
16
+
17
+ ```bash
18
+ pip install jingyi
19
+ ```
20
+
21
+ ## CLI
22
+
23
+ ```bash
24
+ jingyi health
25
+ jingyi status
26
+ jingyi message send --to channel:123 --message "hello" --channel discord
27
+ ```
28
+
jingyi-0.1.0/README.md ADDED
@@ -0,0 +1,18 @@
1
+ # jingyi
2
+
3
+ An OpenClaw-inspired Python demo agent package.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install jingyi
9
+ ```
10
+
11
+ ## CLI
12
+
13
+ ```bash
14
+ jingyi health
15
+ jingyi status
16
+ jingyi message send --to channel:123 --message "hello" --channel discord
17
+ ```
18
+
@@ -0,0 +1,2 @@
1
+ """jingyi: OpenClaw-inspired Python demo agent."""
2
+
@@ -0,0 +1,2 @@
1
+ """Channel layer for jingyi."""
2
+
@@ -0,0 +1,2 @@
1
+ """Plugin channel adapters for jingyi."""
2
+
@@ -0,0 +1,24 @@
1
+ from .types import ChannelActionContext
2
+
3
+
4
+ def handle_discord_action(ctx: ChannelActionContext) -> dict:
5
+ if ctx.action == "send":
6
+ return {
7
+ "ok": True,
8
+ "channel": ctx.channel,
9
+ "action": ctx.action,
10
+ "message_id": "discord-msg-001",
11
+ "to": str(ctx.params.get("to", "")),
12
+ }
13
+ if ctx.action == "poll":
14
+ return {
15
+ "ok": True,
16
+ "channel": ctx.channel,
17
+ "action": ctx.action,
18
+ "poll_id": "discord-poll-001",
19
+ "to": str(ctx.params.get("to", "")),
20
+ }
21
+ if ctx.action in ("react", "delete"):
22
+ return {"ok": True, "channel": ctx.channel, "action": ctx.action}
23
+ raise ValueError(f"Action {ctx.action} not supported for channel {ctx.channel}")
24
+
@@ -0,0 +1,31 @@
1
+ from typing import Dict, Optional
2
+
3
+ from .discord_plugin import handle_discord_action
4
+ from .types import ChannelActionContext, ChannelPlugin
5
+
6
+
7
+ def _default_handler(ctx: ChannelActionContext) -> dict:
8
+ if ctx.action not in ("send", "poll", "react", "delete"):
9
+ raise ValueError(f"Action {ctx.action} not supported for channel {ctx.channel}")
10
+ return {
11
+ "ok": True,
12
+ "channel": ctx.channel,
13
+ "action": ctx.action,
14
+ "to": str(ctx.params.get("to", "")),
15
+ "message_id": f"{ctx.channel}-msg-001",
16
+ }
17
+
18
+
19
+ PLUGIN_REGISTRY: Dict[str, ChannelPlugin] = {
20
+ "discord": ChannelPlugin(id="discord", handle_action=handle_discord_action),
21
+ "telegram": ChannelPlugin(id="telegram", handle_action=_default_handler),
22
+ "whatsapp": ChannelPlugin(id="whatsapp", handle_action=_default_handler),
23
+ }
24
+
25
+
26
+ def dispatch_channel_message_action(ctx: ChannelActionContext) -> Optional[dict]:
27
+ plugin = PLUGIN_REGISTRY.get(ctx.channel)
28
+ if not plugin:
29
+ return None
30
+ return plugin.handle_action(ctx)
31
+
@@ -0,0 +1,23 @@
1
+ from dataclasses import dataclass
2
+ from typing import Callable, Dict, Literal, Optional
3
+
4
+ ChannelId = Literal["discord", "telegram", "whatsapp"]
5
+ ChannelMessageActionName = Literal["send", "poll", "broadcast", "react", "delete"]
6
+
7
+
8
+ @dataclass
9
+ class ChannelActionContext:
10
+ channel: ChannelId
11
+ action: ChannelMessageActionName
12
+ params: Dict[str, object]
13
+ account_id: Optional[str] = None
14
+
15
+
16
+ ActionHandler = Callable[[ChannelActionContext], Dict[str, object]]
17
+
18
+
19
+ @dataclass
20
+ class ChannelPlugin:
21
+ id: ChannelId
22
+ handle_action: ActionHandler
23
+
@@ -0,0 +1,27 @@
1
+ from dataclasses import dataclass
2
+ from typing import Dict, Optional
3
+
4
+
5
+ @dataclass(frozen=True)
6
+ class ChannelMeta:
7
+ id: str
8
+ label: str
9
+ blurb: str
10
+
11
+
12
+ CHANNEL_ORDER = ("telegram", "whatsapp", "discord")
13
+ CHANNEL_ALIASES: Dict[str, str] = {"tg": "telegram", "wa": "whatsapp"}
14
+ CHANNEL_META: Dict[str, ChannelMeta] = {
15
+ "telegram": ChannelMeta("telegram", "Telegram", "Bot API style channel"),
16
+ "whatsapp": ChannelMeta("whatsapp", "WhatsApp", "Web session style channel"),
17
+ "discord": ChannelMeta("discord", "Discord", "Bot token style channel"),
18
+ }
19
+
20
+
21
+ def normalize_channel_id(raw: Optional[str]) -> Optional[str]:
22
+ if not raw:
23
+ return None
24
+ key = raw.strip().lower()
25
+ key = CHANNEL_ALIASES.get(key, key)
26
+ return key if key in CHANNEL_META else None
27
+
@@ -0,0 +1,2 @@
1
+ """CLI layer for jingyi."""
2
+
@@ -0,0 +1,58 @@
1
+ from typing import List, Optional
2
+
3
+ import typer
4
+
5
+ from ..commands.message import message_command
6
+ from ..config import load_config
7
+ from ..runtime import default_runtime
8
+
9
+ app = typer.Typer(help="OpenClaw-inspired Python demo")
10
+ message_app = typer.Typer(help="Send messages and channel actions")
11
+ app.add_typer(message_app, name="message")
12
+
13
+
14
+ @message_app.command("send")
15
+ def message_send(
16
+ to: str = typer.Option(..., "--to", help="Target peer"),
17
+ message: str = typer.Option("", "--message", help="Message text"),
18
+ channel: Optional[str] = typer.Option(None, "--channel", help="Channel id"),
19
+ dry_run: bool = typer.Option(False, "--dry-run", help="Do not execute channel call"),
20
+ ) -> None:
21
+ cfg = load_config()
22
+ data = {
23
+ "action": "send",
24
+ "to": to,
25
+ "message": message,
26
+ "channel": channel,
27
+ "dry_run": dry_run,
28
+ }
29
+ message_command(data, default_runtime, cfg)
30
+
31
+
32
+ @message_app.command("poll")
33
+ def message_poll(
34
+ to: str = typer.Option(..., "--to", help="Target peer"),
35
+ poll_question: str = typer.Option(..., "--poll-question", help="Poll question"),
36
+ poll_option: List[str] = typer.Option(..., "--poll-option", help="Poll options"),
37
+ channel: Optional[str] = typer.Option(None, "--channel", help="Channel id"),
38
+ dry_run: bool = typer.Option(False, "--dry-run", help="Do not execute channel call"),
39
+ ) -> None:
40
+ cfg = load_config()
41
+ data = {
42
+ "action": "poll",
43
+ "to": to,
44
+ "pollQuestion": poll_question,
45
+ "pollOption": poll_option,
46
+ "channel": channel,
47
+ "dry_run": dry_run,
48
+ }
49
+ message_command(data, default_runtime, cfg)
50
+
51
+
52
+ @app.command("routes")
53
+ def show_routes() -> None:
54
+ typer.echo("message send")
55
+ typer.echo("message poll")
56
+ typer.echo("health")
57
+ typer.echo("status")
58
+
@@ -0,0 +1,17 @@
1
+ from typing import List
2
+
3
+ import typer
4
+
5
+
6
+ def try_route_cli(argv: List[str]) -> bool:
7
+ if len(argv) < 2:
8
+ return False
9
+ cmd = argv[1]
10
+ if cmd == "health":
11
+ typer.echo("ok")
12
+ raise typer.Exit(code=0)
13
+ if cmd == "status":
14
+ typer.echo("demo-status: healthy")
15
+ raise typer.Exit(code=0)
16
+ return False
17
+
@@ -0,0 +1,2 @@
1
+ """Command handlers for jingyi."""
2
+
@@ -0,0 +1,28 @@
1
+ from dataclasses import dataclass
2
+ from typing import Dict
3
+
4
+ from ..config import DemoConfig
5
+ from ..infra.outbound.message_action_runner import RunMessageActionParams, run_message_action
6
+ from ..runtime import Runtime
7
+
8
+
9
+ @dataclass
10
+ class MessageResult:
11
+ data: Dict[str, object]
12
+
13
+
14
+ def message_command(opts: Dict[str, object], runtime: Runtime, cfg: DemoConfig) -> MessageResult:
15
+ raw_action = str(opts.get("action", "send")).strip().lower()
16
+ if raw_action not in ("send", "poll", "broadcast", "react", "delete"):
17
+ raise ValueError(f"Unknown message action: {raw_action}")
18
+ result = run_message_action(
19
+ RunMessageActionParams(
20
+ cfg=cfg,
21
+ action=raw_action, # type: ignore[arg-type]
22
+ params=opts,
23
+ dry_run=bool(opts.get("dry_run", False)),
24
+ )
25
+ )
26
+ runtime.log(f"[{result['handled_by']}] {result['action']} on {result['channel']}")
27
+ return MessageResult(data=result)
28
+
@@ -0,0 +1,16 @@
1
+ from dataclasses import dataclass, field
2
+ from typing import Dict, List
3
+
4
+
5
+ @dataclass
6
+ class DemoConfig:
7
+ default_channel: str = "discord"
8
+ enabled_channels: List[str] = field(
9
+ default_factory=lambda: ["discord", "telegram", "whatsapp"]
10
+ )
11
+ bindings: Dict[str, str] = field(default_factory=dict)
12
+
13
+
14
+ def load_config() -> DemoConfig:
15
+ return DemoConfig()
16
+
@@ -0,0 +1,2 @@
1
+ """Infrastructure layer for jingyi."""
2
+
@@ -0,0 +1,2 @@
1
+ """Outbound execution layer for jingyi."""
2
+
@@ -0,0 +1,85 @@
1
+ from dataclasses import dataclass
2
+ from typing import Dict, Optional
3
+
4
+ from ...channels.plugins.message_actions import dispatch_channel_message_action
5
+ from ...channels.plugins.types import ChannelActionContext, ChannelMessageActionName
6
+ from ...channels.registry import normalize_channel_id
7
+ from ...config import DemoConfig
8
+
9
+
10
+ @dataclass
11
+ class RunMessageActionParams:
12
+ cfg: DemoConfig
13
+ action: ChannelMessageActionName
14
+ params: Dict[str, object]
15
+ default_account_id: Optional[str] = None
16
+ dry_run: bool = False
17
+
18
+
19
+ def _resolve_channel(cfg: DemoConfig, params: Dict[str, object]) -> str:
20
+ raw = params.get("channel")
21
+ channel = normalize_channel_id(str(raw)) if raw is not None else None
22
+ if channel is None:
23
+ channel = normalize_channel_id(cfg.default_channel)
24
+ if channel is None:
25
+ raise ValueError("No valid channel resolved.")
26
+ if channel not in cfg.enabled_channels:
27
+ raise ValueError(f"Channel {channel} is not enabled.")
28
+ return channel
29
+
30
+
31
+ def _normalize_target(params: Dict[str, object]) -> None:
32
+ target = str(params.get("target", "")).strip()
33
+ to = str(params.get("to", "")).strip()
34
+ if target and not to:
35
+ params["to"] = target
36
+ if not str(params.get("to", "")).strip():
37
+ raise ValueError("Action requires a target. Pass --to or --target.")
38
+
39
+
40
+ def run_message_action(input_data: RunMessageActionParams) -> dict:
41
+ args = dict(input_data.params)
42
+ channel = _resolve_channel(input_data.cfg, args)
43
+
44
+ if input_data.action in ("send", "poll", "react", "delete"):
45
+ _normalize_target(args)
46
+
47
+ if input_data.action == "broadcast":
48
+ raw_targets = args.get("targets", [])
49
+ if not isinstance(raw_targets, list) or not raw_targets:
50
+ raise ValueError("Broadcast requires non-empty --targets.")
51
+ if input_data.dry_run:
52
+ return {"kind": "broadcast", "handled_by": "dry-run", "results": raw_targets}
53
+ return {
54
+ "kind": "broadcast",
55
+ "handled_by": "core",
56
+ "results": [{"channel": channel, "to": target, "ok": True} for target in raw_targets],
57
+ }
58
+
59
+ if input_data.dry_run:
60
+ return {
61
+ "kind": "action",
62
+ "channel": channel,
63
+ "action": input_data.action,
64
+ "handled_by": "dry-run",
65
+ "payload": {"ok": True},
66
+ }
67
+
68
+ handled = dispatch_channel_message_action(
69
+ ChannelActionContext(
70
+ channel=channel,
71
+ action=input_data.action,
72
+ params=args,
73
+ account_id=input_data.default_account_id,
74
+ )
75
+ )
76
+ if handled is None:
77
+ raise ValueError(f"No plugin registered for channel {channel}")
78
+ return {
79
+ "kind": "action",
80
+ "channel": channel,
81
+ "action": input_data.action,
82
+ "handled_by": "plugin",
83
+ "payload": handled,
84
+ }
85
+
@@ -0,0 +1,14 @@
1
+ import sys
2
+
3
+ from .cli.app import app
4
+ from .cli.route import try_route_cli
5
+
6
+
7
+ def main() -> None:
8
+ try_route_cli(sys.argv)
9
+ app()
10
+
11
+
12
+ if __name__ == "__main__":
13
+ main()
14
+
@@ -0,0 +1,11 @@
1
+ from dataclasses import dataclass
2
+
3
+
4
+ @dataclass
5
+ class Runtime:
6
+ def log(self, message: str) -> None:
7
+ print(message)
8
+
9
+
10
+ default_runtime = Runtime()
11
+
@@ -0,0 +1,28 @@
1
+ Metadata-Version: 2.4
2
+ Name: jingyi
3
+ Version: 0.1.0
4
+ Summary: OpenClaw-inspired Python demo agent
5
+ Requires-Python: >=3.11
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: typer<1.0,>=0.12
8
+ Provides-Extra: dev
9
+ Requires-Dist: pytest<9.0,>=8.0; extra == "dev"
10
+
11
+ # jingyi
12
+
13
+ An OpenClaw-inspired Python demo agent package.
14
+
15
+ ## Install
16
+
17
+ ```bash
18
+ pip install jingyi
19
+ ```
20
+
21
+ ## CLI
22
+
23
+ ```bash
24
+ jingyi health
25
+ jingyi status
26
+ jingyi message send --to channel:123 --message "hello" --channel discord
27
+ ```
28
+
@@ -0,0 +1,44 @@
1
+ README.md
2
+ pyproject.toml
3
+ ./jingyi/__init__.py
4
+ ./jingyi/config.py
5
+ ./jingyi/main.py
6
+ ./jingyi/runtime.py
7
+ ./jingyi/channels/__init__.py
8
+ ./jingyi/channels/registry.py
9
+ ./jingyi/channels/plugins/__init__.py
10
+ ./jingyi/channels/plugins/discord_plugin.py
11
+ ./jingyi/channels/plugins/message_actions.py
12
+ ./jingyi/channels/plugins/types.py
13
+ ./jingyi/cli/__init__.py
14
+ ./jingyi/cli/app.py
15
+ ./jingyi/cli/route.py
16
+ ./jingyi/commands/__init__.py
17
+ ./jingyi/commands/message.py
18
+ ./jingyi/infra/__init__.py
19
+ ./jingyi/infra/outbound/__init__.py
20
+ ./jingyi/infra/outbound/message_action_runner.py
21
+ jingyi/__init__.py
22
+ jingyi/config.py
23
+ jingyi/main.py
24
+ jingyi/runtime.py
25
+ jingyi.egg-info/PKG-INFO
26
+ jingyi.egg-info/SOURCES.txt
27
+ jingyi.egg-info/dependency_links.txt
28
+ jingyi.egg-info/entry_points.txt
29
+ jingyi.egg-info/requires.txt
30
+ jingyi.egg-info/top_level.txt
31
+ jingyi/channels/__init__.py
32
+ jingyi/channels/registry.py
33
+ jingyi/channels/plugins/__init__.py
34
+ jingyi/channels/plugins/discord_plugin.py
35
+ jingyi/channels/plugins/message_actions.py
36
+ jingyi/channels/plugins/types.py
37
+ jingyi/cli/__init__.py
38
+ jingyi/cli/app.py
39
+ jingyi/cli/route.py
40
+ jingyi/commands/__init__.py
41
+ jingyi/commands/message.py
42
+ jingyi/infra/__init__.py
43
+ jingyi/infra/outbound/__init__.py
44
+ jingyi/infra/outbound/message_action_runner.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ jingyi = jingyi.main:main
@@ -0,0 +1,4 @@
1
+ typer<1.0,>=0.12
2
+
3
+ [dev]
4
+ pytest<9.0,>=8.0
@@ -0,0 +1 @@
1
+ jingyi
@@ -0,0 +1,29 @@
1
+ [build-system]
2
+ requires = ["setuptools>=69", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "jingyi"
7
+ version = "0.1.0"
8
+ description = "OpenClaw-inspired Python demo agent"
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ dependencies = [
12
+ "typer>=0.12,<1.0",
13
+ ]
14
+
15
+ [project.optional-dependencies]
16
+ dev = [
17
+ "pytest>=8.0,<9.0",
18
+ ]
19
+
20
+ [project.scripts]
21
+ jingyi = "jingyi.main:main"
22
+
23
+ [tool.setuptools]
24
+ package-dir = {"" = "."}
25
+
26
+ [tool.setuptools.packages.find]
27
+ where = ["."]
28
+ include = ["jingyi*"]
29
+
jingyi-0.1.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+