csp-bot 2.0.3__tar.gz → 2.0.4__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 (69) hide show
  1. {csp_bot-2.0.3 → csp_bot-2.0.4}/PKG-INFO +1 -1
  2. {csp_bot-2.0.3 → csp_bot-2.0.4}/csp_bot/__init__.py +1 -1
  3. {csp_bot-2.0.3 → csp_bot-2.0.4}/csp_bot/commands/agent.py +5 -1
  4. {csp_bot-2.0.3 → csp_bot-2.0.4}/csp_bot/commands/base.py +4 -0
  5. {csp_bot-2.0.3 → csp_bot-2.0.4}/csp_bot/commands/framework.py +11 -1
  6. {csp_bot-2.0.3 → csp_bot-2.0.4}/csp_bot/commands/help.py +8 -2
  7. {csp_bot-2.0.3 → csp_bot-2.0.4}/csp_bot/commands/legacy.py +4 -0
  8. {csp_bot-2.0.3 → csp_bot-2.0.4}/csp_bot/tests/test_agent_command.py +30 -0
  9. csp_bot-2.0.4/csp_bot/tests/test_help_command.py +115 -0
  10. {csp_bot-2.0.3 → csp_bot-2.0.4}/pyproject.toml +2 -2
  11. csp_bot-2.0.3/csp_bot/tests/test_help_command.py +0 -63
  12. {csp_bot-2.0.3 → csp_bot-2.0.4}/.gitignore +0 -0
  13. {csp_bot-2.0.3 → csp_bot-2.0.4}/LICENSE +0 -0
  14. {csp_bot-2.0.3 → csp_bot-2.0.4}/README.md +0 -0
  15. {csp_bot-2.0.3 → csp_bot-2.0.4}/csp_bot/backends/__init__.py +0 -0
  16. {csp_bot-2.0.3 → csp_bot-2.0.4}/csp_bot/backends/discord.py +0 -0
  17. {csp_bot-2.0.3 → csp_bot-2.0.4}/csp_bot/backends/slack.py +0 -0
  18. {csp_bot-2.0.3 → csp_bot-2.0.4}/csp_bot/backends/symphony.py +0 -0
  19. {csp_bot-2.0.3 → csp_bot-2.0.4}/csp_bot/backends/telegram.py +0 -0
  20. {csp_bot-2.0.3 → csp_bot-2.0.4}/csp_bot/bot.py +0 -0
  21. {csp_bot-2.0.3 → csp_bot-2.0.4}/csp_bot/bot_config.py +0 -0
  22. {csp_bot-2.0.3 → csp_bot-2.0.4}/csp_bot/cli.py +0 -0
  23. {csp_bot-2.0.3 → csp_bot-2.0.4}/csp_bot/commands/__init__.py +0 -0
  24. {csp_bot-2.0.3 → csp_bot-2.0.4}/csp_bot/commands/claude_agent.py +0 -0
  25. {csp_bot-2.0.3 → csp_bot-2.0.4}/csp_bot/commands/context.py +0 -0
  26. {csp_bot-2.0.3 → csp_bot-2.0.4}/csp_bot/commands/echo.py +0 -0
  27. {csp_bot-2.0.3 → csp_bot-2.0.4}/csp_bot/commands/executor.py +0 -0
  28. {csp_bot-2.0.3 → csp_bot-2.0.4}/csp_bot/commands/mcp.py +0 -0
  29. {csp_bot-2.0.3 → csp_bot-2.0.4}/csp_bot/commands/schedule.py +0 -0
  30. {csp_bot-2.0.3 → csp_bot-2.0.4}/csp_bot/commands/status.py +0 -0
  31. {csp_bot-2.0.3 → csp_bot-2.0.4}/csp_bot/config/__init__.py +0 -0
  32. {csp_bot-2.0.3 → csp_bot-2.0.4}/csp_bot/config/backend/discord.yaml +0 -0
  33. {csp_bot-2.0.3 → csp_bot-2.0.4}/csp_bot/config/backend/slack.yaml +0 -0
  34. {csp_bot-2.0.3 → csp_bot-2.0.4}/csp_bot/config/backend/symphony.yaml +0 -0
  35. {csp_bot-2.0.3 → csp_bot-2.0.4}/csp_bot/config/backend/symphony_rsa.yaml +0 -0
  36. {csp_bot-2.0.3 → csp_bot-2.0.4}/csp_bot/config/backend/telegram.yaml +0 -0
  37. {csp_bot-2.0.3 → csp_bot-2.0.4}/csp_bot/config/commands.yaml +0 -0
  38. {csp_bot-2.0.3 → csp_bot-2.0.4}/csp_bot/config/conf.yaml +0 -0
  39. {csp_bot-2.0.3 → csp_bot-2.0.4}/csp_bot/config/gateway/all.yaml +0 -0
  40. {csp_bot-2.0.3 → csp_bot-2.0.4}/csp_bot/config/gateway/bot.yaml +0 -0
  41. {csp_bot-2.0.3 → csp_bot-2.0.4}/csp_bot/config/gateway/discord.yaml +0 -0
  42. {csp_bot-2.0.3 → csp_bot-2.0.4}/csp_bot/config/gateway/mixed.yaml +0 -0
  43. {csp_bot-2.0.3 → csp_bot-2.0.4}/csp_bot/config/gateway/slack.yaml +0 -0
  44. {csp_bot-2.0.3 → csp_bot-2.0.4}/csp_bot/config/gateway/symphony.yaml +0 -0
  45. {csp_bot-2.0.3 → csp_bot-2.0.4}/csp_bot/config/gateway/telegram.yaml +0 -0
  46. {csp_bot-2.0.3 → csp_bot-2.0.4}/csp_bot/config/hydra/job_logging/custom.yaml +0 -0
  47. {csp_bot-2.0.3 → csp_bot-2.0.4}/csp_bot/config/modules.yaml +0 -0
  48. {csp_bot-2.0.3 → csp_bot-2.0.4}/csp_bot/gateway/__init__.py +0 -0
  49. {csp_bot-2.0.3 → csp_bot-2.0.4}/csp_bot/gateway/gateway.py +0 -0
  50. {csp_bot-2.0.3 → csp_bot-2.0.4}/csp_bot/persistence.py +0 -0
  51. {csp_bot-2.0.3 → csp_bot-2.0.4}/csp_bot/structs.py +0 -0
  52. {csp_bot-2.0.3 → csp_bot-2.0.4}/csp_bot/tests/__init__.py +0 -0
  53. {csp_bot-2.0.3 → csp_bot-2.0.4}/csp_bot/tests/conftest.py +0 -0
  54. {csp_bot-2.0.3 → csp_bot-2.0.4}/csp_bot/tests/test_agent_mcp.py +0 -0
  55. {csp_bot-2.0.3 → csp_bot-2.0.4}/csp_bot/tests/test_backends.py +0 -0
  56. {csp_bot-2.0.3 → csp_bot-2.0.4}/csp_bot/tests/test_bot.py +0 -0
  57. {csp_bot-2.0.3 → csp_bot-2.0.4}/csp_bot/tests/test_bot_integration.py +0 -0
  58. {csp_bot-2.0.3 → csp_bot-2.0.4}/csp_bot/tests/test_claude_agent.py +0 -0
  59. {csp_bot-2.0.3 → csp_bot-2.0.4}/csp_bot/tests/test_command_framework.py +0 -0
  60. {csp_bot-2.0.3 → csp_bot-2.0.4}/csp_bot/tests/test_config.py +0 -0
  61. {csp_bot-2.0.3 → csp_bot-2.0.4}/csp_bot/tests/test_config_composition.py +0 -0
  62. {csp_bot-2.0.3 → csp_bot-2.0.4}/csp_bot/tests/test_echo.py +0 -0
  63. {csp_bot-2.0.3 → csp_bot-2.0.4}/csp_bot/tests/test_persistence.py +0 -0
  64. {csp_bot-2.0.3 → csp_bot-2.0.4}/csp_bot/tests/test_schedule.py +0 -0
  65. {csp_bot-2.0.3 → csp_bot-2.0.4}/csp_bot/tests/test_slack_entity_regex.py +0 -0
  66. {csp_bot-2.0.3 → csp_bot-2.0.4}/csp_bot/tests/test_status.py +0 -0
  67. {csp_bot-2.0.3 → csp_bot-2.0.4}/csp_bot/tests/test_structs.py +0 -0
  68. {csp_bot-2.0.3 → csp_bot-2.0.4}/csp_bot/tests/test_utils.py +0 -0
  69. {csp_bot-2.0.3 → csp_bot-2.0.4}/csp_bot/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: csp-bot
3
- Version: 2.0.3
3
+ Version: 2.0.4
4
4
  Summary: Reactive chat bot framework
5
5
  Project-URL: Repository, https://github.com/Point72/csp-bot
6
6
  Project-URL: Homepage, https://github.com/Point72/csp-bot
@@ -11,7 +11,7 @@ Key features:
11
11
  - Support for Slack, Symphony, and Discord
12
12
  """
13
13
 
14
- __version__ = "2.0.3"
14
+ __version__ = "2.0.4"
15
15
 
16
16
  # Re-export chatom types for convenience
17
17
  from chatom import Channel, Message, User
@@ -381,6 +381,9 @@ class AgentCommand(ReplyCommand):
381
381
  model_name = kwargs.pop("model_name", None)
382
382
  if model_name is not None:
383
383
  self.model_name = model_name
384
+ root_prompt = kwargs.pop("root_prompt", None)
385
+ if root_prompt is not None:
386
+ self.root_prompt = root_prompt
384
387
  self.mcp_servers = [MCPServerConfig.model_validate(config) for config in kwargs.pop("mcp_servers", [])]
385
388
 
386
389
  @classmethod
@@ -886,7 +889,8 @@ class AgentCommandModel(BaseCommandModel):
886
889
  command: type[AgentCommand]
887
890
  model_name: str = "claude-sonnet-4-6"
888
891
  mcp_servers: list[MCPServerConfig] = Field(default_factory=list)
892
+ root_prompt: str | None = None
889
893
 
890
894
  def create_command(self) -> AgentCommand:
891
895
  """Create the configured agent command."""
892
- return self.command(model_name=self.model_name, mcp_servers=self.mcp_servers)
896
+ return self.command(model_name=self.model_name, mcp_servers=self.mcp_servers, root_prompt=self.root_prompt)
@@ -46,6 +46,10 @@ class BaseCommand(ABC):
46
46
  """Return the help text for the command."""
47
47
  ...
48
48
 
49
+ def usage(self) -> str:
50
+ """Return optional command syntax, without a label."""
51
+ return ""
52
+
49
53
  @abstractmethod
50
54
  def num_recipients(self) -> int:
51
55
  """Return how many users this command can tag.
@@ -31,7 +31,7 @@ _COMMAND_REGISTRY: dict[str, CommandEntry] = {}
31
31
  class CommandEntry:
32
32
  """Internal registry entry for a command."""
33
33
 
34
- __slots__ = ("backends", "handler", "help", "is_class", "name")
34
+ __slots__ = ("backends", "handler", "help", "is_class", "name", "usage")
35
35
 
36
36
  def __init__(
37
37
  self,
@@ -40,18 +40,22 @@ class CommandEntry:
40
40
  handler: Any,
41
41
  backends: list[str] | None = None,
42
42
  is_class: bool = False,
43
+ usage: str = "",
43
44
  ):
44
45
  self.name = name
45
46
  self.help = help
46
47
  self.handler = handler
47
48
  self.backends = backends or []
48
49
  self.is_class = is_class
50
+ self.usage = usage
49
51
 
50
52
 
51
53
  def command(
52
54
  name: str,
53
55
  help: str = "",
54
56
  backends: list[str] | None = None,
57
+ *,
58
+ usage: str = "",
55
59
  ) -> Callable:
56
60
  """Decorator to register a function as a bot command.
57
61
 
@@ -67,6 +71,7 @@ def command(
67
71
  name: The command name (e.g. "echo" for /echo).
68
72
  help: Help text shown by the /help command.
69
73
  backends: List of backends this command supports. Empty = all.
74
+ usage: Optional command syntax, without a label.
70
75
 
71
76
  Returns:
72
77
  The original function, registered in the global command registry.
@@ -85,12 +90,14 @@ def command(
85
90
  handler=fn,
86
91
  backends=backends,
87
92
  is_class=False,
93
+ usage=usage,
88
94
  )
89
95
  _COMMAND_REGISTRY[name] = entry
90
96
  # Stash metadata on the function for introspection
91
97
  fn._command_name = name
92
98
  fn._command_help = help
93
99
  fn._command_backends = backends or []
100
+ fn._command_usage = usage
94
101
  return fn
95
102
 
96
103
  return decorator
@@ -133,6 +140,9 @@ class Command(BaseModel):
133
140
  help: str = ""
134
141
  """Help text shown by the /help command."""
135
142
 
143
+ usage: str = ""
144
+ """Optional command syntax, without a label."""
145
+
136
146
  backends: list[str] = Field(default_factory=list)
137
147
  """Backends this command supports. Empty = all."""
138
148
 
@@ -29,8 +29,14 @@ def _command_name(runner: Any) -> str:
29
29
 
30
30
  def _command_help(runner: Any) -> str:
31
31
  if isinstance(runner, BaseCommand):
32
- return runner.help()
33
- return getattr(runner, "help", "") or ""
32
+ description = runner.help()
33
+ usage = runner.usage()
34
+ else:
35
+ description = getattr(runner, "help", "") or ""
36
+ usage = getattr(runner, "usage", "") or ""
37
+ if not usage:
38
+ return description
39
+ return f"{description} Syntax: {usage}" if description else f"Syntax: {usage}"
34
40
 
35
41
 
36
42
  def _render_table_help(rows: list, backend: str) -> str:
@@ -46,6 +46,10 @@ class LegacyCommandAdapter:
46
46
  def help(self) -> str:
47
47
  return self._command.help()
48
48
 
49
+ @property
50
+ def usage(self) -> str:
51
+ return self._command.usage()
52
+
49
53
  @property
50
54
  def backends(self) -> list[str]:
51
55
  return self._command.backends()
@@ -180,6 +180,36 @@ class TestGetModel:
180
180
 
181
181
 
182
182
  class TestAgentCommandModel:
183
+ def test_creates_command_with_configured_root_prompt(self, bot_command):
184
+ model = AgentCommandModel(command=ConcreteAgentCommand, root_prompt="Use the project handbook for project questions.")
185
+
186
+ command = model.create_command()
187
+
188
+ assert model.root_prompt is not None
189
+ assert command.root_prompt == model.root_prompt
190
+ assert command.build_root_prompt(bot_command) == model.root_prompt
191
+ assert command._prompt_prefix(bot_command).startswith(model.root_prompt)
192
+
193
+ @pytest.mark.parametrize(
194
+ "configured,expected",
195
+ [(None, "Default project context."), ("", ""), ("Custom project context.", "Custom project context.")],
196
+ )
197
+ def test_root_prompt_preserves_defaults_or_uses_explicit_override(self, configured, expected):
198
+ class DefaultPromptCommand(ConcreteAgentCommand):
199
+ root_prompt = "Default project context."
200
+
201
+ model = AgentCommandModel(command=DefaultPromptCommand, root_prompt=configured)
202
+
203
+ assert model.create_command().root_prompt == expected
204
+ assert DefaultPromptCommand.root_prompt == "Default project context."
205
+
206
+ def test_root_prompt_is_instance_specific(self):
207
+ command = ConcreteAgentCommand(root_prompt="Custom project context.")
208
+
209
+ assert command.root_prompt == "Custom project context."
210
+ assert ConcreteAgentCommand().root_prompt == ""
211
+ assert ConcreteAgentCommand.root_prompt == ""
212
+
183
213
  def test_creates_command_with_configured_model_name(self):
184
214
  model = AgentCommandModel(command=ConcreteAgentCommand, model_name="github-copilot:test-model")
185
215
 
@@ -0,0 +1,115 @@
1
+ """Tests for help command rendering."""
2
+
3
+ from types import SimpleNamespace
4
+
5
+ import pytest
6
+ from chatom import Message, User
7
+
8
+ from csp_bot.commands.echo import EchoCommand
9
+ from csp_bot.commands.framework import Command, clear_registry, command, get_registered_commands
10
+ from csp_bot.commands.help import HelpCommand
11
+ from csp_bot.commands.legacy import LegacyCommandAdapter
12
+ from csp_bot.structs import BotCommand, CommandVariant
13
+
14
+
15
+ def _command(backend: str) -> BotCommand:
16
+ return BotCommand(
17
+ command="help",
18
+ args=(),
19
+ source=User(id="user1"),
20
+ targets=(),
21
+ channel_id="channel1",
22
+ channel_name="general",
23
+ backend=backend,
24
+ variant=CommandVariant.REPLY,
25
+ message=Message(id="msg1", content="!help"),
26
+ delay=None,
27
+ schedule="",
28
+ times_run=0,
29
+ )
30
+
31
+
32
+ def _commands():
33
+ return {
34
+ "ask": SimpleNamespace(name="Ask", help="Ask <anything> & get an answer", backends=[]),
35
+ "help": SimpleNamespace(name="Help", help="Get help with bot commands", backends=[]),
36
+ }
37
+
38
+
39
+ def test_help_discord_uses_list_not_markdown_table() -> None:
40
+ result = HelpCommand().execute(_command("discord"), _commands())
41
+
42
+ assert result.content.startswith("**Bot Commands Help**")
43
+ assert "- `/ask` **Ask**: Ask <anything> & get an answer" in result.content
44
+ assert "| Command" not in result.content
45
+
46
+
47
+ def test_help_slack_uses_list_not_code_block_table() -> None:
48
+ result = HelpCommand().execute(_command("slack"), _commands())
49
+
50
+ assert result.content.startswith("*Bot Commands Help*")
51
+ assert "- `/ask` *Ask*: Ask <anything> & get an answer" in result.content
52
+ assert "```" not in result.content
53
+
54
+
55
+ def test_help_telegram_uses_supported_html_list() -> None:
56
+ result = HelpCommand().execute(_command("telegram"), _commands())
57
+
58
+ assert result.content.startswith("<b>Bot Commands Help</b>")
59
+ assert "- <code>/ask</code> <b>Ask</b>: Ask &lt;anything&gt; &amp; get an answer" in result.content
60
+ assert "<table>" not in result.content
61
+
62
+
63
+ def test_help_symphony_keeps_table_rendering() -> None:
64
+ result = HelpCommand().execute(_command("symphony"), _commands())
65
+
66
+ assert "<h3>Bot Commands Help</h3>" in result.content
67
+ assert "<table>" in result.content
68
+
69
+
70
+ @pytest.mark.parametrize("backend", ["discord", "slack", "telegram", "symphony"])
71
+ @pytest.mark.parametrize("runner_kind", ["base", "adapter", "metadata", "decorated", "stateful"])
72
+ def test_help_renders_optional_usage_after_description(backend, runner_kind):
73
+ class UsageCommand(EchoCommand):
74
+ def help(self) -> str:
75
+ return "Echo a message."
76
+
77
+ def usage(self) -> str:
78
+ return "/echo message"
79
+
80
+ if runner_kind == "base":
81
+ runner = UsageCommand()
82
+ elif runner_kind == "adapter":
83
+ runner = LegacyCommandAdapter(UsageCommand())
84
+ elif runner_kind == "metadata":
85
+ runner = SimpleNamespace(name="Echo", help="Echo a message.", usage="/echo message", backends=[])
86
+ elif runner_kind == "stateful":
87
+ runner = Command(name="Echo", help="Echo a message.", usage="/echo message")
88
+ else:
89
+ clear_registry()
90
+ try:
91
+
92
+ @command(name="echo", help="Echo a message.", usage="/echo message")
93
+ def echo(context):
94
+ return context.args_text
95
+
96
+ runner = get_registered_commands()["echo"]
97
+ finally:
98
+ clear_registry()
99
+
100
+ result = HelpCommand().execute(_command(backend), {"echo": runner})
101
+ assert "Echo a message. Syntax: /echo message" in result.content
102
+ assert result.content.count("Syntax:") == 1
103
+
104
+
105
+ def test_help_only_base_command_keeps_existing_output():
106
+ runner = EchoCommand()
107
+ assert runner.usage() == ""
108
+ result = HelpCommand().execute(_command("discord"), {"echo": runner})
109
+ assert result.content.endswith(f"- `/echo` **{runner.name()}**: {runner.help()}")
110
+
111
+
112
+ def test_help_renders_usage_without_description():
113
+ runner = Command(name="Echo", usage="/echo message")
114
+ result = HelpCommand().execute(_command("discord"), {"echo": runner})
115
+ assert result.content.endswith("- `/echo` **Echo**: Syntax: /echo message")
@@ -12,7 +12,7 @@ authors = [
12
12
  description = "Reactive chat bot framework"
13
13
  readme = "README.md"
14
14
  license = { text = "Apache-2.0" }
15
- version = "2.0.3"
15
+ version = "2.0.4"
16
16
  requires-python = ">=3.11"
17
17
  keywords = [
18
18
  "csp",
@@ -100,7 +100,7 @@ Repository = "https://github.com/Point72/csp-bot"
100
100
  Homepage = "https://github.com/Point72/csp-bot"
101
101
 
102
102
  [tool.bumpversion]
103
- current_version = "2.0.3"
103
+ current_version = "2.0.4"
104
104
  commit = true
105
105
  tag = true
106
106
  commit_args = "-s"
@@ -1,63 +0,0 @@
1
- """Tests for help command rendering."""
2
-
3
- from types import SimpleNamespace
4
-
5
- from chatom import Message, User
6
-
7
- from csp_bot.commands.help import HelpCommand
8
- from csp_bot.structs import BotCommand, CommandVariant
9
-
10
-
11
- def _command(backend: str) -> BotCommand:
12
- return BotCommand(
13
- command="help",
14
- args=(),
15
- source=User(id="user1"),
16
- targets=(),
17
- channel_id="channel1",
18
- channel_name="general",
19
- backend=backend,
20
- variant=CommandVariant.REPLY,
21
- message=Message(id="msg1", content="!help"),
22
- delay=None,
23
- schedule="",
24
- times_run=0,
25
- )
26
-
27
-
28
- def _commands():
29
- return {
30
- "ask": SimpleNamespace(name="Ask", help="Ask <anything> & get an answer", backends=[]),
31
- "help": SimpleNamespace(name="Help", help="Get help with bot commands", backends=[]),
32
- }
33
-
34
-
35
- def test_help_discord_uses_list_not_markdown_table() -> None:
36
- result = HelpCommand().execute(_command("discord"), _commands())
37
-
38
- assert result.content.startswith("**Bot Commands Help**")
39
- assert "- `/ask` **Ask**: Ask <anything> & get an answer" in result.content
40
- assert "| Command" not in result.content
41
-
42
-
43
- def test_help_slack_uses_list_not_code_block_table() -> None:
44
- result = HelpCommand().execute(_command("slack"), _commands())
45
-
46
- assert result.content.startswith("*Bot Commands Help*")
47
- assert "- `/ask` *Ask*: Ask <anything> & get an answer" in result.content
48
- assert "```" not in result.content
49
-
50
-
51
- def test_help_telegram_uses_supported_html_list() -> None:
52
- result = HelpCommand().execute(_command("telegram"), _commands())
53
-
54
- assert result.content.startswith("<b>Bot Commands Help</b>")
55
- assert "- <code>/ask</code> <b>Ask</b>: Ask &lt;anything&gt; &amp; get an answer" in result.content
56
- assert "<table>" not in result.content
57
-
58
-
59
- def test_help_symphony_keeps_table_rendering() -> None:
60
- result = HelpCommand().execute(_command("symphony"), _commands())
61
-
62
- assert "<h3>Bot Commands Help</h3>" in result.content
63
- assert "<table>" in result.content
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes