Files
inter-hub/Application/Helper/AgentBridge.hs
Bernd Worsch 133dae3d23
Some checks failed
Test / test (push) Has been cancelled
feat(WP-0012): IHF Phase 11 — Advanced AI Federation
- Schema: AgentRegistration, ModelRoutingPolicy, AgentDelegation,
  CollectiveProposal, CollectiveProposalContribution, AiGovernancePolicy,
  AgentPerformanceRecord + ALTER TABLE agent_proposals
  (migration 1744156800; CHECK constraints on trust_level, status,
  consensus_status — GAAF compliant)

- Bridge: scripts/llm_bridge.py (llm-connect subprocess seam) +
  Application/Helper/AgentBridge.hs (callBridge, callAgent,
  checkGovernancePolicy, jsonArrayTexts)

- Routing: Application/Helper/ModelRouter.hs (resolveAgent,
  resolveAllAgents) + ModelRoutingPolicies CRUD

- Registry: AgentRegistrations CRUD (Index/Show/New/Edit/Performance),
  DeactivateAgentAction, ComputeAgentPerformanceAction

- Delegation: AgentDelegations controller + views, DelegateSubtaskAction
  with token budget enforcement at bridge call time

- Collective: CollectiveProposals controller + views,
  CreateCollectiveProposalAction (fan-out → synthesis → consensus detection)

- Governance: AiGovernancePolicies CRUD + ToggleAiGovernancePolicyAction;
  checkGovernancePolicy enforced at all 4 Phase 5 invocation points

- Phase 5 wiring: replaced callClaudeApi in Widgets, DecisionRecords,
  RequirementCandidates with resolveAgent + callAgent + token tracking

- llm-connect feature requests: ~/llm-connect/FEATURE_REQUESTS.md
  (FR-1 HTTP serve, FR-2 RoutingPolicy, FR-3 async, FR-4 BudgetTracker)

- GAAF scorecard: 3.61 (up from 3.56); Functional 3.4→3.6, Extensions 3.8→3.9

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-01 20:57:17 +00:00

117 lines
4.0 KiB
Haskell

module Application.Helper.AgentBridge where
-- IHF Phase 11 — Advanced AI Federation (IHUB-WP-0012)
-- Haskell wrapper around scripts/llm_bridge.py (llm-connect subprocess bridge).
import IHP.Prelude
import IHP.ControllerPrelude
import Data.Aeson (object, (.=), encode, decode, Value, FromJSON(..), (.:), (.:?))
import qualified Data.Aeson as A
import qualified Data.ByteString.Lazy as LBS
import System.Process (readProcessWithExitCode)
import System.Exit (ExitCode(..))
import Generated.Types
-- ---------------------------------------------------------------------------
-- Request / response types
data BridgeRequest = BridgeRequest
{ provider :: !Text
, model :: !Text
, systemPrompt :: !(Maybe Text)
, prompt :: !Text
, maxTokens :: !Int
, temperature :: !Double
}
data BridgeResponse = BridgeResponse
{ content :: !Text
, modelUsed :: !Text
, tokensIn :: !Int
, tokensOut :: !Int
, finishReason :: !Text
} deriving (Show)
data BridgeError = BridgeError
{ errorMessage :: !Text
, errorType :: !Text
} deriving (Show)
instance FromJSON BridgeResponse where
parseJSON = A.withObject "BridgeResponse" \o -> BridgeResponse
<$> o .: "content"
<*> o .: "model"
<*> o .: "tokensIn"
<*> o .: "tokensOut"
<*> o .: "finishReason"
instance FromJSON BridgeError where
parseJSON = A.withObject "BridgeError" \o -> BridgeError
<$> o .: "error"
<*> o .: "errorType"
-- ---------------------------------------------------------------------------
-- Core bridge call
-- | Invoke the llm_bridge.py subprocess with the given request.
callBridge :: BridgeRequest -> IO (Either BridgeError BridgeResponse)
callBridge req = do
let payload = LBS.toStrict . encode $ object
[ "provider" .= req.provider
, "model" .= req.model
, "systemPrompt" .= req.systemPrompt
, "prompt" .= req.prompt
, "maxTokens" .= req.maxTokens
, "temperature" .= req.temperature
]
(exitCode, stdout, stderr) <-
readProcessWithExitCode "python3" ["scripts/llm_bridge.py"] (cs payload)
let outBytes = LBS.fromStrict (cs stdout)
case exitCode of
ExitSuccess ->
case decode outBytes of
Just v -> pure (Right v)
Nothing -> pure (Left (BridgeError "Unparseable bridge output" "ParseError"))
ExitFailure _ ->
case decode outBytes of
Just v -> pure (Left v)
Nothing -> pure (Left (BridgeError (cs stderr) "BridgeError"))
-- | Call the bridge using an AgentRegistration record.
callAgent :: AgentRegistration -> Text -> IO (Either BridgeError BridgeResponse)
callAgent agent userPrompt =
callBridge BridgeRequest
{ provider = agent.provider
, model = agent.modelName
, systemPrompt = agent.systemPrompt
, prompt = userPrompt
, maxTokens = 2000
, temperature = 0.7
}
-- ---------------------------------------------------------------------------
-- AI governance policy check
-- | Returns True if the agent is allowed to perform the 'propose' action on
-- the given artifact_type in this hub.
-- When no policy exists the default is permissive (True).
checkGovernancePolicy ::
(?modelContext :: ModelContext) =>
Id Hub -> Id AgentRegistration -> Text -> IO Bool
checkGovernancePolicy hubId agentId artifactType = do
mPolicy <- query @AiGovernancePolicy
|> filterWhere (#hubId, hubId)
|> filterWhere (#agentRegistrationId, agentId)
|> filterWhere (#artifactType, artifactType)
|> filterWhere (#isActive, True)
|> fetchOneOrNothing
case mPolicy of
Nothing -> pure True
Just p -> pure ("propose" `elem` jsonArrayTexts p.allowedActions)
-- | Extract Text values from a JSONB array.
jsonArrayTexts :: Value -> [Text]
jsonArrayTexts (A.Array vs) =
[ t | A.String t <- toList vs ]
jsonArrayTexts _ = []