Skip to main content

apollo/
context.rs

1use crate::channels::IncomingMessage;
2
3const GROUP_KEYWORDS: &[&str] = &[
4    "apollo",
5    "plugin",
6    "plugins",
7    "plugin layer",
8    "manifest",
9    "settings",
10    "command",
11    "commands",
12    "upgrade",
13    "upgrades",
14    "transport",
15    "help",
16    "bot",
17    "module",
18    "modules",
19    "configure",
20    "configuration",
21    "how do i",
22    "what is",
23    "what does",
24    "why is",
25    "can you",
26    "could you",
27];
28
29const GENERAL_ASSISTANT_PATTERNS: &[&str] = &[
30    "what can you do",
31    "how does this work",
32    "how do i",
33    "what is this",
34    "what does this do",
35    "can you help",
36    "could you help",
37    "setup",
38    "set up",
39    "configure",
40    "configuration",
41    "plugin",
42    "plugins",
43    "manifest",
44    "layer",
45    "transport",
46    "command",
47    "commands",
48];
49
50fn contains_any(text: &str, needles: &[&str]) -> bool {
51    needles.iter().any(|needle| text.contains(needle))
52}
53
54pub fn is_assistant_topic(text: &str) -> bool {
55    let lower = text.trim().to_lowercase();
56    if lower.is_empty() {
57        return false;
58    }
59
60    contains_any(&lower, GROUP_KEYWORDS) || contains_any(&lower, GENERAL_ASSISTANT_PATTERNS)
61}
62
63pub fn should_respond(msg: &IncomingMessage) -> bool {
64    if !msg.is_group {
65        return true;
66    }
67
68    let text = msg.text.trim().to_lowercase();
69    if text.is_empty() {
70        return false;
71    }
72
73    if text.starts_with('/') || text.starts_with('!') {
74        return true;
75    }
76
77    let direct_mention = text.contains("@apollo");
78    direct_mention || is_assistant_topic(&text)
79}
80
81pub fn routing_guidance(is_group: bool, transport: &str) -> Option<String> {
82    if !is_group {
83        return None;
84    }
85
86    Some(format!(
87        "## Group chat routing
88Transport: {transport}
89
90Respond even without a direct mention when the message is about apollo, apollo-live, plugins, plugin manifests, settings, commands, upgrades, transport, or asks for help with the bot. Stay silent for unrelated ambient chatter. When responding, be concise and reference the relevant command, plugin, or setting path when useful.",
91        transport = transport
92    ))
93}
94
95pub fn plugin_layer_policy_prompt() -> String {
96    "## Plugin layer policy
97Treat live functionality as a plugin layer on top of core. Prefer plugin manifests, hooks, and layered overrides rather than hardcoding live-only behavior into core runtime paths. Keep plugin-specific configuration isolated from core defaults so upgrades remain compatible.
98".to_string()
99}
100
101pub fn transport_policy_prompt(transport: &str) -> String {
102    format!(
103        "## Transport policy
104Default transport: {transport}
105
106Treat the configured transport as a thin adapter over the core runtime, with settings overrides isolated from the core config so upgrades do not conflict. Use context-aware routing for group chats, but only answer ambient messages when they are clearly about the assistant, its plugins, or operational commands.",
107        transport = transport
108    )
109}
110
111pub fn group_memory_key(chat_id: &str) -> String {
112    format!("group:{chat_id}")
113}
114
115pub fn group_memory_prompt(chat_id: &str, summary: &str) -> String {
116    format!(
117        "## Rolling group memory\nChat: {chat_id}\n\n{}",
118        summary.trim()
119    )
120}