takopi-slack-plugin 0.0.15__py3-none-any.whl

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.
@@ -0,0 +1 @@
1
+ """Slack transport plugin for Takopi."""
@@ -0,0 +1,193 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import shutil
5
+ from pathlib import Path
6
+
7
+ import anyio
8
+
9
+ from takopi.api import (
10
+ EngineBackend,
11
+ SetupIssue,
12
+ SetupResult,
13
+ TransportBackend,
14
+ TransportRuntime,
15
+ ExecBridgeConfig,
16
+ ConfigError,
17
+ HOME_CONFIG_PATH,
18
+ install_issue,
19
+ load_settings,
20
+ )
21
+
22
+ from .bridge import SlackBridgeConfig, SlackPresenter, SlackTransport, run_main_loop
23
+ from .client import SlackClient
24
+ from .config import SlackTransportSettings
25
+ from .onboarding import interactive_setup
26
+ from .thread_sessions import SlackThreadSessionStore, resolve_sessions_path
27
+
28
+ _CREATE_CONFIG_TITLE = "create a config"
29
+ _CONFIGURE_SLACK_TITLE = "configure slack"
30
+
31
+
32
+ def _config_issue(path: Path, *, title: str) -> SetupIssue:
33
+ display = _display_path(path)
34
+ return SetupIssue(title, (f" {display}",))
35
+
36
+
37
+ def _display_path(path: Path) -> str:
38
+ home = Path.home()
39
+ try:
40
+ return f"~/{path.relative_to(home)}"
41
+ except ValueError:
42
+ return str(path)
43
+
44
+
45
+ def _expect_transport_settings(
46
+ transport_config: object, *, config_path: Path
47
+ ) -> SlackTransportSettings:
48
+ return SlackTransportSettings.from_config(
49
+ transport_config, config_path=config_path
50
+ )
51
+
52
+
53
+ def check_setup(
54
+ backend: EngineBackend,
55
+ *,
56
+ transport_override: str | None = None,
57
+ ) -> SetupResult:
58
+ issues: list[SetupIssue] = []
59
+ config_path = HOME_CONFIG_PATH
60
+ cmd = backend.cli_cmd or backend.id
61
+ backend_issues: list[SetupIssue] = []
62
+ if shutil.which(cmd) is None:
63
+ backend_issues.append(install_issue(cmd, backend.install_cmd))
64
+
65
+ try:
66
+ settings, config_path = load_settings()
67
+ if transport_override:
68
+ settings = settings.model_copy(update={"transport": transport_override})
69
+ if settings.transport != "slack":
70
+ issues.append(_config_issue(config_path, title=_CONFIGURE_SLACK_TITLE))
71
+ else:
72
+ try:
73
+ transport_config = settings.transport_config(
74
+ "slack", config_path=config_path
75
+ )
76
+ _expect_transport_settings(
77
+ transport_config, config_path=config_path
78
+ )
79
+ except ConfigError:
80
+ issues.append(_config_issue(config_path, title=_CONFIGURE_SLACK_TITLE))
81
+ except ConfigError:
82
+ issues.extend(backend_issues)
83
+ title = (
84
+ _CONFIGURE_SLACK_TITLE
85
+ if config_path.exists() and config_path.is_file()
86
+ else _CREATE_CONFIG_TITLE
87
+ )
88
+ issues.append(_config_issue(config_path, title=title))
89
+ return SetupResult(issues=issues, config_path=config_path)
90
+
91
+ issues.extend(backend_issues)
92
+ return SetupResult(issues=issues, config_path=config_path)
93
+
94
+
95
+ class SlackBackend(TransportBackend):
96
+ id = "slack"
97
+ description = "Slack bot"
98
+
99
+ def check_setup(
100
+ self,
101
+ engine_backend: EngineBackend,
102
+ *,
103
+ transport_override: str | None = None,
104
+ ) -> SetupResult:
105
+ return check_setup(engine_backend, transport_override=transport_override)
106
+
107
+ async def interactive_setup(self, *, force: bool) -> bool:
108
+ return await interactive_setup(force=force)
109
+
110
+ def lock_token(self, *, transport_config: object, _config_path: Path) -> str | None:
111
+ settings = _expect_transport_settings(
112
+ transport_config, config_path=_config_path
113
+ )
114
+ return settings.bot_token
115
+
116
+ def build_and_run(
117
+ self,
118
+ *,
119
+ transport_config: object,
120
+ config_path: Path,
121
+ runtime: TransportRuntime,
122
+ final_notify: bool,
123
+ default_engine_override: str | None,
124
+ ) -> None:
125
+ settings = _expect_transport_settings(
126
+ transport_config, config_path=config_path
127
+ )
128
+ startup_msg = _build_startup_message(runtime, startup_pwd=os.getcwd())
129
+ client = SlackClient(settings.bot_token)
130
+ transport = SlackTransport(client)
131
+ presenter = SlackPresenter(message_overflow=settings.message_overflow)
132
+ exec_cfg = ExecBridgeConfig(
133
+ transport=transport,
134
+ presenter=presenter,
135
+ final_notify=final_notify,
136
+ )
137
+ thread_store = SlackThreadSessionStore(
138
+ resolve_sessions_path(config_path)
139
+ )
140
+ cfg = SlackBridgeConfig(
141
+ client=client,
142
+ runtime=runtime,
143
+ channel_id=settings.channel_id,
144
+ app_token=settings.app_token,
145
+ startup_msg=startup_msg,
146
+ exec_cfg=exec_cfg,
147
+ thread_store=thread_store,
148
+ )
149
+
150
+ async def run_loop() -> None:
151
+ await run_main_loop(
152
+ cfg,
153
+ watch_config=runtime.watch_config,
154
+ default_engine_override=default_engine_override,
155
+ transport_id=self.id,
156
+ transport_config=settings,
157
+ )
158
+
159
+ anyio.run(run_loop)
160
+
161
+
162
+ def _build_startup_message(runtime: TransportRuntime, *, startup_pwd: str) -> str:
163
+ available_engines = list(runtime.available_engine_ids())
164
+ missing_engines = list(runtime.missing_engine_ids())
165
+ misconfigured_engines = list(runtime.engine_ids_with_status("bad_config"))
166
+ failed_engines = list(runtime.engine_ids_with_status("load_error"))
167
+
168
+ engine_list = ", ".join(available_engines) if available_engines else "none"
169
+
170
+ notes: list[str] = []
171
+ if missing_engines:
172
+ notes.append(f"not installed: {', '.join(missing_engines)}")
173
+ if misconfigured_engines:
174
+ notes.append(f"misconfigured: {', '.join(misconfigured_engines)}")
175
+ if failed_engines:
176
+ notes.append(f"failed to load: {', '.join(failed_engines)}")
177
+ if notes:
178
+ engine_list = f"{engine_list} ({'; '.join(notes)})"
179
+
180
+ project_aliases = sorted(set(runtime.project_aliases()), key=str.lower)
181
+ project_list = ", ".join(project_aliases) if project_aliases else "none"
182
+
183
+ return (
184
+ "takopi is ready\n\n"
185
+ f"default: `{runtime.default_engine}`\n"
186
+ f"agents: `{engine_list}`\n"
187
+ f"projects: `{project_list}`\n"
188
+ f"working in: `{startup_pwd}`"
189
+ )
190
+
191
+
192
+ slack_backend = SlackBackend()
193
+ BACKEND = slack_backend