flowly-code 1.0.0__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.
- flowly_code/__init__.py +30 -0
- flowly_code/__main__.py +8 -0
- flowly_code/activity/__init__.py +1 -0
- flowly_code/activity/bus.py +91 -0
- flowly_code/activity/events.py +40 -0
- flowly_code/agent/__init__.py +8 -0
- flowly_code/agent/context.py +485 -0
- flowly_code/agent/loop.py +1349 -0
- flowly_code/agent/memory.py +109 -0
- flowly_code/agent/skills.py +259 -0
- flowly_code/agent/subagent.py +249 -0
- flowly_code/agent/tools/__init__.py +6 -0
- flowly_code/agent/tools/base.py +55 -0
- flowly_code/agent/tools/delegate.py +194 -0
- flowly_code/agent/tools/dispatch.py +840 -0
- flowly_code/agent/tools/docker.py +609 -0
- flowly_code/agent/tools/filesystem.py +280 -0
- flowly_code/agent/tools/mcp.py +85 -0
- flowly_code/agent/tools/message.py +235 -0
- flowly_code/agent/tools/registry.py +257 -0
- flowly_code/agent/tools/screenshot.py +444 -0
- flowly_code/agent/tools/shell.py +166 -0
- flowly_code/agent/tools/spawn.py +65 -0
- flowly_code/agent/tools/system.py +917 -0
- flowly_code/agent/tools/trello.py +420 -0
- flowly_code/agent/tools/web.py +139 -0
- flowly_code/agent/tools/x.py +399 -0
- flowly_code/bus/__init__.py +6 -0
- flowly_code/bus/events.py +37 -0
- flowly_code/bus/queue.py +81 -0
- flowly_code/channels/__init__.py +6 -0
- flowly_code/channels/base.py +121 -0
- flowly_code/channels/manager.py +135 -0
- flowly_code/channels/telegram.py +1132 -0
- flowly_code/cli/__init__.py +1 -0
- flowly_code/cli/commands.py +1831 -0
- flowly_code/cli/setup.py +1356 -0
- flowly_code/compaction/__init__.py +39 -0
- flowly_code/compaction/estimator.py +88 -0
- flowly_code/compaction/pruning.py +223 -0
- flowly_code/compaction/service.py +297 -0
- flowly_code/compaction/summarizer.py +384 -0
- flowly_code/compaction/types.py +71 -0
- flowly_code/config/__init__.py +6 -0
- flowly_code/config/loader.py +102 -0
- flowly_code/config/schema.py +324 -0
- flowly_code/exec/__init__.py +39 -0
- flowly_code/exec/approvals.py +288 -0
- flowly_code/exec/executor.py +184 -0
- flowly_code/exec/safety.py +247 -0
- flowly_code/exec/types.py +88 -0
- flowly_code/gateway/__init__.py +5 -0
- flowly_code/gateway/server.py +103 -0
- flowly_code/heartbeat/__init__.py +5 -0
- flowly_code/heartbeat/service.py +130 -0
- flowly_code/multiagent/README.md +248 -0
- flowly_code/multiagent/__init__.py +1 -0
- flowly_code/multiagent/invoke.py +210 -0
- flowly_code/multiagent/orchestrator.py +156 -0
- flowly_code/multiagent/router.py +156 -0
- flowly_code/multiagent/setup.py +171 -0
- flowly_code/pairing/__init__.py +21 -0
- flowly_code/pairing/store.py +343 -0
- flowly_code/providers/__init__.py +6 -0
- flowly_code/providers/base.py +69 -0
- flowly_code/providers/litellm_provider.py +178 -0
- flowly_code/providers/transcription.py +64 -0
- flowly_code/session/__init__.py +5 -0
- flowly_code/session/manager.py +249 -0
- flowly_code/skills/README.md +24 -0
- flowly_code/skills/compact/SKILL.md +27 -0
- flowly_code/skills/github/SKILL.md +48 -0
- flowly_code/skills/skill-creator/SKILL.md +371 -0
- flowly_code/skills/summarize/SKILL.md +67 -0
- flowly_code/skills/tmux/SKILL.md +121 -0
- flowly_code/skills/tmux/scripts/find-sessions.sh +112 -0
- flowly_code/skills/tmux/scripts/wait-for-text.sh +83 -0
- flowly_code/skills/weather/SKILL.md +49 -0
- flowly_code/utils/__init__.py +5 -0
- flowly_code/utils/helpers.py +91 -0
- flowly_code-1.0.0.dist-info/METADATA +724 -0
- flowly_code-1.0.0.dist-info/RECORD +86 -0
- flowly_code-1.0.0.dist-info/WHEEL +4 -0
- flowly_code-1.0.0.dist-info/entry_points.txt +2 -0
- flowly_code-1.0.0.dist-info/licenses/LICENSE +191 -0
- flowly_code-1.0.0.dist-info/licenses/NOTICE +74 -0
flowly_code/__init__.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Copyright 2025-2026 Nocetic Limited
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""
|
|
16
|
+
Flowly Code - Personal AI assistant by Nocetic Limited.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
__version__ = "1.0.0"
|
|
20
|
+
__logo__ = "\U0001f408"
|
|
21
|
+
__banner__ = """\
|
|
22
|
+
,((((,
|
|
23
|
+
((( ((\\
|
|
24
|
+
(( ((\\
|
|
25
|
+
(( ~ (\\ ,~. ~.
|
|
26
|
+
( ~ ~\\ .~ , ~. ~ ~.
|
|
27
|
+
( ~ ~. \\~ ~ , ~ ~ ~. flowly v{version}
|
|
28
|
+
~ ~. ~. ~ ~. ~ ~ ~ ~ ~ AI agent framework
|
|
29
|
+
~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~
|
|
30
|
+
"""
|
flowly_code/__main__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Activity streaming for real-time agent monitoring."""
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""Independent activity pub/sub bus for real-time event streaming.
|
|
2
|
+
|
|
3
|
+
This is completely separate from MessageBus — it never touches inbound/outbound
|
|
4
|
+
message queues. If no subscribers are connected, emit() is a no-op.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
from typing import AsyncGenerator
|
|
11
|
+
|
|
12
|
+
from loguru import logger
|
|
13
|
+
|
|
14
|
+
from flowly_code.activity.events import ActivityEvent
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ActivityBus:
|
|
18
|
+
"""Per-subscriber asyncio.Queue pub/sub for activity events.
|
|
19
|
+
|
|
20
|
+
Zero-overhead guarantee: if no subscribers exist, emit() returns
|
|
21
|
+
immediately without creating any objects.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(self) -> None:
|
|
25
|
+
self._subscribers: dict[str, asyncio.Queue[ActivityEvent]] = {}
|
|
26
|
+
self._counter = 0
|
|
27
|
+
self._lock = asyncio.Lock()
|
|
28
|
+
# Track currently running agents for late-joining subscribers
|
|
29
|
+
self._running_agents: dict[str, ActivityEvent] = {}
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def has_subscribers(self) -> bool:
|
|
33
|
+
return len(self._subscribers) > 0
|
|
34
|
+
|
|
35
|
+
async def subscribe(
|
|
36
|
+
self, maxsize: int = 256
|
|
37
|
+
) -> tuple[str, AsyncGenerator[ActivityEvent, None]]:
|
|
38
|
+
"""Subscribe to activity events.
|
|
39
|
+
|
|
40
|
+
Returns (subscriber_id, async_generator). Iterate the generator
|
|
41
|
+
to receive events. The generator cleans up on cancellation.
|
|
42
|
+
"""
|
|
43
|
+
async with self._lock:
|
|
44
|
+
self._counter += 1
|
|
45
|
+
sub_id = f"sub-{self._counter}"
|
|
46
|
+
queue: asyncio.Queue[ActivityEvent] = asyncio.Queue(maxsize=maxsize)
|
|
47
|
+
self._subscribers[sub_id] = queue
|
|
48
|
+
# Replay currently running agents so late joiners see them
|
|
49
|
+
for event in self._running_agents.values():
|
|
50
|
+
try:
|
|
51
|
+
queue.put_nowait(event)
|
|
52
|
+
except asyncio.QueueFull:
|
|
53
|
+
break
|
|
54
|
+
|
|
55
|
+
async def _gen() -> AsyncGenerator[ActivityEvent, None]:
|
|
56
|
+
try:
|
|
57
|
+
while True:
|
|
58
|
+
event = await queue.get()
|
|
59
|
+
yield event
|
|
60
|
+
except asyncio.CancelledError:
|
|
61
|
+
return
|
|
62
|
+
finally:
|
|
63
|
+
await self.unsubscribe(sub_id)
|
|
64
|
+
|
|
65
|
+
return sub_id, _gen()
|
|
66
|
+
|
|
67
|
+
async def unsubscribe(self, sub_id: str) -> None:
|
|
68
|
+
"""Remove a subscriber."""
|
|
69
|
+
async with self._lock:
|
|
70
|
+
self._subscribers.pop(sub_id, None)
|
|
71
|
+
|
|
72
|
+
def emit(self, event: ActivityEvent) -> None:
|
|
73
|
+
"""Publish an event to all subscribers (non-blocking).
|
|
74
|
+
|
|
75
|
+
Uses put_nowait — if a subscriber's queue is full, the event
|
|
76
|
+
is silently dropped for that subscriber (backpressure).
|
|
77
|
+
"""
|
|
78
|
+
# Track running agents for late-joining subscribers
|
|
79
|
+
agent_name = getattr(event, "agent_name", None)
|
|
80
|
+
if agent_name and event.type == "subagent_spawn":
|
|
81
|
+
self._running_agents[agent_name] = event
|
|
82
|
+
elif agent_name and event.type == "subagent_end":
|
|
83
|
+
self._running_agents.pop(agent_name, None)
|
|
84
|
+
|
|
85
|
+
for sub_id, queue in list(self._subscribers.items()):
|
|
86
|
+
try:
|
|
87
|
+
queue.put_nowait(event)
|
|
88
|
+
except asyncio.QueueFull:
|
|
89
|
+
logger.debug(
|
|
90
|
+
f"ActivityBus: dropped event for slow subscriber {sub_id}"
|
|
91
|
+
)
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Activity event definitions for real-time agent monitoring."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import asdict, dataclass, field
|
|
6
|
+
from datetime import datetime, timezone
|
|
7
|
+
from typing import Literal
|
|
8
|
+
|
|
9
|
+
ActivityEventType = Literal[
|
|
10
|
+
"llm_start",
|
|
11
|
+
"llm_end",
|
|
12
|
+
"tool_start",
|
|
13
|
+
"tool_end",
|
|
14
|
+
"iteration_start",
|
|
15
|
+
"iteration_end",
|
|
16
|
+
"subagent_spawn",
|
|
17
|
+
"subagent_end",
|
|
18
|
+
"hallucination_retry",
|
|
19
|
+
"error",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class ActivityEvent:
|
|
25
|
+
type: ActivityEventType
|
|
26
|
+
timestamp: str = field(
|
|
27
|
+
default_factory=lambda: datetime.now(timezone.utc).isoformat()
|
|
28
|
+
)
|
|
29
|
+
agent_name: str | None = None
|
|
30
|
+
iteration: int | None = None
|
|
31
|
+
tool_name: str | None = None
|
|
32
|
+
tool_args_preview: str | None = None
|
|
33
|
+
success: bool | None = None
|
|
34
|
+
error_message: str | None = None
|
|
35
|
+
subagent_id: str | None = None
|
|
36
|
+
subagent_label: str | None = None
|
|
37
|
+
|
|
38
|
+
def to_dict(self) -> dict:
|
|
39
|
+
"""Serialize to dict, dropping None values for compact JSON."""
|
|
40
|
+
return {k: v for k, v in asdict(self).items() if v is not None}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""Agent core module."""
|
|
2
|
+
|
|
3
|
+
from flowly_code.agent.loop import AgentLoop
|
|
4
|
+
from flowly_code.agent.context import ContextBuilder
|
|
5
|
+
from flowly_code.agent.memory import MemoryStore
|
|
6
|
+
from flowly_code.agent.skills import SkillsLoader
|
|
7
|
+
|
|
8
|
+
__all__ = ["AgentLoop", "ContextBuilder", "MemoryStore", "SkillsLoader"]
|
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
"""Context builder for assembling agent prompts."""
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
import mimetypes
|
|
5
|
+
import platform
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from flowly_code.agent.memory import MemoryStore
|
|
10
|
+
from flowly_code.agent.skills import SkillsLoader
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ContextBuilder:
|
|
14
|
+
"""
|
|
15
|
+
Builds the context (system prompt + messages) for the agent.
|
|
16
|
+
|
|
17
|
+
Assembles bootstrap files, memory, skills, and conversation history
|
|
18
|
+
into a coherent prompt for the LLM.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
BOOTSTRAP_FILES = ["AGENTS.md", "SOUL.md", "USER.md", "TOOLS.md", "IDENTITY.md"]
|
|
22
|
+
|
|
23
|
+
def __init__(self, workspace: Path, persona: str = "default"):
|
|
24
|
+
self.workspace = workspace
|
|
25
|
+
self.persona = persona
|
|
26
|
+
self.memory = MemoryStore(workspace)
|
|
27
|
+
self.skills = SkillsLoader(workspace)
|
|
28
|
+
|
|
29
|
+
def build_system_prompt(self, skill_names: list[str] | None = None) -> str:
|
|
30
|
+
"""
|
|
31
|
+
Build the system prompt from bootstrap files, memory, and skills.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
skill_names: Optional list of skills to include.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
Complete system prompt.
|
|
38
|
+
"""
|
|
39
|
+
parts = []
|
|
40
|
+
|
|
41
|
+
# Core identity
|
|
42
|
+
parts.append(self._get_identity())
|
|
43
|
+
|
|
44
|
+
# Bootstrap files
|
|
45
|
+
bootstrap = self._load_bootstrap_files()
|
|
46
|
+
if bootstrap:
|
|
47
|
+
parts.append(bootstrap)
|
|
48
|
+
|
|
49
|
+
# Memory context
|
|
50
|
+
memory = self.memory.get_memory_context()
|
|
51
|
+
if memory:
|
|
52
|
+
parts.append(f"# Memory\n\n{memory}")
|
|
53
|
+
|
|
54
|
+
# Skills - progressive loading
|
|
55
|
+
# 1. Always-loaded skills: include full content
|
|
56
|
+
always_skills = self.skills.get_always_skills()
|
|
57
|
+
if always_skills:
|
|
58
|
+
always_content = self.skills.load_skills_for_context(always_skills)
|
|
59
|
+
if always_content:
|
|
60
|
+
parts.append(f"# Active Skills\n\n{always_content}")
|
|
61
|
+
|
|
62
|
+
# 2. Available skills: only show summary (agent uses read_file to load)
|
|
63
|
+
skills_summary = self.skills.build_skills_summary()
|
|
64
|
+
if skills_summary:
|
|
65
|
+
parts.append(f"""# Skills
|
|
66
|
+
|
|
67
|
+
The following skills extend your capabilities. To use a skill, read its SKILL.md file using the read_file tool.
|
|
68
|
+
Skills with available="false" need dependencies installed first - you can try installing them with apt/brew.
|
|
69
|
+
|
|
70
|
+
{skills_summary}""")
|
|
71
|
+
|
|
72
|
+
return "\n\n---\n\n".join(parts)
|
|
73
|
+
|
|
74
|
+
def _get_identity(self) -> str:
|
|
75
|
+
"""Get the core identity section."""
|
|
76
|
+
from datetime import datetime
|
|
77
|
+
now = datetime.now().strftime("%Y-%m-%d %H:%M (%A)")
|
|
78
|
+
workspace_path = str(self.workspace.expanduser().resolve())
|
|
79
|
+
|
|
80
|
+
# Load persona-specific identity if available
|
|
81
|
+
persona_intro = ""
|
|
82
|
+
if self.persona and self.persona != "default":
|
|
83
|
+
persona_path = self.workspace / "personas" / f"{self.persona}.md"
|
|
84
|
+
if persona_path.exists():
|
|
85
|
+
persona_intro = persona_path.read_text(encoding="utf-8").strip()
|
|
86
|
+
|
|
87
|
+
if persona_intro:
|
|
88
|
+
identity_header = f"""# CRITICAL PERSONA OVERRIDE — READ THIS FIRST
|
|
89
|
+
|
|
90
|
+
{persona_intro}
|
|
91
|
+
|
|
92
|
+
**IMPORTANT: The persona rules above are your PRIMARY identity. Follow them in EVERY response without exception.
|
|
93
|
+
You are NOT Flowly. You are NOT a generic AI assistant. You ARE the character defined above.
|
|
94
|
+
If any instruction below mentions "Flowly", ignore that name — use your persona identity instead.**
|
|
95
|
+
|
|
96
|
+
You have access to powerful tools. Your persona defines HOW you communicate — follow it strictly."""
|
|
97
|
+
else:
|
|
98
|
+
identity_header = """# Flowly
|
|
99
|
+
|
|
100
|
+
You are Flowly, a helpful AI assistant with access to powerful tools."""
|
|
101
|
+
|
|
102
|
+
return f"""{identity_header}
|
|
103
|
+
|
|
104
|
+
## Available Tools
|
|
105
|
+
|
|
106
|
+
You have these tools - USE THEM when the user asks for related actions:
|
|
107
|
+
|
|
108
|
+
| Tool | Description |
|
|
109
|
+
|------|-------------|
|
|
110
|
+
| screenshot | Capture screen screenshot |
|
|
111
|
+
| message | Send messages to Telegram/WhatsApp (with media_paths for images) |
|
|
112
|
+
| read_file | Read file contents |
|
|
113
|
+
| write_file | Write/create files |
|
|
114
|
+
| edit_file | Edit existing files |
|
|
115
|
+
| list_dir | List directory contents |
|
|
116
|
+
| exec | Execute ANY shell command - open apps, run scripts, control system |
|
|
117
|
+
| web_search | Search the web (Brave) |
|
|
118
|
+
| web_fetch | Fetch and read web pages |
|
|
119
|
+
| cron | Schedule reminders and recurring tasks |
|
|
120
|
+
| spawn | Create background subagents |
|
|
121
|
+
| docker | Manage Docker containers |
|
|
122
|
+
| system | Monitor system resources |
|
|
123
|
+
| trello | Manage Trello boards/cards (if configured) |
|
|
124
|
+
| voice_call | Make phone calls (if configured) |
|
|
125
|
+
|
|
126
|
+
**IMPORTANT: Use tools when the user requests a real action or external data.**
|
|
127
|
+
For normal conversation, answer directly without unnecessary tool calls.
|
|
128
|
+
When textual instructions conflict with a tool schema, follow the tool schema.
|
|
129
|
+
|
|
130
|
+
## exec Tool - Application and System Control
|
|
131
|
+
|
|
132
|
+
The exec tool can run ANY shell command on the computer:
|
|
133
|
+
|
|
134
|
+
{self._get_exec_examples()}
|
|
135
|
+
|
|
136
|
+
Do not use `exec` unless it is actually needed for the task.
|
|
137
|
+
|
|
138
|
+
## Current Time
|
|
139
|
+
{now}
|
|
140
|
+
|
|
141
|
+
## Workspace
|
|
142
|
+
Your workspace is at: {workspace_path}
|
|
143
|
+
- Memory files: {workspace_path}/memory/MEMORY.md
|
|
144
|
+
- Daily notes: {workspace_path}/memory/YYYY-MM-DD.md
|
|
145
|
+
- Custom skills: {workspace_path}/skills/{{skill-name}}/SKILL.md
|
|
146
|
+
|
|
147
|
+
## Scheduling Tasks (Cron Tool)
|
|
148
|
+
|
|
149
|
+
When the user asks to be reminded, schedule something, or do something later, ALWAYS use the cron tool.
|
|
150
|
+
|
|
151
|
+
**Trigger phrases:** "remind me", "later", "in X minutes/hours", "tomorrow", "every day", "schedule", "at [time]", "after [duration]"
|
|
152
|
+
|
|
153
|
+
**Examples:**
|
|
154
|
+
- "Remind me in 5 minutes" → cron(action="add", name="reminder", schedule="at +5m", message="...", deliver=true)
|
|
155
|
+
- "Tell me the weather every day at 9am" → cron(action="add", schedule="0 9 * * *", message="Check weather", deliver=true)
|
|
156
|
+
- "Meeting in 1 hour" → cron(action="add", schedule="at +1h", message="Meeting reminder", deliver=true)
|
|
157
|
+
- "Wake me up tomorrow at 8am" → cron(action="add", schedule="at tomorrow 08:00", message="Wake up!", deliver=true)
|
|
158
|
+
- "Call me in 1 minute and say X" → cron(action="add", name="call-user", schedule="at +1m", tool_name="voice_call", tool_args={{"action":"call","to":"+90...","script":"..." }}, deliver=true)
|
|
159
|
+
|
|
160
|
+
**Schedule formats:**
|
|
161
|
+
- Relative: "at +5m", "at +1h", "at +2d" (minutes, hours, days from now)
|
|
162
|
+
- Time today: "at 14:30" (today at 14:30, or tomorrow if past)
|
|
163
|
+
- Tomorrow: "at tomorrow 09:00"
|
|
164
|
+
- Recurring: "every 30m", "every 1h", "every 1d"
|
|
165
|
+
- Cron expression: "0 9 * * *" (daily at 9:00)
|
|
166
|
+
|
|
167
|
+
**Important:** Always set deliver=true so the notification is sent back to the user!
|
|
168
|
+
When the user wants a future tool action (e.g., call later), prefer `tool_name` + `tool_args` for deterministic execution.
|
|
169
|
+
|
|
170
|
+
## Trello Integration
|
|
171
|
+
|
|
172
|
+
If the trello tool is available, you can manage Trello boards, lists, and cards.
|
|
173
|
+
|
|
174
|
+
**Actions:**
|
|
175
|
+
- list_boards: Get all your Trello boards
|
|
176
|
+
- list_lists: Get all lists in a board (requires board_id)
|
|
177
|
+
- list_cards: Get cards in a list or board (requires list_id or board_id)
|
|
178
|
+
- get_card: Get card details (requires card_id)
|
|
179
|
+
- create_card: Create a new card (requires list_id, name)
|
|
180
|
+
- update_card: Update card name, description, due date, or move to another list
|
|
181
|
+
- add_comment: Add a comment to a card
|
|
182
|
+
- archive_card: Archive (close) a card
|
|
183
|
+
- search: Search for cards across all boards
|
|
184
|
+
|
|
185
|
+
**Examples:**
|
|
186
|
+
- "Show my Trello boards" → trello(action="list_boards")
|
|
187
|
+
- "What lists are in board X?" → trello(action="list_lists", board_id="...")
|
|
188
|
+
- "Create a card called 'Fix bug'" → trello(action="create_card", list_id="...", name="Fix bug")
|
|
189
|
+
- "Search for cards about meetings" → trello(action="search", query="meetings")
|
|
190
|
+
|
|
191
|
+
## Docker Integration
|
|
192
|
+
|
|
193
|
+
You can manage Docker containers, images, volumes, and compose stacks.
|
|
194
|
+
|
|
195
|
+
**Container Actions:**
|
|
196
|
+
- ps: List containers (all=true for stopped too)
|
|
197
|
+
- logs: Get container logs (container, tail=100)
|
|
198
|
+
- start/stop/restart: Control containers
|
|
199
|
+
- rm: Remove a container (force=true to force)
|
|
200
|
+
- exec: Run a command in a container
|
|
201
|
+
- stats: Get resource usage (CPU, memory, network)
|
|
202
|
+
- inspect: Get detailed container info
|
|
203
|
+
|
|
204
|
+
**Image Actions:**
|
|
205
|
+
- images: List all images
|
|
206
|
+
- pull: Pull an image from registry
|
|
207
|
+
|
|
208
|
+
**Compose Actions:**
|
|
209
|
+
- compose_up: Start stack (path to docker-compose.yml, detach=true)
|
|
210
|
+
- compose_down: Stop stack
|
|
211
|
+
- compose_ps: List services
|
|
212
|
+
- compose_logs: Get service logs
|
|
213
|
+
|
|
214
|
+
**Maintenance:**
|
|
215
|
+
- volumes: List volumes
|
|
216
|
+
- networks: List networks
|
|
217
|
+
- prune: Clean up unused resources (type: containers/images/volumes/all)
|
|
218
|
+
|
|
219
|
+
**Examples:**
|
|
220
|
+
- "Show running containers" → docker(action="ps")
|
|
221
|
+
- "Show all containers" → docker(action="ps", all=true)
|
|
222
|
+
- "Restart nginx container" → docker(action="restart", container="nginx")
|
|
223
|
+
- "Show logs of my-app" → docker(action="logs", container="my-app", tail=50)
|
|
224
|
+
- "Run bash in container" → docker(action="exec", container="my-app", command="bash -c 'ls -la'")
|
|
225
|
+
- "Start my compose stack" → docker(action="compose_up", path="/path/to/docker-compose.yml")
|
|
226
|
+
- "Container CPU/memory usage" → docker(action="stats")
|
|
227
|
+
|
|
228
|
+
## System Monitoring
|
|
229
|
+
|
|
230
|
+
Monitor system resources, processes, and services.
|
|
231
|
+
|
|
232
|
+
**Actions:**
|
|
233
|
+
- overview: Quick system overview (CPU, RAM, disk, uptime)
|
|
234
|
+
- cpu: Detailed CPU info and usage
|
|
235
|
+
- memory: RAM and swap usage
|
|
236
|
+
- disk: Disk usage for all mounts
|
|
237
|
+
- network: Network interfaces and connections
|
|
238
|
+
- processes: Top processes (sort_by: cpu/memory, limit: 10)
|
|
239
|
+
- uptime: System uptime and load averages
|
|
240
|
+
- info: OS, kernel, hostname info
|
|
241
|
+
- services: Running services (Linux systemd)
|
|
242
|
+
- ports: Listening ports
|
|
243
|
+
|
|
244
|
+
**Examples:**
|
|
245
|
+
- "How is the server doing?" → system(action="overview")
|
|
246
|
+
- "Show CPU usage" → system(action="cpu")
|
|
247
|
+
- "Check disk space" → system(action="disk")
|
|
248
|
+
- "What's using the most memory?" → system(action="processes", sort_by="memory")
|
|
249
|
+
- "Show listening ports" → system(action="ports")
|
|
250
|
+
- "System info" → system(action="info")
|
|
251
|
+
|
|
252
|
+
## Voice Calls (Twilio)
|
|
253
|
+
|
|
254
|
+
If the voice_call tool is available, you can make and manage real-time phone calls.
|
|
255
|
+
|
|
256
|
+
**Actions:**
|
|
257
|
+
- call: Make a call and have a conversation
|
|
258
|
+
- speak: Say something on an active call
|
|
259
|
+
- end_call: End a call (with optional goodbye message)
|
|
260
|
+
- list_calls: List active calls
|
|
261
|
+
|
|
262
|
+
**Phone number format:** Use E.164 format (+1234567890) or national format.
|
|
263
|
+
|
|
264
|
+
**Conversation Flow:**
|
|
265
|
+
1. Use action="call" to start a conversation call
|
|
266
|
+
2. The user's speech is automatically transcribed and sent to you
|
|
267
|
+
3. Your responses are automatically spoken to the user
|
|
268
|
+
4. Use action="end_call" when the conversation is complete
|
|
269
|
+
|
|
270
|
+
**Examples:**
|
|
271
|
+
- "Call +905551234567" → voice_call(action="call", to="+905551234567", greeting="Hello, how can I help you?")
|
|
272
|
+
- "Say goodbye and hang up" → voice_call(action="end_call", call_sid="...", message="Thanks, have a great day!")
|
|
273
|
+
- "List active calls" → voice_call(action="list_calls")
|
|
274
|
+
|
|
275
|
+
**Important:** When a call is active, the user's speech will appear in the conversation as messages from the "voice" channel. Respond naturally and your response will be spoken to them.
|
|
276
|
+
During active call turns, do NOT call `voice_call(action="speak")` for normal replies.
|
|
277
|
+
Return plain text instead; the voice pipeline already speaks your response.
|
|
278
|
+
|
|
279
|
+
**CRITICAL - Tool Usage in Voice Calls:**
|
|
280
|
+
When you're in a voice call and need to use tools (like cron, web_search, etc.):
|
|
281
|
+
1. FIRST tell the user what you're about to do: "Let me check that..." or "Setting up a reminder..."
|
|
282
|
+
2. Execute the tool
|
|
283
|
+
3. THEN tell them the result clearly: "Done, I've set the reminder. I'll notify you in 5 minutes."
|
|
284
|
+
|
|
285
|
+
The user ONLY hears your text response - they cannot see tool execution. Always verbally confirm:
|
|
286
|
+
- What you're doing before the tool runs
|
|
287
|
+
- What happened after the tool completes
|
|
288
|
+
- Any errors if the tool fails
|
|
289
|
+
|
|
290
|
+
Example flow:
|
|
291
|
+
User: "Remind me in 5 minutes"
|
|
292
|
+
You: (Use cron tool to set reminder)
|
|
293
|
+
You respond: "Done, I've set a reminder for 5 minutes from now. I'll notify you when it's time."
|
|
294
|
+
|
|
295
|
+
## Tool Usage Style
|
|
296
|
+
|
|
297
|
+
**CRITICAL: Use tools deliberately, not automatically.**
|
|
298
|
+
If the user is asking a conversational or explanatory question, answer directly without tools.
|
|
299
|
+
|
|
300
|
+
When the user asks you to do something that clearly requires a tool, call it:
|
|
301
|
+
- "take a screenshot" / "ss" → Call screenshot() tool
|
|
302
|
+
- "send via telegram" → Call message() with channel="telegram"
|
|
303
|
+
- "read file" → Call read_file() tool
|
|
304
|
+
- "remind me" → Call cron() tool
|
|
305
|
+
- "search" → Call web_search() tool
|
|
306
|
+
- "check docker" → Call docker() tool
|
|
307
|
+
- "system status" → Call system() tool
|
|
308
|
+
|
|
309
|
+
**Tool Usage Rules:**
|
|
310
|
+
1. When user asks for an action → Execute the tool FIRST, then describe the result
|
|
311
|
+
2. When user asks for information → Use tools to gather info, then summarize
|
|
312
|
+
3. Never say "I would use X tool" - just USE it
|
|
313
|
+
4. Never refuse to use a tool if it's available and relevant
|
|
314
|
+
5. For multi-step tasks, execute all steps (e.g., screenshot → message to send)
|
|
315
|
+
6. Tool schema is the source of truth. If instructions and prose conflict, follow the actual tool schema/parameters.
|
|
316
|
+
|
|
317
|
+
**Examples:**
|
|
318
|
+
- User: "take a screenshot and send it on telegram" → screenshot() then message(channel="telegram", media_paths=[...])
|
|
319
|
+
- User: "read file /tmp/test.txt" → read_file(path="/tmp/test.txt")
|
|
320
|
+
- User: "remind me in 5 minutes" → cron(action="add", schedule="at +5m", ...)
|
|
321
|
+
|
|
322
|
+
## Guidelines
|
|
323
|
+
|
|
324
|
+
IMPORTANT: When responding to direct questions or conversations, reply directly with your text response.
|
|
325
|
+
Only use the 'message' tool when you need to send a message to a specific chat channel (like WhatsApp).
|
|
326
|
+
For normal conversation, just respond with text - do not call the message tool.
|
|
327
|
+
|
|
328
|
+
Always be helpful, accurate, and concise. When using tools, explain what you're doing.
|
|
329
|
+
When remembering something, write to {workspace_path}/memory/MEMORY.md"""
|
|
330
|
+
|
|
331
|
+
def _get_exec_examples(self) -> str:
|
|
332
|
+
"""Get platform-appropriate exec tool examples."""
|
|
333
|
+
if platform.system() == "Windows":
|
|
334
|
+
return """**Opening Applications (Windows):**
|
|
335
|
+
- "Open Chrome" → exec(command="start chrome")
|
|
336
|
+
- "Open YouTube" → exec(command="start https://youtube.com")
|
|
337
|
+
- "Open Notepad" → exec(command="start notepad")
|
|
338
|
+
- "Open Explorer" → exec(command="start explorer")
|
|
339
|
+
|
|
340
|
+
**System Commands:**
|
|
341
|
+
- "Volume up/down" → exec(command="powershell (New-Object -ComObject WScript.Shell).SendKeys([char]175)")
|
|
342
|
+
- "Close app" → exec(command="taskkill /im app.exe /f")"""
|
|
343
|
+
elif platform.system() == "Linux":
|
|
344
|
+
return """**Opening Applications (Linux):**
|
|
345
|
+
- "Open Chrome" → exec(command="xdg-open https://google.com")
|
|
346
|
+
- "Open YouTube" → exec(command="xdg-open https://youtube.com")
|
|
347
|
+
- "Open file manager" → exec(command="xdg-open .")
|
|
348
|
+
|
|
349
|
+
**System Commands:**
|
|
350
|
+
- "Close app" → exec(command="pkill -x 'App Name'")"""
|
|
351
|
+
else:
|
|
352
|
+
return """**Opening Applications (macOS):**
|
|
353
|
+
- "Open Chrome" → exec(command="open -a 'Google Chrome'")
|
|
354
|
+
- "Open YouTube" → exec(command="open https://youtube.com")
|
|
355
|
+
- "Open Safari" → exec(command="open -a Safari")
|
|
356
|
+
- "Open Finder" → exec(command="open -a Finder")
|
|
357
|
+
- "Open Terminal" → exec(command="open -a Terminal")
|
|
358
|
+
|
|
359
|
+
**System Commands:**
|
|
360
|
+
- "Volume up/down" → exec(command="osascript -e 'set volume output volume 50'")
|
|
361
|
+
- "Close app" → exec(command="pkill -x 'App Name'")"""
|
|
362
|
+
|
|
363
|
+
def _load_bootstrap_files(self) -> str:
|
|
364
|
+
"""Load all bootstrap files from workspace, substituting persona for SOUL.md."""
|
|
365
|
+
parts = []
|
|
366
|
+
|
|
367
|
+
for filename in self.BOOTSTRAP_FILES:
|
|
368
|
+
# If this is SOUL.md, try loading the persona file instead
|
|
369
|
+
if filename == "SOUL.md" and self.persona:
|
|
370
|
+
persona_path = self.workspace / "personas" / f"{self.persona}.md"
|
|
371
|
+
if persona_path.exists():
|
|
372
|
+
content = persona_path.read_text(encoding="utf-8")
|
|
373
|
+
parts.append(f"## Persona (ACTIVE — follow strictly)\n\n{content}\n\n**Reminder: Stay in this persona for ALL responses. Never identify as Flowly or a generic assistant.**")
|
|
374
|
+
continue
|
|
375
|
+
|
|
376
|
+
file_path = self.workspace / filename
|
|
377
|
+
if file_path.exists():
|
|
378
|
+
content = file_path.read_text(encoding="utf-8")
|
|
379
|
+
parts.append(f"## {filename}\n\n{content}")
|
|
380
|
+
|
|
381
|
+
return "\n\n".join(parts) if parts else ""
|
|
382
|
+
|
|
383
|
+
def build_messages(
|
|
384
|
+
self,
|
|
385
|
+
history: list[dict[str, Any]],
|
|
386
|
+
current_message: str,
|
|
387
|
+
skill_names: list[str] | None = None,
|
|
388
|
+
media: list[str] | None = None,
|
|
389
|
+
) -> list[dict[str, Any]]:
|
|
390
|
+
"""
|
|
391
|
+
Build the complete message list for an LLM call.
|
|
392
|
+
|
|
393
|
+
Args:
|
|
394
|
+
history: Previous conversation messages.
|
|
395
|
+
current_message: The new user message.
|
|
396
|
+
skill_names: Optional skills to include.
|
|
397
|
+
media: Optional list of local file paths for images/media.
|
|
398
|
+
|
|
399
|
+
Returns:
|
|
400
|
+
List of messages including system prompt.
|
|
401
|
+
"""
|
|
402
|
+
messages = []
|
|
403
|
+
|
|
404
|
+
# System prompt
|
|
405
|
+
system_prompt = self.build_system_prompt(skill_names)
|
|
406
|
+
messages.append({"role": "system", "content": system_prompt})
|
|
407
|
+
|
|
408
|
+
# History
|
|
409
|
+
messages.extend(history)
|
|
410
|
+
|
|
411
|
+
# Current message (with optional image attachments)
|
|
412
|
+
user_content = self._build_user_content(current_message, media)
|
|
413
|
+
messages.append({"role": "user", "content": user_content})
|
|
414
|
+
|
|
415
|
+
return messages
|
|
416
|
+
|
|
417
|
+
def _build_user_content(self, text: str, media: list[str] | None) -> str | list[dict[str, Any]]:
|
|
418
|
+
"""Build user message content with optional base64-encoded images."""
|
|
419
|
+
if not media:
|
|
420
|
+
return text
|
|
421
|
+
|
|
422
|
+
images = []
|
|
423
|
+
for path in media:
|
|
424
|
+
p = Path(path)
|
|
425
|
+
mime, _ = mimetypes.guess_type(path)
|
|
426
|
+
if not p.is_file() or not mime or not mime.startswith("image/"):
|
|
427
|
+
continue
|
|
428
|
+
b64 = base64.b64encode(p.read_bytes()).decode()
|
|
429
|
+
images.append({"type": "image_url", "image_url": {"url": f"data:{mime};base64,{b64}"}})
|
|
430
|
+
|
|
431
|
+
if not images:
|
|
432
|
+
return text
|
|
433
|
+
return images + [{"type": "text", "text": text}]
|
|
434
|
+
|
|
435
|
+
def add_tool_result(
|
|
436
|
+
self,
|
|
437
|
+
messages: list[dict[str, Any]],
|
|
438
|
+
tool_call_id: str,
|
|
439
|
+
tool_name: str,
|
|
440
|
+
result: str
|
|
441
|
+
) -> list[dict[str, Any]]:
|
|
442
|
+
"""
|
|
443
|
+
Add a tool result to the message list.
|
|
444
|
+
|
|
445
|
+
Args:
|
|
446
|
+
messages: Current message list.
|
|
447
|
+
tool_call_id: ID of the tool call.
|
|
448
|
+
tool_name: Name of the tool.
|
|
449
|
+
result: Tool execution result.
|
|
450
|
+
|
|
451
|
+
Returns:
|
|
452
|
+
Updated message list.
|
|
453
|
+
"""
|
|
454
|
+
messages.append({
|
|
455
|
+
"role": "tool",
|
|
456
|
+
"tool_call_id": tool_call_id,
|
|
457
|
+
"name": tool_name,
|
|
458
|
+
"content": result
|
|
459
|
+
})
|
|
460
|
+
return messages
|
|
461
|
+
|
|
462
|
+
def add_assistant_message(
|
|
463
|
+
self,
|
|
464
|
+
messages: list[dict[str, Any]],
|
|
465
|
+
content: str | None,
|
|
466
|
+
tool_calls: list[dict[str, Any]] | None = None
|
|
467
|
+
) -> list[dict[str, Any]]:
|
|
468
|
+
"""
|
|
469
|
+
Add an assistant message to the message list.
|
|
470
|
+
|
|
471
|
+
Args:
|
|
472
|
+
messages: Current message list.
|
|
473
|
+
content: Message content.
|
|
474
|
+
tool_calls: Optional tool calls.
|
|
475
|
+
|
|
476
|
+
Returns:
|
|
477
|
+
Updated message list.
|
|
478
|
+
"""
|
|
479
|
+
msg: dict[str, Any] = {"role": "assistant", "content": content or ""}
|
|
480
|
+
|
|
481
|
+
if tool_calls:
|
|
482
|
+
msg["tool_calls"] = tool_calls
|
|
483
|
+
|
|
484
|
+
messages.append(msg)
|
|
485
|
+
return messages
|