diff options
| author | Ben Sima <ben@bensima.com> | 2025-12-19 16:41:01 -0500 |
|---|---|---|
| committer | Ben Sima <ben@bensima.com> | 2025-12-19 16:41:01 -0500 |
| commit | e856c766584ed933bed0b79c7ef47b6d98b0fb7e (patch) | |
| tree | a630081a106678533837c8e284d04146b2593cb5 /Omni | |
| parent | 37d6503342ef9e171fef88960f7baceaa4d1a641 (diff) | |
Omni/Agent: wire prompt templating system to agents
- Telegram.hs: add loadTelegramSystemPrompt with fallback
- Subagent.hs: add loadSystemPromptForRole with fallback
- Coder.hs: add loadCoderSystemPrompt with fallback
- Ava.nix: add tmpfiles rules for /home/ava/prompts/
- Prompts.hs: fix test to expect .mustache extension
Templates loaded at runtime from $AVA_DATA_ROOT/prompts/.
Falls back to hardcoded prompts if templates not found.
Amp-Thread-ID: https://ampcode.com/threads/T-019b3878-73be-77ec-97cc-d092a28d211e
Co-authored-by: Amp <amp@ampcode.com>
Diffstat (limited to 'Omni')
| -rw-r--r-- | Omni/Agent/Prompts.hs | 2 | ||||
| -rw-r--r-- | Omni/Agent/Subagent.hs | 36 | ||||
| -rw-r--r-- | Omni/Agent/Subagent/Coder.hs | 22 | ||||
| -rw-r--r-- | Omni/Agent/Telegram.hs | 24 | ||||
| -rw-r--r-- | Omni/Dev/Beryllium/Ava.nix | 8 |
5 files changed, 77 insertions, 15 deletions
diff --git a/Omni/Agent/Prompts.hs b/Omni/Agent/Prompts.hs index 231df3c..70414ca 100644 --- a/Omni/Agent/Prompts.hs +++ b/Omni/Agent/Prompts.hs @@ -80,7 +80,7 @@ test = "Omni.Agent.Prompts" [ Test.unit "promptPath constructs correct path" <| do let path = promptPath "agents/telegram/system" - ("agents/telegram/system.md" `List.isSuffixOf` path) Test.@=? True, + ("agents/telegram/system.mustache" `List.isSuffixOf` path) Test.@=? True, Test.unit "parsePromptMetadata parses frontmatter" <| do let content = "---\n\ diff --git a/Omni/Agent/Subagent.hs b/Omni/Agent/Subagent.hs index f5e04b0..9f3052d 100644 --- a/Omni/Agent/Subagent.hs +++ b/Omni/Agent/Subagent.hs @@ -18,6 +18,8 @@ -- : out omni-agent-subagent -- : dep aeson -- : dep async +-- : dep directory +-- : dep mustache -- : dep stm -- : dep uuid module Omni.Agent.Subagent @@ -92,6 +94,7 @@ import qualified Data.UUID import qualified Data.UUID.V4 import qualified Omni.Agent.AuditLog as AuditLog import qualified Omni.Agent.Engine as Engine +import qualified Omni.Agent.Prompts as Prompts import qualified Omni.Agent.Provider as Provider import qualified Omni.Agent.Subagent.Coder as Coder import qualified Omni.Agent.Tools as Tools @@ -611,6 +614,21 @@ toolsForRole Researcher keys = toolsForRole Coder _keys = Coder.coderTools toolsForRole (CustomRole _) keys = toolsForRole Researcher keys +-- | Load system prompt from template, falling back to hardcoded if unavailable +loadSystemPromptForRole :: SubagentRole -> Text -> Maybe Text -> IO Text +loadSystemPromptForRole role task maybeContext = do + let ctx = + Aeson.object + [ "role_description" .= roleDescription role, + "task" .= task, + "context" .= maybeContext + ] + result <- Prompts.renderPrompt "subagents/generic/system" ctx + case result of + Right prompt -> pure prompt + Left _err -> pure (systemPromptForRole role task maybeContext) + +-- | Hardcoded fallback prompt for subagents systemPromptForRole :: SubagentRole -> Text -> Maybe Text -> Text systemPromptForRole role task maybeContext = Text.unlines @@ -647,14 +665,14 @@ systemPromptForRole role task maybeContext = "}", "```" ] - where - roleDescription :: SubagentRole -> Text - roleDescription WebCrawler = "web research" - roleDescription CodeReviewer = "code review" - roleDescription DataExtractor = "data extraction" - roleDescription Researcher = "research" - roleDescription Coder = "coding" - roleDescription (CustomRole name) = name + +roleDescription :: SubagentRole -> Text +roleDescription WebCrawler = "web research" +roleDescription CodeReviewer = "code review" +roleDescription DataExtractor = "data extraction" +roleDescription Researcher = "research" +roleDescription Coder = "coding" +roleDescription (CustomRole name) = name runSubagent :: SubagentApiKeys -> SubagentConfig -> IO SubagentResult runSubagent keys config = runSubagentWithCallbacks keys config defaultCallbacks @@ -778,7 +796,7 @@ runGenericSubagent keys config callbacks = do let role = subagentRole config let model = fromMaybe (modelForRole role) (subagentModel config) let tools = toolsForRole role keys - let systemPrompt = systemPromptForRole role (subagentTask config) (subagentContext config) + systemPrompt <- loadSystemPromptForRole role (subagentTask config) (subagentContext config) onSubagentStart callbacks ("Starting " <> tshow role <> " subagent...") diff --git a/Omni/Agent/Subagent/Coder.hs b/Omni/Agent/Subagent/Coder.hs index 523b035..865a97e 100644 --- a/Omni/Agent/Subagent/Coder.hs +++ b/Omni/Agent/Subagent/Coder.hs @@ -17,6 +17,8 @@ -- : out omni-agent-subagent-coder -- : dep aeson -- : dep async +-- : dep directory +-- : dep mustache -- : dep stm -- : dep time module Omni.Agent.Subagent.Coder @@ -52,6 +54,7 @@ import qualified Data.Aeson as Aeson import qualified Data.Text as Text import qualified Data.Time.Clock as Clock import qualified Omni.Agent.Engine as Engine +import qualified Omni.Agent.Prompts as Prompts import qualified Omni.Agent.Provider as Provider import qualified Omni.Agent.Tools as Tools import qualified Omni.Test as Test @@ -242,7 +245,22 @@ coderTools = Tools.searchAndReadTool ] --- | System prompt for the Coder subagent +-- | Load system prompt from template, falling back to hardcoded if unavailable +loadCoderSystemPrompt :: CoderConfig -> Maybe Text -> IO Text +loadCoderSystemPrompt cfg maybeInitState = do + let ctx = + Aeson.object + [ "namespace" .= coderNamespace cfg, + "task" .= coderTask cfg, + "context" .= coderContext cfg, + "init_state" .= maybeInitState + ] + result <- Prompts.renderPrompt "subagents/coder/system" ctx + case result of + Right prompt -> pure prompt + Left _err -> pure (coderSystemPrompt cfg maybeInitState) + +-- | Hardcoded fallback system prompt for the Coder subagent coderSystemPrompt :: CoderConfig -> Maybe Text -> Text coderSystemPrompt cfg maybeInitState = Text.unlines @@ -364,7 +382,7 @@ runWorkVerifyLoop openRouterKey cfg initState verifyAttempt accTokens accCost = -- Phase 2: Work (run the agent) let provider = Provider.defaultOpenRouter openRouterKey (coderModel cfg) - let systemPrompt = coderSystemPrompt cfg initState + systemPrompt <- loadCoderSystemPrompt cfg initState let guardrails = Engine.Guardrails { Engine.guardrailMaxCostCents = coderMaxCost cfg - accCost, diff --git a/Omni/Agent/Telegram.hs b/Omni/Agent/Telegram.hs index 0ebf4ca..7b2beaa 100644 --- a/Omni/Agent/Telegram.hs +++ b/Omni/Agent/Telegram.hs @@ -13,7 +13,9 @@ -- -- : out omni-agent-telegram -- : dep aeson +-- : dep directory -- : dep http-conduit +-- : dep mustache -- : dep stm -- : dep HaskellNet -- : dep HaskellNet-SSL @@ -85,6 +87,7 @@ import qualified Omni.Agent.AuditLog as AuditLog import qualified Omni.Agent.Engine as Engine import qualified Omni.Agent.Memory as Memory import qualified Omni.Agent.Paths as Paths +import qualified Omni.Agent.Prompts as Prompts import qualified Omni.Agent.Provider as Provider import qualified Omni.Agent.Skills as Skills import qualified Omni.Agent.Subagent as Subagent @@ -154,8 +157,20 @@ test = benChatId :: Int benChatId = 33193730 +-- | Load system prompt from template, falling back to hardcoded if unavailable +loadTelegramSystemPrompt :: IO Text +loadTelegramSystemPrompt = do + result <- Prompts.renderPrompt "agents/telegram/system" (Aeson.object []) + case result of + Right prompt -> pure prompt + Left _err -> pure telegramSystemPromptFallback + +-- | Hardcoded fallback prompt (used if template not found) telegramSystemPrompt :: Text -telegramSystemPrompt = +telegramSystemPrompt = telegramSystemPromptFallback + +telegramSystemPromptFallback :: Text +telegramSystemPromptFallback = Text.unlines [ "don't worry about formalities. respond conversationally, in short messages, not long essays. ask follow up questions before answering if you need to.", "", @@ -1100,8 +1115,11 @@ processEngagedMessage tgConfig provider engineCfg msg uid userName chatId userMe "prioritize: urgent items first, then emails needing response, then suggest unsubscribing from marketing." ] else "" - systemPrompt = - telegramSystemPrompt + + basePrompt <- loadTelegramSystemPrompt + + let systemPrompt = + basePrompt <> "\n\n## Current Date and Time\n" <> timeStr <> chatContext diff --git a/Omni/Dev/Beryllium/Ava.nix b/Omni/Dev/Beryllium/Ava.nix index d732249..becbf9e 100644 --- a/Omni/Dev/Beryllium/Ava.nix +++ b/Omni/Dev/Beryllium/Ava.nix @@ -83,6 +83,14 @@ in { "d /home/ava 0755 ava users -" "d /home/ava/omni 0755 ava users -" "d /home/ava/skills 0755 ava users -" + "d /home/ava/prompts 0755 ava users -" + "d /home/ava/prompts/agents 0755 ava users -" + "d /home/ava/prompts/agents/telegram 0755 ava users -" + "d /home/ava/prompts/shared 0755 ava users -" + "d /home/ava/prompts/shared/formatting 0755 ava users -" + "d /home/ava/prompts/subagents 0755 ava users -" + "d /home/ava/prompts/subagents/generic 0755 ava users -" + "d /home/ava/prompts/subagents/coder 0755 ava users -" "d /home/ava/outreach 0755 ava users -" "d /home/ava/outreach/pending 0755 ava users -" "d /home/ava/outreach/approved 0755 ava users -" |
