verdant 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
verdant/__init__.py ADDED
@@ -0,0 +1,35 @@
1
+ from .config import Settings, get_settings
2
+ from .client import VerdantAPIError, VerdantClient
3
+ from .models import (
4
+ AuditPayload,
5
+ BaselineStageOutput,
6
+ BiasSeverity,
7
+ BiasStageOutput,
8
+ ContextType,
9
+ ExplainStageOutput,
10
+ IntentStageOutput,
11
+ PipelineRunRequest,
12
+ PipelineStageOutputs,
13
+ RiskLevel,
14
+ TrustStageOutput,
15
+ WrapResult,
16
+ )
17
+
18
+ __all__ = [
19
+ "AuditPayload",
20
+ "BaselineStageOutput",
21
+ "BiasSeverity",
22
+ "BiasStageOutput",
23
+ "ContextType",
24
+ "ExplainStageOutput",
25
+ "IntentStageOutput",
26
+ "PipelineRunRequest",
27
+ "PipelineStageOutputs",
28
+ "RiskLevel",
29
+ "Settings",
30
+ "TrustStageOutput",
31
+ "VerdantAPIError",
32
+ "VerdantClient",
33
+ "WrapResult",
34
+ "get_settings",
35
+ ]
verdant/client.py ADDED
@@ -0,0 +1,114 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Callable
4
+
5
+ import httpx
6
+
7
+ from .config import Settings, get_settings
8
+ from .models import ContextType, WrapResult
9
+ from .pipeline import VerdantPipeline
10
+
11
+
12
+ class VerdantClient:
13
+ def __init__(
14
+ self,
15
+ api_key: str | None = None,
16
+ *,
17
+ base_url: str | None = None,
18
+ settings: Settings | None = None,
19
+ pipeline: VerdantPipeline | None = None,
20
+ ) -> None:
21
+ self.settings = settings or get_settings()
22
+ updates: dict[str, Any] = {}
23
+ if api_key:
24
+ updates["verdant_api_key"] = api_key
25
+ if base_url:
26
+ updates["verdant_api_url"] = base_url
27
+ if updates:
28
+ self.settings = self.settings.model_copy(update=updates)
29
+ self.pipeline = pipeline or VerdantPipeline(self.settings)
30
+
31
+ @property
32
+ def _is_hosted(self) -> bool:
33
+ """Use the hosted API when both a base URL and a VERDANT key are configured."""
34
+ return bool(self.settings.verdant_api_url and self.settings.verdant_api_key)
35
+
36
+ async def wrap(
37
+ self,
38
+ fn: Callable[..., Any],
39
+ *,
40
+ context_type: str | ContextType | None = None,
41
+ input_text: str | None = None,
42
+ metadata: dict[str, Any] | None = None,
43
+ **fn_kwargs: Any,
44
+ ) -> WrapResult:
45
+ """Wrap a local model call and run the reasoning pipeline in-process."""
46
+ return await self.pipeline.run(
47
+ fn=fn,
48
+ context_type=context_type,
49
+ input_text=input_text,
50
+ fn_kwargs=fn_kwargs,
51
+ metadata=metadata,
52
+ )
53
+
54
+ async def run(
55
+ self,
56
+ *,
57
+ context_type: str | ContextType,
58
+ input_text: str,
59
+ metadata: dict[str, Any] | None = None,
60
+ ) -> WrapResult:
61
+ """Run the reasoning pipeline for the given input.
62
+
63
+ In hosted mode (a ``verdant_api_url`` and ``verdant_api_key`` are set) this calls
64
+ the VERDANT API over HTTP, so provider (Claude/Gemini) keys stay server-side. Otherwise
65
+ it runs the pipeline in-process.
66
+ """
67
+ if self._is_hosted:
68
+ return await self._run_hosted(context_type=context_type, input_text=input_text, metadata=metadata)
69
+ return await self.pipeline.run(
70
+ context_type=context_type,
71
+ input_text=input_text,
72
+ metadata=metadata,
73
+ )
74
+
75
+ async def _run_hosted(
76
+ self,
77
+ *,
78
+ context_type: str | ContextType,
79
+ input_text: str,
80
+ metadata: dict[str, Any] | None,
81
+ ) -> WrapResult:
82
+ url = self.settings.verdant_api_url.rstrip("/") + "/pipeline/run"
83
+ context_value = context_type.value if isinstance(context_type, ContextType) else str(context_type)
84
+ payload = {
85
+ "context_type": context_value,
86
+ "input_text": input_text,
87
+ "metadata": metadata or {},
88
+ }
89
+ headers = {"Authorization": f"Bearer {self.settings.verdant_api_key}"}
90
+
91
+ async with httpx.AsyncClient(timeout=self.settings.request_timeout_seconds) as client:
92
+ response = await client.post(url, json=payload, headers=headers)
93
+
94
+ try:
95
+ body = response.json()
96
+ except ValueError:
97
+ body = None
98
+
99
+ if response.status_code >= 400:
100
+ message = None
101
+ if isinstance(body, dict) and body.get("error"):
102
+ message = body["error"].get("message")
103
+ raise VerdantAPIError(message or f"VERDANT API returned {response.status_code}", status_code=response.status_code)
104
+
105
+ if not isinstance(body, dict) or body.get("data") is None:
106
+ raise VerdantAPIError("VERDANT API returned an unexpected response.", status_code=response.status_code)
107
+
108
+ return WrapResult.model_validate(body["data"])
109
+
110
+
111
+ class VerdantAPIError(RuntimeError):
112
+ def __init__(self, message: str, *, status_code: int | None = None) -> None:
113
+ super().__init__(message)
114
+ self.status_code = status_code
verdant/config.py ADDED
@@ -0,0 +1,59 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from functools import lru_cache
5
+
6
+ from pydantic import BaseModel, ConfigDict, Field
7
+
8
+
9
+ def _split_csv(value: str | None, default: list[str]) -> list[str]:
10
+ if not value:
11
+ return default
12
+ return [item.strip() for item in value.split(",") if item.strip()]
13
+
14
+
15
+ class Settings(BaseModel):
16
+ model_config = ConfigDict(extra="ignore")
17
+
18
+ verdant_api_key: str = ""
19
+ verdant_api_url: str = ""
20
+ anthropic_api_key: str = ""
21
+ gemini_api_key: str = ""
22
+ supabase_url: str = ""
23
+ supabase_service_role_key: str = ""
24
+ redis_url: str = "redis://localhost:6379/0"
25
+ webhook_secret: str = ""
26
+ environment: str = "development"
27
+ log_level: str = "info"
28
+ trust_score_alert_threshold: int = Field(default=40, ge=0, le=100)
29
+ cors_origins: list[str] = Field(default_factory=lambda: ["*"])
30
+ claude_model: str = "claude-sonnet-4-6"
31
+ gemini_model: str = "gemini-2.5-flash"
32
+ request_timeout_seconds: float = 30.0
33
+ webhook_timeout_seconds: float = 10.0
34
+
35
+ @classmethod
36
+ def from_env(cls) -> "Settings":
37
+ return cls(
38
+ verdant_api_key=os.getenv("VERDANT_API_KEY", ""),
39
+ verdant_api_url=os.getenv("VERDANT_API_URL", ""),
40
+ anthropic_api_key=os.getenv("ANTHROPIC_API_KEY", ""),
41
+ gemini_api_key=os.getenv("GEMINI_API_KEY", ""),
42
+ supabase_url=os.getenv("SUPABASE_URL", ""),
43
+ supabase_service_role_key=os.getenv("SUPABASE_SERVICE_ROLE_KEY", ""),
44
+ redis_url=os.getenv("REDIS_URL", "redis://localhost:6379/0"),
45
+ webhook_secret=os.getenv("WEBHOOK_SECRET", ""),
46
+ environment=os.getenv("ENVIRONMENT", "development"),
47
+ log_level=os.getenv("LOG_LEVEL", "info"),
48
+ trust_score_alert_threshold=int(os.getenv("TRUST_SCORE_ALERT_THRESHOLD", "40")),
49
+ cors_origins=_split_csv(os.getenv("CORS_ORIGINS"), ["*"]),
50
+ claude_model=os.getenv("CLAUDE_MODEL", "claude-sonnet-4-6"),
51
+ gemini_model=os.getenv("GEMINI_MODEL", "gemini-2.5-flash"),
52
+ request_timeout_seconds=float(os.getenv("REQUEST_TIMEOUT_SECONDS", "30")),
53
+ webhook_timeout_seconds=float(os.getenv("WEBHOOK_TIMEOUT_SECONDS", "10")),
54
+ )
55
+
56
+
57
+ @lru_cache(maxsize=1)
58
+ def get_settings() -> Settings:
59
+ return Settings.from_env()
verdant/models.py ADDED
@@ -0,0 +1,152 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import datetime, timezone
4
+ from enum import Enum
5
+ from typing import Any
6
+ from uuid import UUID, uuid4
7
+
8
+ from pydantic import BaseModel, ConfigDict, Field, field_validator
9
+
10
+
11
+ class VerdantBaseModel(BaseModel):
12
+ model_config = ConfigDict(extra="forbid", populate_by_name=True, protected_namespaces=())
13
+
14
+
15
+ class ContextType(str, Enum):
16
+ hiring = "hiring"
17
+ lending = "lending"
18
+ content = "content"
19
+ healthcare = "healthcare"
20
+
21
+ @classmethod
22
+ def normalize(cls, value: str | ContextType | None) -> ContextType:
23
+ if isinstance(value, cls):
24
+ return value
25
+ if value is None:
26
+ return cls.content
27
+
28
+ normalized = str(value).strip().lower().replace(" ", "_").replace("-", "_")
29
+ aliases = {
30
+ "content_moderation": cls.content,
31
+ "content_moderation_review": cls.content,
32
+ "moderation": cls.content,
33
+ }
34
+ if normalized in aliases:
35
+ return aliases[normalized]
36
+
37
+ for member in cls:
38
+ if member.value == normalized:
39
+ return member
40
+ raise ValueError(f"Unsupported context type: {value}")
41
+
42
+
43
+ class BiasSeverity(str, Enum):
44
+ low = "low"
45
+ medium = "medium"
46
+ high = "high"
47
+ critical = "critical"
48
+
49
+
50
+ class RiskLevel(str, Enum):
51
+ low = "low"
52
+ medium = "medium"
53
+ high = "high"
54
+ critical = "critical"
55
+
56
+
57
+ class IntentStageOutput(VerdantBaseModel):
58
+ detected_intent: str
59
+ context_type: ContextType
60
+ user_intent_summary: str
61
+ entities: list[str] = Field(default_factory=list)
62
+ signals: list[str] = Field(default_factory=list)
63
+ confidence: float = Field(ge=0.0, le=1.0)
64
+ needs_review: bool = False
65
+
66
+
67
+ class BaselineStageOutput(VerdantBaseModel):
68
+ context_type: ContextType
69
+ baseline_name: str
70
+ baseline_version: str
71
+ demographic_focus: list[str] = Field(default_factory=list)
72
+ baseline_summary: str
73
+ policy_notes: list[str] = Field(default_factory=list)
74
+ baseline_data: dict[str, Any] = Field(default_factory=dict)
75
+ source: str = "supabase"
76
+ confidence: float = Field(ge=0.0, le=1.0)
77
+
78
+
79
+ class BiasStageOutput(VerdantBaseModel):
80
+ flags: list[str] = Field(default_factory=list)
81
+ matched_patterns: list[str] = Field(default_factory=list)
82
+ severity: BiasSeverity = BiasSeverity.low
83
+ bias_score: int = Field(ge=0, le=100)
84
+ summary: str
85
+ confidence: float = Field(ge=0.0, le=1.0)
86
+
87
+ @field_validator("bias_score", mode="before")
88
+ @classmethod
89
+ def _clamp_bias_score(cls, value: Any) -> int:
90
+ return max(0, min(100, int(value)))
91
+
92
+
93
+ class ExplainStageOutput(VerdantBaseModel):
94
+ plain_language_explanation: str
95
+ reasoning_summary: list[str] = Field(default_factory=list)
96
+ caveats: list[str] = Field(default_factory=list)
97
+ confidence: float = Field(ge=0.0, le=1.0)
98
+
99
+
100
+ class TrustStageOutput(VerdantBaseModel):
101
+ trust_score: int = Field(ge=0, le=100)
102
+ risk_level: RiskLevel
103
+ score_breakdown: dict[str, float] = Field(default_factory=dict)
104
+ reasons: list[str] = Field(default_factory=list)
105
+ alerts: list[str] = Field(default_factory=list)
106
+ confidence: float = Field(ge=0.0, le=1.0)
107
+
108
+ @field_validator("trust_score", mode="before")
109
+ @classmethod
110
+ def _clamp_trust_score(cls, value: Any) -> int:
111
+ return max(0, min(100, int(value)))
112
+
113
+
114
+ class PipelineStageOutputs(VerdantBaseModel):
115
+ intent: IntentStageOutput
116
+ baseline: BaselineStageOutput
117
+ bias: BiasStageOutput
118
+ explanation: ExplainStageOutput
119
+ trust: TrustStageOutput
120
+
121
+
122
+ class AuditPayload(VerdantBaseModel):
123
+ audit_id: UUID | None = None
124
+ request_id: UUID = Field(default_factory=uuid4)
125
+ created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
126
+ context_type: ContextType
127
+ input_text: str
128
+ output_text: str
129
+ raw_output: Any = None
130
+ clean_output: Any = None
131
+ stages: PipelineStageOutputs
132
+ trust_score: int = Field(ge=0, le=100)
133
+ flags: list[str] = Field(default_factory=list)
134
+ explanation: str
135
+ metadata: dict[str, Any] = Field(default_factory=dict)
136
+ model_name: str = "claude-sonnet-4-6"
137
+ duration_ms: int = 0
138
+ error: str | None = None
139
+
140
+
141
+ class WrapResult(VerdantBaseModel):
142
+ output: Any
143
+ audit: AuditPayload
144
+ trust_score: int = Field(ge=0, le=100)
145
+ flags: list[str] = Field(default_factory=list)
146
+ explanation: str
147
+
148
+
149
+ class PipelineRunRequest(VerdantBaseModel):
150
+ input_text: str
151
+ context_type: ContextType
152
+ metadata: dict[str, Any] = Field(default_factory=dict)
verdant/pipeline.py ADDED
@@ -0,0 +1,274 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import inspect
5
+ import json
6
+ import logging
7
+ import time
8
+ from typing import Any, Callable
9
+
10
+ from .config import Settings, get_settings
11
+ from .models import (
12
+ AuditPayload,
13
+ BaselineStageOutput,
14
+ BiasStageOutput,
15
+ ContextType,
16
+ ExplainStageOutput,
17
+ IntentStageOutput,
18
+ PipelineStageOutputs,
19
+ RiskLevel,
20
+ TrustStageOutput,
21
+ WrapResult,
22
+ )
23
+ from .services.cache_service import CacheService
24
+ from .services.claude_service import ClaudeService
25
+ from .services.db_service import DBService
26
+ from .services.gemini_service import GeminiService
27
+ from .stages.baseline import load_baseline
28
+ from .stages.bias import match_bias_patterns
29
+ from .stages.explain import generate_explanation
30
+ from .stages.intent import extract_intent
31
+ from .stages.trust import synthesize_trust_score
32
+
33
+ logger = logging.getLogger(__name__)
34
+
35
+
36
+ def _json_safe(value: Any) -> Any:
37
+ if hasattr(value, "model_dump"):
38
+ return value.model_dump(mode="json")
39
+ if isinstance(value, dict):
40
+ return {str(key): _json_safe(item) for key, item in value.items()}
41
+ if isinstance(value, list):
42
+ return [_json_safe(item) for item in value]
43
+ if isinstance(value, tuple):
44
+ return [_json_safe(item) for item in value]
45
+ if isinstance(value, set):
46
+ return [_json_safe(item) for item in value]
47
+ return str(value)
48
+
49
+
50
+ def _stringify_output(value: Any) -> str:
51
+ if value is None:
52
+ return ""
53
+ if isinstance(value, str):
54
+ return value
55
+ if isinstance(value, dict):
56
+ if isinstance(value.get("output"), str):
57
+ return value["output"]
58
+ if "choices" in value:
59
+ choices = value.get("choices") or []
60
+ if choices:
61
+ first = choices[0]
62
+ message = first.get("message") if isinstance(first, dict) else getattr(first, "message", None)
63
+ if isinstance(message, dict) and isinstance(message.get("content"), str):
64
+ return message["content"]
65
+ if hasattr(message, "content"):
66
+ content = getattr(message, "content")
67
+ if isinstance(content, str):
68
+ return content
69
+ return json.dumps(value, ensure_ascii=False, default=str)
70
+ for attr in ("output_text", "text"):
71
+ attr_value = getattr(value, attr, None)
72
+ if isinstance(attr_value, str):
73
+ return attr_value
74
+ if hasattr(value, "model_dump"):
75
+ dumped = value.model_dump(mode="json")
76
+ if isinstance(dumped, dict):
77
+ return _stringify_output(dumped)
78
+ return str(value)
79
+
80
+
81
+ def _derive_input_text(fn_kwargs: dict[str, Any]) -> str:
82
+ for key in ("input_text", "prompt", "content", "text", "message"):
83
+ value = fn_kwargs.get(key)
84
+ if isinstance(value, str) and value.strip():
85
+ return value
86
+ messages = fn_kwargs.get("messages")
87
+ if isinstance(messages, list) and messages:
88
+ parts: list[str] = []
89
+ for message in messages:
90
+ if isinstance(message, dict):
91
+ role = message.get("role", "user")
92
+ content = message.get("content", "")
93
+ else:
94
+ role = getattr(message, "role", "user")
95
+ content = getattr(message, "content", "")
96
+ parts.append(f"{role}: {content}")
97
+ if parts:
98
+ return "\n".join(parts)
99
+ return json.dumps(_json_safe(fn_kwargs), ensure_ascii=False, default=str)
100
+
101
+
102
+ async def _call_target(fn: Callable[..., Any], fn_kwargs: dict[str, Any]) -> Any:
103
+ result = fn(**fn_kwargs)
104
+ if inspect.isawaitable(result):
105
+ return await result
106
+ return result
107
+
108
+
109
+ class VerdantPipeline:
110
+ def __init__(
111
+ self,
112
+ settings: Settings | None = None,
113
+ *,
114
+ claude_service: ClaudeService | None = None,
115
+ gemini_service: GeminiService | None = None,
116
+ cache_service: CacheService | None = None,
117
+ db_service: DBService | None = None,
118
+ ) -> None:
119
+ self.settings = settings or get_settings()
120
+ self.claude_service = claude_service or ClaudeService(self.settings)
121
+ self.gemini_service = gemini_service or GeminiService(self.settings)
122
+ self.cache_service = cache_service or CacheService(self.settings)
123
+ self.db_service = db_service or DBService(self.settings)
124
+
125
+ async def _generate_default_output(
126
+ self,
127
+ input_text: str,
128
+ intent: IntentStageOutput,
129
+ baseline: BaselineStageOutput,
130
+ metadata: dict[str, Any],
131
+ ) -> str:
132
+ prompt = (
133
+ "Respond helpfully to the user's request and return JSON only.\n\n"
134
+ f"Context type: {intent.context_type.value}\n"
135
+ f"Intent summary: {intent.user_intent_summary}\n"
136
+ f"Baseline summary: {baseline.baseline_summary}\n"
137
+ f"Input text:\n{input_text}\n\n"
138
+ f"Metadata:\n{json.dumps(_json_safe(metadata), ensure_ascii=False, default=str)}\n"
139
+ )
140
+
141
+ try:
142
+ return await self.claude_service.generate_text("generate", prompt)
143
+ except Exception as exc:
144
+ logger.warning("Default output generation failed, using input echo fallback: %s", exc)
145
+ return input_text
146
+
147
+ async def run(
148
+ self,
149
+ fn: Callable[..., Any] | None = None,
150
+ *,
151
+ context_type: str | ContextType | None = None,
152
+ input_text: str | None = None,
153
+ fn_kwargs: dict[str, Any] | None = None,
154
+ metadata: dict[str, Any] | None = None,
155
+ ) -> WrapResult:
156
+ started_at = time.perf_counter()
157
+ metadata = metadata or {}
158
+ fn_kwargs = dict(fn_kwargs or {})
159
+ resolved_input_text = input_text or _derive_input_text(fn_kwargs)
160
+
161
+ intent = await extract_intent(
162
+ resolved_input_text,
163
+ context_type=context_type,
164
+ settings=self.settings,
165
+ claude_service=self.claude_service,
166
+ gemini_service=self.gemini_service,
167
+ )
168
+
169
+ baseline_context = ContextType.normalize(context_type) if context_type else intent.context_type
170
+ baseline = await load_baseline(
171
+ baseline_context,
172
+ settings=self.settings,
173
+ db_service=self.db_service,
174
+ cache_service=self.cache_service,
175
+ )
176
+
177
+ pipeline_error: str | None = None
178
+
179
+ if fn is None:
180
+ try:
181
+ raw_output: Any = await self._generate_default_output(resolved_input_text, intent, baseline, metadata)
182
+ clean_output: Any = raw_output
183
+ except Exception as exc: # pragma: no cover - catch-all guard
184
+ logger.exception("Default generation failed unexpectedly: %s", exc)
185
+ raw_output = {"error": str(exc)}
186
+ clean_output = ""
187
+ pipeline_error = str(exc)
188
+ else:
189
+ try:
190
+ raw_output = await _call_target(fn, fn_kwargs)
191
+ clean_output = _json_safe(raw_output)
192
+ except Exception as exc:
193
+ logger.warning("Wrapped function raised; continuing with fallback output: %s", exc)
194
+ raw_output = {"error": str(exc)}
195
+ clean_output = ""
196
+ pipeline_error = str(exc)
197
+
198
+ output_text = _stringify_output(clean_output)
199
+ bias = await match_bias_patterns(
200
+ resolved_input_text,
201
+ output_text,
202
+ intent=intent,
203
+ baseline=baseline,
204
+ )
205
+ explanation = await generate_explanation(
206
+ resolved_input_text,
207
+ output_text,
208
+ intent=intent,
209
+ baseline=baseline,
210
+ bias=bias,
211
+ settings=self.settings,
212
+ claude_service=self.claude_service,
213
+ gemini_service=self.gemini_service,
214
+ )
215
+ trust = await synthesize_trust_score(
216
+ intent=intent,
217
+ baseline=baseline,
218
+ bias=bias,
219
+ explanation=explanation,
220
+ settings=self.settings,
221
+ db_service=self.db_service,
222
+ )
223
+
224
+ if pipeline_error:
225
+ explanation = explanation.model_copy(
226
+ update={
227
+ "plain_language_explanation": (
228
+ f"{explanation.plain_language_explanation} "
229
+ f"The wrapped AI call failed: {pipeline_error}."
230
+ ).strip(),
231
+ "caveats": explanation.caveats + ["No valid upstream output was produced."],
232
+ }
233
+ )
234
+ trust = trust.model_copy(
235
+ update={
236
+ "trust_score": min(trust.trust_score, 15),
237
+ "risk_level": RiskLevel.critical,
238
+ "alerts": list(dict.fromkeys(trust.alerts + ["Wrapped function call failed"])),
239
+ "reasons": trust.reasons + [f"Upstream call failed: {pipeline_error}."],
240
+ "score_breakdown": {**trust.score_breakdown, "upstream_call_failed": 0.0},
241
+ }
242
+ )
243
+
244
+ stages = PipelineStageOutputs(
245
+ intent=intent,
246
+ baseline=baseline,
247
+ bias=bias,
248
+ explanation=explanation,
249
+ trust=trust,
250
+ )
251
+
252
+ audit = AuditPayload(
253
+ context_type=ContextType.normalize(intent.context_type),
254
+ input_text=resolved_input_text,
255
+ output_text=output_text,
256
+ raw_output=_json_safe(raw_output),
257
+ clean_output=_json_safe(clean_output),
258
+ stages=stages,
259
+ trust_score=trust.trust_score,
260
+ flags=list(bias.flags),
261
+ explanation=explanation.plain_language_explanation,
262
+ metadata=_json_safe(metadata),
263
+ model_name=self.settings.claude_model,
264
+ duration_ms=int((time.perf_counter() - started_at) * 1000),
265
+ error=raw_output.get("error") if isinstance(raw_output, dict) else None,
266
+ )
267
+
268
+ return WrapResult(
269
+ output=clean_output,
270
+ audit=audit,
271
+ trust_score=trust.trust_score,
272
+ flags=list(bias.flags),
273
+ explanation=explanation.plain_language_explanation,
274
+ )
@@ -0,0 +1 @@
1
+ # Service package marker.