generated from coulomb/repo-seed
Some checks failed
Test / test (push) Has been cancelled
- 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>
117 lines
4.0 KiB
Haskell
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 _ = []
|