Skill: manmeet3591/lca-skill (version 0.1.0)

--- name: lca description: AI-guided Life Cycle Assessment using openLCA. Connects to openLCA via IPC to help non-experts build product systems, run impact assessments, and interpret results. version: 0.1.0 metadata: openclaw: requires: bins: - python3 env: - OPENLCA_IPC_PORT primaryEnv: OPENLCA_IPC_PORT emoji: "\U0001F30D" homepage: https://github.com/manmeet3591/claw_lca install: - kind: uv package: olca-ipc bins: [] --- # LCA — Life Cycle Assessment Skill You are an AI-guided Life Cycle Assessment assistant. You help users perform environmental impact assessments using openLCA, even if they have no prior LCA experience. ## When to activate Activate this skill when the user mentions any of: - Life cycle assessment / LCA - Carbon footprint analysis - Environmental impact assessment - Sustainability analysis of a product or process - Comparing environmental impact of materials or products - LCIA (Life Cycle Impact Assessment) - openLCA ## Prerequisites Before running any LCA commands, confirm with the user: 1. **openLCA 2.x** is installed and running on their machine (or a reachable server) 2. **IPC server is started** in openLCA: Tools > Developer tools > IPC Server (default port: 8080) 3. **A database is open** in openLCA with relevant data (e.g., ecoinvent, ELCD, or USDA LCA Commons) 4. The environment variable `OPENLCA_IPC_PORT` is set (default: `8080`) If prerequisites are not met, guide the user through setup. ## How to use the bridge script All openLCA operations go through the Python bridge script. Run commands like: ```bash python3 scripts/lca_bridge.py <command> [args...] ``` The script connects to `localhost:$OPENLCA_IPC_PORT` (default 8080) and returns JSON. ### Available commands | Command | Arguments | Description | |---------|-----------|-------------| | `ping` | — | Check if openLCA IPC server is reachable | | `list_databases` | — | List available databases | | `list_processes` | `[search_term]` | List processes, optionally filtered | | `list_flows` | `[search_term]` | List flows, optionally filtered | | `list_impact_methods` | — | List available LCIA impact methods | | `list_impact_categories` | `<method_id>` | List categories for an impact method | | `get_process` | `<process_id>` | Get full details of a process | | `create_product_system` | `<process_id>` | Auto-create a product system from a process | | `calculate` | `<system_id> <method_id>` | Run LCIA calculation | | `get_result` | `<result_id>` | Get impact assessment results | | `get_contributions` | `<result_id> <category_id>` | Get process contributions for an impact category | ### Example workflow ```bash # 1. Check connection python3 scripts/lca_bridge.py ping # 2. Find a process python3 scripts/lca_bridge.py list_processes "paper cup" # 3. Create product system python3 scripts/lca_bridge.py create_product_system <process_id> # 4. Find an impact method python3 scripts/lca_bridge.py list_impact_methods # 5. Run calculation python3 scripts/lca_bridge.py calculate <system_id> <method_id> # 6. Get results python3 scripts/lca_bridge.py get_result <result_id> ``` ## Guided workflow When a user asks for an LCA, follow these steps: ### Step 1: Understand the goal Ask the user: - What product, process, or service do they want to assess? - What is the functional unit? (e.g., "1 paper cup", "1 kg of steel", "1 kWh of electricity") - What environmental impacts are they interested in? (climate change, water use, acidification, etc.) ### Step 2: Connect and explore - Ping the openLCA IPC server to confirm connection - List available processes to find relevant ones - If the exact process isn't available, suggest alternatives or explain what data they need ### Step 3: Build the product system - Help the user select the reference process - Create a product system using `create_product_system` - Explain what a product system is (the network of connected processes) ### Step 4: Select impact method - List available LCIA methods (e.g., ReCiPe, CML, TRACI, EF 3.0) - Recommend an appropriate method based on the user's goals and region - Briefly explain what the method measures ### Step 5: Calculate and interpret - Run the LCIA calculation - Present results in a clear table with units - Highlight the most significant impact categories - Identify hotspots (which processes contribute most) - Explain results in plain language ### Step 6: Compare (if requested) - If the user wants to compare alternatives, repeat steps 3-5 for each option - Present a side-by-side comparison table - Provide clear recommendations ## Key LCA concepts to explain when relevant - **Functional unit**: The quantified performance of a product system (what you're comparing) - **System boundary**: What's included/excluded in the assessment - **Life cycle stages**: Raw material extraction → Manufacturing → Use → End of life - **LCIA**: Translating inventory data (emissions, resources) into environmental impact scores - **Impact categories**: Climate change (kg CO2-eq), acidification, eutrophication, etc. - **Hotspot analysis**: Identifying which processes or flows contribute most to impacts ## Error handling - If the IPC server is unreachable, guide the user to start it in openLCA - If no database is open, tell the user to open one in openLCA - If a process is not found, suggest searching with different terms or checking available databases - Always show the raw error message from the bridge script for debugging

path: scripts/lca_bridge.py
content: #!/usr/bin/env python3
"""
lca_bridge.py — CLI bridge between OpenClaw and openLCA IPC server.

Usage:
    python3 lca_bridge.py <command> [args...]

All output is JSON to stdout. Errors return {"error": "message"}.
Connects to localhost on the port specified by OPENLCA_IPC_PORT (default 8080).
"""

import json
import os
import sys

try:
    import olca_ipc as ipc
    import olca_schema as o
except ImportError:
    print(json.dumps({"error": "olca-ipc not installed. Run: pip install olca-ipc"}))
    sys.exit(1)


def get_client():
    port = int(os.environ.get("OPENLCA_IPC_PORT", "8080"))
    return ipc.Client(port)


def out(data):
    print(json.dumps(data, indent=2, default=str))


def cmd_ping():
    try:
        client = get_client()
        # Try to list something small to verify connection
        client.get_descriptors(o.Flow)
        out({"status": "connected", "port": int(os.environ.get("OPENLCA_IPC_PORT", "8080"))})
    except Exception as e:
        out({"error": f"Cannot connect to openLCA IPC server: {e}"})


def cmd_list_processes(search=None):
    client = get_client()
    descriptors = client.get_descriptors(o.Process)
    processes = [{"id": d.id, "name": d.name} for d in descriptors]
    if search:
        search_lower = search.lower()
        processes = [p for p in processes if search_lower in p["name"].lower()]
    out({"count": len(processes), "processes": processes[:100]})


def cmd_list_flows(search=None):
    client = get_client()
    descriptors = client.get_descriptors(o.Flow)
    flows = [{"id": d.id, "name": d.name} for d in descriptors]
    if search:
        search_lower = search.lower()
        flows = [f for f in flows if search_lower in f["name"].lower()]
    out({"count": len(flows), "flows": flows[:100]})


def cmd_list_impact_methods():
    client = get_client()
    descriptors = client.get_descriptors(o.ImpactMethod)
    methods = [{"id": d.id, "name": d.name} for d in descriptors]
    out({"count": len(methods), "methods": methods})


def cmd_list_impact_categories(method_id):
    client = get_client()
    method = client.get(o.ImpactMethod, method_id)
    if not method:
        out({"error": f"Impact method {method_id} not found"})
        return
    categories = []
    if method.impact_categories:
        for cat in method.impact_categories:
            categories.append({"id": cat.id, "name": cat.name})
    out({"method": method.name, "count": len(categories), "categories": categories})


def cmd_get_process(process_id):
    client = get_client()
    process = client.get(o.Process, process_id)
    if not process:
        out({"error": f"Process {process_id} not found"})
        return
    exchanges = []
    if process.exchanges:
        for ex in process.exchanges:
            exchanges.append({
                "flow": ex.flow.name if ex.flow else None,
                "amount": ex.amount,
                "unit": ex.unit.name if ex.unit else None,
                "is_input": ex.is_input,
                "is_quantitative_reference": ex.is_quantitative_reference,
            })
    out({
        "id": process.id,
        "name": process.name,
        "description": process.description,
        "process_type": str(process.process_type) if process.process_type else None,
        "exchanges": exchanges,
    })


def cmd_create_product_system(process_id):
    client = get_client()
    config = o.LinkingConfig(
        prefer_unit_processes=True,
        provider_linking=o.ProviderLinking.PREFER_DEFAULTS,
    )
    system = client.create_product_system(process_id, config)
    if not system:
        out({"error": f"Failed to create product system for process {process_id}"})
        return
    out({
        "id": system.id,
        "name": system.name,
        "description": system.description,
    })


def cmd_calculate(system_id, method_id):
    client = get_client()
    setup = o.CalculationSetup(
        target=o.Ref(ref_type=o.RefType.ProductSystem, id=system_id),
        impact_method=o.Ref(id=method_id),
    )
    result = client.calculate(setup)
    if not result:
        out({"error": "Calculation failed"})
        return

    impacts = []
    for impact in result.get_total_impacts():
        impacts.append({
            "category": impact.impact_category.name if impact.impact_category else None,
            "value": impact.amount,
            "unit": impact.impact_category.ref_unit if impact.impact_category else None,
        })

    result_id = result.uid if hasattr(result, 'uid') else id(result)
    out({
        "result_id": str(result_id),
        "system_id": system_id,
        "method_id": method_id,
        "impacts": impacts,
    })


def cmd_get_contributions(result_id, category_id):
    out({
        "info": "Contribution analysis requires an active result handle. "
                "Run 'calculate' first and use the result object directly. "
                "This is a placeholder for the full implementation.",
        "result_id": result_id,
        "category_id": category_id,
    })


COMMANDS = {
    "ping": (cmd_ping, 0, 0),
    "list_processes": (cmd_list_processes, 0, 1),
    "list_flows": (cmd_list_flows, 0, 1),
    "list_impact_methods": (cmd_list_impact_methods, 0, 0),
    "list_impact_categories": (cmd_list_impact_categories, 1, 1),
    "get_process": (cmd_get_process, 1, 1),
    "create_product_system": (cmd_create_product_system, 1, 1),
    "calculate": (cmd_calculate, 2, 2),
    "get_result": (cmd_calculate, 2, 2),  # alias
    "get_contributions": (cmd_get_contributions, 2, 2),
}


def main():
    if len(sys.argv) < 2 or sys.argv[1] in ("-h", "--help"):
        out({
            "usage": "python3 lca_bridge.py <command> [args...]",
            "commands": list(COMMANDS.keys()),
        })
        sys.exit(0)

    cmd_name = sys.argv[1]
    args = sys.argv[2:]

    if cmd_name not in COMMANDS:
        out({"error": f"Unknown command: {cmd_name}", "available": list(COMMANDS.keys())})
        sys.exit(1)

    fn, min_args, max_args = COMMANDS[cmd_name]
    if len(args) < min_args or len(args) > max_args:
        out({"error": f"'{cmd_name}' expects {min_args}-{max_args} args, got {len(args)}"})
        sys.exit(1)

    try:
        fn(*args)
    except Exception as e:
        out({"error": str(e)})
        sys.exit(1)


if __name__ == "__main__":
    main()


path: scripts/requirements.txt
content: olca-ipc>=2.4.0


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: clean (LOW)