Skip to content

Commit cd41a2f

Browse files
authored
Merge pull request #19 from John-Lin/wip/align-shell-arch
refactor: align shell and logging architecture with agentic-slackbot
2 parents 058e38a + bf46791 commit cd41a2f

6 files changed

Lines changed: 212 additions & 88 deletions

File tree

README.md

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ See also: [agentic-slackbot](https://github.com/John-Lin/agentic-slackbot) — a
1212
- Supports OpenAI, Azure OpenAI endpoints
1313
- Per-conversation history with automatic truncation
1414
- Group reply chain — after `@mention`, anyone can continue by replying
15-
- Local shell skills — let the agent run shell scripts from `skills/` (opt-in via `SHELL_SKILLS_ENABLED`)
15+
- Optional local shell via `ShellTool`, controlled by `SHELL_ENABLED` and `SHELL_SKILLS_DIR`
1616

1717
## Install Dependencies
1818

@@ -43,8 +43,12 @@ export TELEGRAM_BOT_TOKEN=""
4343
export OPENAI_API_KEY=""
4444
export OPENAI_MODEL="gpt-5.4"
4545
46-
# Shell skills (disabled by default)
47-
# export SHELL_SKILLS_ENABLED=1
46+
# Local shell (disabled by default)
47+
# export SHELL_ENABLED=1
48+
# export SHELL_SKILLS_DIR="./skills" # optional; mount skills alongside the shell
49+
50+
# Optional verbose OpenAI Agents SDK logging
51+
# export AGENT_VERBOSE_LOG=1
4852
```
4953

5054
## Agent Instructions
@@ -173,6 +177,38 @@ uv run bot access group remove <GROUP_ID>
173177

174178
Group members do not need to pair individually — access is controlled at the group level.
175179

180+
## Local Shell (Optional)
181+
182+
The bot can expose a local `ShellTool`. This is **disabled by default**. Enable it with:
183+
184+
```
185+
export SHELL_ENABLED=1
186+
```
187+
188+
With just `SHELL_ENABLED=1`, the agent gets bare local shell access with no pre-defined skills.
189+
190+
### Shell Skills (Optional)
191+
192+
You can optionally mount a skills directory alongside the shell. Each immediate subdirectory containing a `SKILL.md` file is registered as a skill and exposed to the agent as a hint (skills are advisory metadata — they do **not** sandbox command execution).
193+
194+
```
195+
export SHELL_ENABLED=1
196+
export SHELL_SKILLS_DIR="./skills"
197+
```
198+
199+
`SHELL_SKILLS_DIR` is ignored unless `SHELL_ENABLED` is set. If the directory is missing or contains no valid skills, the bot falls back to a bare shell and logs a warning.
200+
201+
The `SKILL.md` file should have YAML frontmatter with `name` and `description` fields:
202+
203+
```markdown
204+
---
205+
name: my-skill
206+
description: A brief description of what this skill does
207+
---
208+
209+
Detailed instructions for the agent...
210+
```
211+
176212
## Docker
177213

178214
```bash

app.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
import logging
77
import sys
88

9+
from agents import enable_verbose_stdout_logging
10+
911
from bot.agents import OpenAIAgent
1012
from bot.auth import add_group
1113
from bot.auth import allow_user
@@ -15,15 +17,22 @@
1517
from bot.auth import remove_user
1618
from bot.auth import set_dm_policy
1719
from bot.config import Configuration
20+
from bot.config import env_flag
1821
from bot.telegram import TelegramMCPBot
1922

2023

21-
async def start_bot() -> None:
22-
"""Initialize and run the Telegram bot."""
24+
def _configure_logging() -> None:
2325
logging.basicConfig(
2426
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
2527
level=logging.INFO,
2628
)
29+
if env_flag("AGENT_VERBOSE_LOG"):
30+
enable_verbose_stdout_logging()
31+
32+
33+
async def start_bot() -> None:
34+
"""Initialize and run the Telegram bot."""
35+
_configure_logging()
2736
config = Configuration()
2837

2938
server_config = config.load_config("servers_config.json")

bot/agents.py

Lines changed: 43 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,13 @@
2020
from agents.tracing import set_tracing_disabled
2121
from openai import AsyncOpenAI
2222

23+
from .config import env_flag
24+
2325
INSTRUCTIONS_FILE = Path("instructions.md")
2426

2527
MAX_TURNS = 10
2628
MCP_SESSION_TIMEOUT_SECONDS = 30.0
2729
SHELL_TIMEOUT = 30.0
28-
SKILLS_DIR = Path(__file__).resolve().parent.parent / "skills"
2930

3031
set_tracing_disabled(True)
3132

@@ -80,17 +81,17 @@ def _parse_skill_description(content: str) -> str:
8081
return ""
8182

8283

83-
def _load_shell_skills() -> list[ShellToolLocalSkill]:
84-
"""Discover local shell skills under SKILLS_DIR.
84+
def _load_shell_skills(skills_dir: Path) -> list[ShellToolLocalSkill]:
85+
"""Discover local shell skills under ``skills_dir``.
8586
86-
Each immediate subdirectory of SKILLS_DIR containing a SKILL.md is mounted
87-
as a ShellToolLocalSkill. The skill name is the directory name; the
88-
description is read from the SKILL.md YAML frontmatter.
87+
Each immediate subdirectory containing a ``SKILL.md`` file is mounted as a
88+
``ShellToolLocalSkill``. The skill name is the directory name; the
89+
description is read from the ``SKILL.md`` YAML frontmatter.
8990
"""
90-
if not SKILLS_DIR.is_dir():
91+
if not skills_dir.is_dir():
9192
return []
9293
skills: list[ShellToolLocalSkill] = []
93-
for skill_dir in sorted(SKILLS_DIR.iterdir()):
94+
for skill_dir in sorted(skills_dir.iterdir()):
9495
skill_md = skill_dir / "SKILL.md"
9596
if not skill_dir.is_dir() or not skill_md.is_file():
9697
continue
@@ -109,6 +110,37 @@ def _load_shell_skills() -> list[ShellToolLocalSkill]:
109110
return skills
110111

111112

113+
def _get_shell_environment() -> ShellToolLocalEnvironment | None:
114+
"""Return the configured local shell environment, if enabled.
115+
116+
Controlled by two independent environment variables:
117+
118+
* ``SHELL_ENABLED`` — when truthy, the ``ShellTool`` is attached to the agent.
119+
* ``SHELL_SKILLS_DIR`` — optional path; when set, skills discovered under it
120+
are mounted alongside the shell. Ignored if ``SHELL_ENABLED`` is not set.
121+
"""
122+
skills_dir_env = os.getenv("SHELL_SKILLS_DIR")
123+
if not env_flag("SHELL_ENABLED"):
124+
if skills_dir_env:
125+
logging.warning(
126+
"SHELL_SKILLS_DIR=%r is set but SHELL_ENABLED is not; ignoring skills dir.",
127+
skills_dir_env,
128+
)
129+
return None
130+
131+
environment: ShellToolLocalEnvironment = {"type": "local"}
132+
if skills_dir_env:
133+
skills = _load_shell_skills(Path(skills_dir_env))
134+
if skills:
135+
environment["skills"] = skills
136+
else:
137+
logging.warning(
138+
"SHELL_SKILLS_DIR=%r yielded no skills; attaching bare local shell.",
139+
skills_dir_env,
140+
)
141+
return environment
142+
143+
112144
async def _shell_executor(request: ShellCommandRequest) -> str:
113145
"""Run each shell command from the request and return combined output.
114146
@@ -215,11 +247,9 @@ def from_dict(cls, name: str, config: dict[str, Any]) -> OpenAIAgent:
215247
)
216248
)
217249
tools: list[Any] = []
218-
if os.getenv("SHELL_SKILLS_ENABLED"):
219-
skills = _load_shell_skills()
220-
if skills:
221-
environment = ShellToolLocalEnvironment(type="local", skills=skills)
222-
tools.append(ShellTool(executor=_shell_executor, environment=environment))
250+
environment = _get_shell_environment()
251+
if environment is not None:
252+
tools.append(ShellTool(executor=_shell_executor, environment=environment))
223253

224254
instructions = _load_instructions()
225255
return cls(name, instructions=instructions, mcp_servers=mcp_servers, tools=tools)

bot/config.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,21 @@
88

99
logger = logging.getLogger(__name__)
1010

11+
_FALSY_ENV_VALUES = frozenset({"", "0", "false", "no", "off"})
12+
13+
14+
def env_flag(name: str) -> bool:
15+
"""Return True if env var ``name`` is set to a truthy value.
16+
17+
Common falsy spellings (empty, "0", "false", "no", "off") are treated as
18+
disabled so that ``FOO=0`` behaves as users intuitively expect rather than
19+
as Python's default "non-empty string is truthy" rule.
20+
"""
21+
raw = os.getenv(name)
22+
if raw is None:
23+
return False
24+
return raw.strip().lower() not in _FALSY_ENV_VALUES
25+
1126

1227
class Configuration:
1328
"""Manages configuration and environment variables for the MCP Telegram bot."""

0 commit comments

Comments
 (0)