Mirror of https://github.com/roostorg/coop-integration-example
github.com/roostorg/coop-integration-example
1/**
2 * Example Coop integration plugin with two signal types:
3 * 1. Random Signal Selection – boolean, probability from org config (tests config saving).
4 * 2. Random Score – numeric 0–100, threshold set in the rule (tests score vs threshold).
5 */
6
7import {
8 assertModelCardHasRequiredSections,
9 type CoopIntegrationPlugin,
10 type IntegrationManifest,
11 type ModelCard,
12 type PluginSignalContext,
13 type PluginSignalDescriptor,
14} from '@roostorg/types';
15
16const SIGNAL_TYPE_RANDOM_SELECTION = 'RANDOM_SIGNAL_SELECTION';
17const SIGNAL_TYPE_RANDOM_SCORE = 'RANDOM_SCORE';
18const INTEGRATION_ID = 'COOP_INTEGRATION_EXAMPLE';
19const DEFAULT_TRUE_PERCENTAGE = 50;
20
21const modelCard: ModelCard = {
22 modelName: 'Coop Integration Example',
23 version: '2.0.1',
24 releaseDate: 'March 2026',
25 sections: [
26 {
27 id: 'trainingData',
28 title: 'Training Data',
29 fields: [
30 {
31 label: 'Overview',
32 value:
33 'This reference integration does not use a trained model. Outputs are randomly generated for demonstration and testing of COOP rules, configuration, and UI only.',
34 },
35 ],
36 },
37 {
38 id: 'policyAndTaxonomy',
39 title: 'Policy and Taxonomy',
40 fields: [
41 {
42 label: 'Scope',
43 value:
44 'Not a content policy engine. Signals are placeholders: boolean “coin flip” with configurable probability and a numeric random score for threshold exercises in rules.',
45 },
46 ],
47 },
48 {
49 id: 'annotationMethodology',
50 title: 'Annotation Methodology',
51 fields: [
52 {
53 label: 'Method',
54 value:
55 'No human or automated labeling pipeline. Values are produced with Math.random() (or equivalent logic) at evaluation time.',
56 },
57 ],
58 },
59 {
60 id: 'performanceBenchmarks',
61 title: 'Performance and Benchmarks',
62 fields: [
63 {
64 label: 'Benchmarks',
65 value:
66 'No precision, recall, or latency benchmarks apply. Do not use performance claims from this package in production decisions.',
67 },
68 ],
69 },
70 {
71 id: 'biasAndLimitations',
72 title: 'Bias and Limitations',
73 fields: [
74 {
75 label: 'Limitations',
76 value:
77 'Outputs are uncorrelated with input content. Unsuitable for safety, compliance, or moderation decisions. For integration testing and developer learning only.',
78 },
79 ],
80 },
81 {
82 id: 'implementationGuidance',
83 title: 'Implementation Guidance',
84 fields: [
85 {
86 label: 'Signals',
87 value: `${SIGNAL_TYPE_RANDOM_SELECTION} (boolean; org config truePercentage 0–100). ${SIGNAL_TYPE_RANDOM_SCORE} (number 0–100; set threshold and above/below in the rule).`,
88 },
89 {
90 label: 'Configuration',
91 value:
92 'Random Signal Selection requires org integration config (true percentage). Random Score requires no integration config.',
93 },
94 {
95 label: 'Versioning',
96 value:
97 'modelCard.version and manifest.version identify this integration plugin release. They are independent of the @roostorg/types dependency major version (e.g. 2.x).',
98 },
99 ],
100 },
101 {
102 id: 'relevantLinks',
103 title: 'Relevant Links',
104 fields: [
105 {
106 label: 'Repository',
107 value: 'https://github.com/roostorg/coop-integration-example',
108 },
109 {
110 label: 'Documentation',
111 value: 'https://roostorg.github.io/coop/INTEGRATIONS_PLUGIN.html',
112 },
113 ],
114 },
115 ],
116};
117
118assertModelCardHasRequiredSections(modelCard);
119
120const manifest: IntegrationManifest = {
121 id: INTEGRATION_ID,
122 name: 'Coop Integration Example',
123 /** Same semver as modelCard.version: this plugin’s release, not @roostorg/types. */
124 version: '2.0.1',
125 description:
126 'Example plugin with two signals: config-driven boolean and a numeric score you compare with a threshold in the rule.',
127 docsUrl: 'https://roostorg.github.io/coop/INTEGRATIONS_PLUGIN.html',
128 requiresConfig: true,
129 configurationFields: [
130 {
131 key: 'truePercentage',
132 label: 'True percentage (0–100)',
133 required: true,
134 inputType: 'text',
135 placeholder: '50',
136 description:
137 'Used by Random Signal Selection only. Probability (0–100) that it returns true. Default 50 if not set.',
138 },
139 ],
140 signalTypeIds: [SIGNAL_TYPE_RANDOM_SELECTION, SIGNAL_TYPE_RANDOM_SCORE],
141 modelCard,
142 logoPath: 'roost-example-logo.png',
143 logoWithBackgroundPath: 'roost-example-with-background.png',
144};
145
146/** Parses truePercentage from org config; returns 0–100, default 50 if missing or invalid. */
147function parseTruePercentage(config: Record<string, unknown>): number {
148 const v = config.truePercentage;
149 if (v === undefined || v === null) return DEFAULT_TRUE_PERCENTAGE;
150 const n = typeof v === 'number' ? v : Number(String(v).trim());
151 if (!Number.isFinite(n)) return DEFAULT_TRUE_PERCENTAGE;
152 return Math.max(0, Math.min(100, n));
153}
154
155function hasTruePercentageConfig(config: Record<string, unknown> | null | undefined): boolean {
156 if (config == null) return false;
157 const v = (config as { truePercentage?: unknown }).truePercentage;
158 return v !== undefined && v !== null && String(v).trim() !== '';
159}
160
161function createRandomSignalSelectionDescriptor(
162 context: PluginSignalContext,
163): PluginSignalDescriptor {
164 const { integrationId, getCredential } = context;
165 const outputType = { scalarType: 'BOOLEAN' as const };
166
167 return {
168 id: { type: SIGNAL_TYPE_RANDOM_SELECTION },
169 displayName: 'Coin Flip Selection',
170 description:
171 'Returns true or false at random, with a configurable probability (true percentage 0–100) from the integration config.',
172 docsUrl: null,
173 recommendedThresholds: {
174 highPrecisionThreshold: 0.5,
175 highRecallThreshold: 0.5,
176 },
177 supportedLanguages: 'ALL',
178 pricingStructure: { type: 'FREE' },
179 eligibleInputs: ['STRING', 'IMAGE', 'FULL_ITEM'],
180 outputType,
181 getCost: () => 0,
182 needsMatchingValues: false,
183 eligibleSubcategories: [],
184 needsActionPenalties: false,
185 integration: integrationId,
186 allowedInAutomatedRules: true,
187
188 async run(input: unknown): Promise<{ outputType: typeof outputType; score: boolean }> {
189 const orgId = (input as { orgId?: string })?.orgId;
190 if (typeof orgId !== 'string') {
191 return { outputType, score: Math.random() < DEFAULT_TRUE_PERCENTAGE / 100 };
192 }
193 const config = await getCredential(orgId);
194 const truePct = parseTruePercentage(config ?? {});
195 const score = Math.random() * 100 < truePct;
196 // Because outputType is { scalarType: 'BOOLEAN' }, Coop will use the output score as is for the condition.
197 return { outputType, score };
198 },
199
200 async getDisabledInfo(orgId: string) {
201 const config = await getCredential(orgId);
202 if (hasTruePercentageConfig(config ?? undefined)) {
203 return { disabled: false };
204 }
205 return {
206 disabled: true,
207 disabledMessage:
208 'Configure the integration (True percentage 0–100) in Org settings to use this signal.',
209 };
210 },
211 };
212}
213
214function createRandomScoreDescriptor(
215 context: PluginSignalContext,
216): PluginSignalDescriptor {
217 const { integrationId } = context;
218 const outputType = { scalarType: 'NUMBER' as const };
219
220 return {
221 id: { type: SIGNAL_TYPE_RANDOM_SCORE },
222 displayName: 'Random Score',
223 description:
224 'Returns a random number from 0 up to (but not including) 100. Set a threshold in the rule (e.g. 50) and choose "above" or "below" to test numeric conditions.',
225 docsUrl: null,
226 recommendedThresholds: {
227 highPrecisionThreshold: 50,
228 highRecallThreshold: 50,
229 },
230 supportedLanguages: 'ALL',
231 pricingStructure: { type: 'FREE' },
232 eligibleInputs: ['STRING', 'IMAGE', 'FULL_ITEM'],
233 outputType,
234 getCost: () => 0,
235 needsMatchingValues: false,
236 eligibleSubcategories: [],
237 needsActionPenalties: false,
238 integration: integrationId,
239 allowedInAutomatedRules: true,
240
241 async run(
242 _input: unknown,
243 ): Promise<{ outputType: typeof outputType; score: number }> {
244 // [0, 100) — same scale as percentages elsewhere in this plugin (e.g. truePercentage).
245 const score = Math.random() * 100;
246 return { outputType, score };
247 },
248
249 async getDisabledInfo() {
250 return { disabled: false };
251 },
252 };
253}
254
255function createSignals(
256 context: PluginSignalContext,
257): ReadonlyArray<{ signalTypeId: string; signal: PluginSignalDescriptor }> {
258 return [
259 {
260 signalTypeId: SIGNAL_TYPE_RANDOM_SELECTION,
261 signal: createRandomSignalSelectionDescriptor(context),
262 },
263 {
264 signalTypeId: SIGNAL_TYPE_RANDOM_SCORE,
265 signal: createRandomScoreDescriptor(context),
266 },
267 ];
268}
269
270const plugin: CoopIntegrationPlugin = {
271 manifest,
272 createSignals,
273};
274
275export default plugin;
276export { manifest, createSignals };