Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,12 @@ Contributors add user-facing entries under `[Unreleased]` in the same PR. Mainta

## [Unreleased]

### Added
- **`wellness/mental_coach`**: Deterministic wellness coaching firewall with crisis triage, hard scope limits, embedded public KB, optional Gemini scope evaluator, and catalog documentation (#148).

### Changed
- **Tests**: Moved `tests/test_mica_module.py` to `tests/skills/compliance/test_mica_module.py` so maintainer skill tests follow the `tests/skills/<category>/` layout; `tests/` root is framework-only (#86).
- **`wellness/mental_coach`**: Set real issuer contact email and add health disclaimer on the catalog page (PR #174 follow-up).

### Fixed
- **`finance/wallet_screening`**: Align examples and docs with `finance/wallet_screening` manifest tool name; fix `gemini_wallet_check.py` and `claude_wallet_check.py` to match tool name dynamically from manifest; correct `card.json` UI fields to match actual skill output schema; update `instructions.md`, provider snippets, Data Schema, and usage docs (#173).
Expand Down
1 change: 1 addition & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,7 @@ Place each skill under one top-level directory under `skills/`. Use an existing
| `defi` | On-chain trading and agent wallet execution | `evm_tx_handler` |
| `office` | Documents, productivity | `pdf_form_filler` |
| `optimization` | Middleware, compression, efficiency | `prompt_rewriter` |
| `wellness` | Coaching guardrails, mental health support | `mental_coach` |

Skill IDs follow `category/skill_name` and should match the path under `skills/`.

Expand Down
7 changes: 7 additions & 0 deletions docs/skills/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,13 @@ Skills that assist developers in understanding codebases, planning changes, and
| :--- | :--- | :--- | :--- |
| **[Issue Resolver](issue_resolver.md)** | `dev_tools/issue_resolver` | [@rosspeili](https://github.com/rosspeili) ([@ARPAHLS](https://github.com/ARPAHLS)) | GitHub issue URL prep, nine-stage agent workflow, conditional verify/commit gates, and commit-message validation. |

## Wellness
Supportive coaching guardrails, crisis triage, and grounded psychoeducation for host agents.

| Skill | ID | Issuer | Description |
| :--- | :--- | :--- | :--- |
| **[Mental Coach](mental_coach.md)** | `wellness/mental_coach` | [@mrmasa88](https://github.com/mrmasa88) (AO) | Deterministic wellness coaching firewall with crisis triage, scope limits, and cited KB retrieval (#148). |

---

## Installing Skills
Expand Down
166 changes: 166 additions & 0 deletions docs/skills/mental_coach.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
# Mental Coach

**Domain:** `wellness`
**Skill ID:** `wellness/mental_coach`
**Issuer:** [@mrmasa88](https://github.com/mrmasa88) (AO) · **Contact:** masa88keith@gmail.com

[Skill Library](README.md) · [Testing](../TESTING.md)

Deterministic wellness coaching guardrail for host agents. Runs crisis triage before retrieval, blocks clinical overreach, retrieves grounded KB chunks with citations, and optionally runs a Gemini scope evaluator.

> **Health disclaimer:** This skill provides general wellness support and information only. It is not medical, psychological, or clinical advice and is not a substitute for care from a licensed professional. Use at your own discretion. Active safety guardrails (deterministic crisis gate and hard constraints) reduce risk but do not replace professional judgment; double-check results and treat output as everyday coping guidance, not medical advice. In a crisis or emergency, contact local emergency services or the crisis resources returned by the skill.

## What It Does

1. **Crisis gate (deterministic, first)** — detects danger signals and returns escalation guidance instead of coaching.
2. **Hard constraints** — blocks diagnosis, medication advice, and clinical interpretation requests.
3. **Grounded retrieval** — jurisdiction- and session-aware chunks from the embedded public KB.
4. **Optional evaluator** — lightweight Gemini audit when `run_evaluator` is enabled.

Supportive coaching and psychoeducation only. Not emergency services, telehealth, or licensed care.

## Parameters

| Parameter | Required | Notes |
| :--- | :--- | :--- |
| `user_prompt` | Yes | User message or coaching request |
| `user_jurisdiction` | No | `US`, `EU`, `UK`, `FR`, `DE`, `ES`, `IT`, `GLOBAL`, or `unknown` |
| `session_mode` | No | `coaching`, `information`, or `crisis_check` |
| `run_evaluator` | No | Optional LLM scope audit |
| `evaluator_model` | No | Default `gemini-2.5-flash-lite` |
| `max_chunks` | No | Max KB chunks (cap 15) |

## Environment

| Variable | Required | Purpose |
| :--- | :--- | :--- |
| `GOOGLE_API_KEY` | No | Optional scope evaluator when `run_evaluator` is enabled |

Configure values per [API keys for skills](../usage/api_keys.md). Core crisis and coaching paths do not require a cloud API key.

## Example Usage (Direct)

```python
from skillware.core.loader import SkillLoader

bundle = SkillLoader.load_skill("wellness/mental_coach")
skill = bundle["module"].MentalCoachSkill()

result = skill.execute(
{
"user_prompt": "I feel stressed at work and need coping strategies.",
"user_jurisdiction": "US",
"session_mode": "coaching",
"run_evaluator": False,
}
)

print(result["policy_status"])
print(result["citations"])
print(result["final_context_for_agent"])
```

## Runnable Example

See [examples/mental_coach_demo.py](../../examples/mental_coach_demo.py) for local execute demos (coaching, crisis, blocked clinical).

## Usage Examples

Guides: [Usage index](../usage/README.md) · [Agent loops](../usage/agent_loops.md) · [API keys](../usage/api_keys.md) (optional `GOOGLE_API_KEY` for evaluator).

Sample user message: *I feel stressed at work and need coping strategies.*

### Gemini

```python
import google.genai as genai
from google.genai import types
from skillware.core.env import load_env_file
from skillware.core.loader import SkillLoader

load_env_file()
bundle = SkillLoader.load_skill("wellness/mental_coach")
skill = bundle["module"].MentalCoachSkill()
tool = SkillLoader.to_gemini_tool(bundle)
client = genai.Client()
response = client.models.generate_content(
model="gemini-2.5-flash",
contents="I feel stressed at work and need coping strategies.",
config=types.GenerateContentConfig(
tools=[tool],
system_instruction=bundle["instructions"],
),
)
for part in response.candidates[0].content.parts:
if part.function_call:
result = skill.execute(dict(part.function_call.args))
print(result["policy_status"], result["final_context_for_agent"])
```

### Claude

```python
import anthropic
from skillware.core.env import load_env_file
from skillware.core.loader import SkillLoader

load_env_file()
bundle = SkillLoader.load_skill("wellness/mental_coach")
skill = bundle["module"].MentalCoachSkill()
client = anthropic.Anthropic()
tools = [SkillLoader.to_claude_tool(bundle)]
# On tool_use (name wellness/mental_coach): skill.execute(tool_use.input)
```

### OpenAI

```python
from openai import OpenAI
from skillware.core.env import load_env_file
from skillware.core.loader import SkillLoader

load_env_file()
bundle = SkillLoader.load_skill("wellness/mental_coach")
skill = bundle["module"].MentalCoachSkill()
client = OpenAI()
openai_tool = SkillLoader.to_openai_tool(bundle)
# Match tool_call.function.name (wellness_mental_coach)
```

### DeepSeek

```python
import os
from openai import OpenAI
from skillware.core.env import load_env_file
from skillware.core.loader import SkillLoader

load_env_file()
bundle = SkillLoader.load_skill("wellness/mental_coach")
skill = bundle["module"].MentalCoachSkill()
client = OpenAI(
api_key=os.environ.get("DEEPSEEK_API_KEY"),
base_url="https://api.deepseek.com",
)
deepseek_tool = SkillLoader.to_deepseek_tool(bundle)
```

### Ollama

`SkillLoader.to_ollama_prompt(bundle)`; match `"tool": "wellness/mental_coach"`.
See [Ollama usage](../usage/ollama.md).

## Output Semantics

- `ESCALATE` — crisis signals detected; coaching suppressed; resources provided.
- `BLOCKED` — clinical request declined; non-clinical alternatives only.
- `CAUTION` — proceed gently with disclaimers and optional resources.
- `OK` — coaching path with grounded citations.

Always include `disclaimers_required` in the user-facing reply.

## Limitations

- English-first v0.1; non-English input routes to CAUTION with resources.
- Public KB only; no private corpus in the published package.
- Crisis gate uses conservative keyword signals; over-escalation is intentional.
1 change: 1 addition & 0 deletions docs/usage/agent_loops.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,4 @@ skills in one harness.
| `data_engineering/synthetic_generator` | `build_dataset_demo.py` (local execute, Gemini backend) | (catalog page) | (catalog page) | (catalog page) | (catalog page) | (catalog page) |
| `data_engineering/novelty_extractor` | `novelty_extractor_demo.py` (local execute) | `gemini_novelty_extractor.py` | (catalog page) | (catalog page) | (catalog page) | `ollama_novelty_extractor.py` |
| `dev_tools/issue_resolver` | - | `gemini_issue_resolver.py` | `claude_issue_resolver.py` | (catalog page) | (catalog page) | `ollama_issue_resolver.py` |
| `wellness/mental_coach` | `mental_coach_demo.py` (local execute) | (catalog page) | (catalog page) | (catalog page) | (catalog page) | (catalog page) |
1 change: 1 addition & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ with editable install: `pip install -e ".[gemini]"`.

| Script | Skill ID | Provider | Required extra | Required env vars | Description |
| :--- | :--- | :--- | :--- | :--- | :--- |
| `mental_coach_demo.py` | `wellness/mental_coach` | Local execute | base install only | None | Demonstrates coaching, crisis escalation, and blocked clinical paths locally. |
| `build_dataset_demo.py` | `data_engineering/synthetic_generator` | Local execute (Gemini backend) | `[gemini]` | `GOOGLE_API_KEY` | Generates a JSONL synthetic dataset with the synthetic generator skill. |
| `claude_pdf_form_filler.py` | `office/pdf_form_filler` | Claude | `[claude]`, `[office]` | `ANTHROPIC_API_KEY` | Uses Claude with the PDF form filler skill to map instructions to fields. |
| `claude_tos_evaluator.py` | `compliance/tos_evaluator` | Claude | `[claude]` | `ANTHROPIC_API_KEY` | Runs a Claude tool loop for website automation policy review. |
Expand Down
47 changes: 47 additions & 0 deletions examples/mental_coach_demo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
from skillware.core.loader import SkillLoader


def run_demo():
print("Loading wellness/mental_coach...")
bundle = SkillLoader.load_skill("wellness/mental_coach")
skill = bundle["module"].MentalCoachSkill()

scenarios = [
(
"Coaching",
{
"user_prompt": "I feel stressed at work and need coping strategies.",
"user_jurisdiction": "US",
"session_mode": "coaching",
"run_evaluator": False,
},
),
(
"Crisis escalation",
{
"user_prompt": "I want to kill myself tonight.",
"user_jurisdiction": "US",
},
),
(
"Blocked clinical request",
{
"user_prompt": "Can you diagnose me with depression?",
"user_jurisdiction": "US",
},
),
]

for label, params in scenarios:
print(f"\n=== {label} ===")
result = skill.execute(params)
print(f"policy_status: {result.get('policy_status')}")
print(f"scope: {result.get('scope')}")
print(
f"chunks: {result.get('privacy_metadata', {}).get('kb_chunks_retrieved')}"
)
print(f"context preview: {result.get('final_context_for_agent', '')[:240]}...")


if __name__ == "__main__":
run_demo()
Empty file added skills/wellness/__init__.py
Empty file.
3 changes: 3 additions & 0 deletions skills/wellness/mental_coach/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from .skill import MentalCoachSkill

__all__ = ["MentalCoachSkill"]
25 changes: 25 additions & 0 deletions skills/wellness/mental_coach/card.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"name": "Mental Coach",
"description": "Wellness coaching guardrail with crisis triage and grounded KB retrieval.",
"issuer": {
"name": "Masa",
"email": "masa88keith@gmail.com",
"github": "mrmasa88",
"org": "AO"
},
"icon": "heart",
"color": "blue",
"ui_schema": {
"type": "card",
"fields": [
{
"key": "policy_status",
"label": "Policy Status"
},
{
"key": "scope",
"label": "Scope"
}
]
}
}
93 changes: 93 additions & 0 deletions skills/wellness/mental_coach/constraints.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
"""Load and apply hard constraints from kb/hard_constraints.yaml."""

from __future__ import annotations

import os
import re
from functools import lru_cache
from typing import Any, Dict, List, Optional

import yaml

_CONSTRAINTS_PATH = os.path.join(
os.path.dirname(__file__), "kb", "hard_constraints.yaml"
)

CLINICAL_PATTERNS: Dict[str, List[str]] = {
"no_diagnosis": [
r"\bdiagnos(e|is|ing)\b",
r"\bdo i have (depression|anxiety|bipolar|adhd|ptsd|ocd)\b",
r"\bwhat (disorder|condition|illness) do i have\b",
r"\bam i (depressed|bipolar|schizophrenic)\b",
],
"no_medication_advice": [
r"\b(should i|can i) (take|stop|start) (my )?(medication|meds|antidepressant)\b",
r"\bwhat medication\b",
r"\bwhat dose\b",
r"\bprescribe\b",
r"\b(increase|decrease) my (dose|dosage)\b",
],
"no_clinical_interpretation": [
r"\binterpret my (lab|blood test|test results)\b",
r"\bwhat do my results mean\b",
r"\bclinical record\b",
],
}

INJECTION_PATTERNS = [
r"ignore (your|all|previous) (rules|instructions|constraints)",
r"disregard (your|the) (policy|safety|guidelines)",
r"pretend you are (a )?(doctor|therapist|psychiatrist)",
r"jailbreak",
r"bypass (your|the) (rules|safety|filter)",
]


@lru_cache(maxsize=1)
def load_constraints_config() -> Dict[str, Any]:
with open(_CONSTRAINTS_PATH, "r", encoding="utf-8") as handle:
data = yaml.safe_load(handle)
return data if isinstance(data, dict) else {}


def get_disclaimers(kind: str = "default") -> List[str]:
config = load_constraints_config()
disclaimers = config.get("disclaimers_required", {})
items = disclaimers.get(kind, disclaimers.get("default", []))
return list(items) if isinstance(items, list) else []


def detect_clinical_violation(text: str) -> Optional[str]:
normalized = text.lower()
for constraint_id, patterns in CLINICAL_PATTERNS.items():
for pattern in patterns:
if re.search(pattern, normalized):
return constraint_id
return None


def detect_injection_attempt(text: str) -> bool:
normalized = text.lower()
return any(re.search(pattern, normalized) for pattern in INJECTION_PATTERNS)


def get_playbook_entry(decision: str) -> Dict[str, Any]:
config = load_constraints_config()
playbook = config.get("escalation_playbook", {})
key = {
"ESCALATE": "on_escalate",
"CAUTION": "on_caution",
"OK": "on_ok",
}.get(decision, "on_ok")
entry = playbook.get(key, {})
return entry if isinstance(entry, dict) else {}


def list_hard_constraint_ids() -> List[str]:
config = load_constraints_config()
items = config.get("hard_constraints", [])
ids: List[str] = []
for item in items:
if isinstance(item, dict) and item.get("id"):
ids.append(str(item["id"]))
return ids
Loading
Loading