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
6 changes: 3 additions & 3 deletions README.ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ CIの状態、必須check、main保護の手順は [CI / main branch quality gat

## LLM/API非連携方針

OpenAI、Anthropic、Google、Azure、Ollama、ローカルLLMを含むLLM連携は実装しない。ラベル付け、分析、レポートはルールベースと統計処理で行う。
OpenAI、Anthropic、Google、Azure、Ollama、ローカルLLMを含むLLM連携は実装しない。ラベル付け、分析、レポートはルールベースと統計処理で行う。ユーザーがローカル出力を手動で外部へ渡すことは連携ではなく、OpsMineFlowから送信・接続・応答取込は行わない。

## 商用利用しやすいApache-2.0ライセンス

Expand All @@ -42,7 +42,7 @@ OpenAI、Anthropic、Google、Azure、Ollama、ローカルLLMを含むLLM連携
- ボトルネック候補抽出
- 繰り返し作業・アプリ往復検出
- 自動化候補スコアリング
- Markdown/HTML/CSV/JSON/Mermaid/SVG/draw.io系エクスポート
- Markdown/HTML/CSV/JSON/Mermaid/SVG/draw.io系エクスポート、手動Mermaid handoff ZIP

## ローカル製品版スコープ

Expand Down Expand Up @@ -115,7 +115,7 @@ CSVでは主に `case_id`、`activity`、`timestamp_start`、`timestamp_end`、`

### 6. 結果を出力する

**ホーム > 出力** でMarkdown、JSON、CSV、Mermaid、draw.ioを選び、内容を確認する。マスキングと機密フラグを見てから **指定先へ保存** または **ダウンロード** を押す。
**ホーム > 出力** でMarkdown、JSON、CSV、Mermaid、draw.io、または **LLM handoff (ZIP)** を選び、内容を確認する。マスキングと機密フラグを見てから **指定先へ保存** または **ダウンロード** を押す。hand-off ZIPは、外部LLMがMermaid Markdownを書くために使える集計根拠・JSON Schema・制約を含む手動受け渡し用のローカル成果物であり、OpsMineFlowからLLMへ接続や送信はしない。詳細は[サンプル契約](docs/samples/LLM_MERMAID_HANDOFF.md)

### 7. 診断とデータ削除

Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ The project is licensed under Apache-2.0. Direct dependencies must be commercial
- Bottleneck candidate detection
- Repeated pattern and app-switching analysis
- Automation candidate scoring
- Markdown, JSON, CSV, Mermaid, SVG, and draw.io-oriented exports
- Markdown, JSON, CSV, Mermaid, SVG, draw.io-oriented exports, and a manual Mermaid handoff ZIP

## Local Product Scope

Expand Down Expand Up @@ -120,7 +120,7 @@ CSV commonly uses `case_id`, `activity`, `timestamp_start`, `timestamp_end`, `us

### 6. Export Results

Open **Home > Exports**, choose Markdown, JSON, CSV, Mermaid, or draw.io, and preview it. Review masking and confidential flags before choosing **Save to Path** or **Download**.
Open **Home > Exports**, choose Markdown, JSON, CSV, Mermaid, draw.io, or **LLM handoff (ZIP)**, and preview it. Review masking and confidential flags before choosing **Save to Path** or **Download**. The handoff ZIP is a local, manual transfer artifact: it contains aggregate process evidence and a versioned schema so an external LLM can write Mermaid Markdown, but OpsMineFlow never connects to an LLM or sends the file. See [the handoff bundle example](docs/samples/LLM_MERMAID_HANDOFF.md).

### 7. Check or Remove Local Data

Expand Down
1 change: 1 addition & 0 deletions apps/desktop/src-tauri/src/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1136,6 +1136,7 @@ fn export_file_details(format: &str) -> Result<(&'static str, &'static str), Str
"csv" => Ok(("csv", "opsmineflow-events.csv")),
"mermaid" => Ok(("mmd", "opsmineflow-flow.mmd")),
"drawio" => Ok(("drawio", "opsmineflow-flow.drawio")),
"llm-handoff" => Ok(("zip", "opsmineflow-mermaid-handoff.zip")),
_ => Err("choose a supported export format".to_owned()),
}
}
Expand Down
22 changes: 17 additions & 5 deletions apps/desktop/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -579,10 +579,15 @@ function downloadExport(
csv?: string;
mermaid?: string;
drawio?: string;
filename?: string;
zip_base64?: string;
};
const filename = `opsmineflow-export.${format === "markdown" ? "md" : format === "drawio" ? "drawio" : format}`;
const filename = format === "llm-handoff"
? typed.filename || "opsmineflow-mermaid-handoff.zip"
: `opsmineflow-export.${format === "markdown" ? "md" : format === "drawio" ? "drawio" : format}`;
let content = "";
let mime = "text/plain;charset=utf-8";
let binaryContent: ArrayBuffer | null = null;

if (format === "markdown") {
content = typed.markdown || "";
Expand All @@ -595,12 +600,18 @@ function downloadExport(
mime = "text/csv;charset=utf-8";
} else if (format === "mermaid") {
content = typed.mermaid || "";
} else if (format === "llm-handoff") {
const binary = atob(typed.zip_base64 || "");
const bytes = Uint8Array.from(binary, (character) => character.charCodeAt(0));
binaryContent = new ArrayBuffer(bytes.byteLength);
new Uint8Array(binaryContent).set(bytes);
mime = "application/zip";
} else {
content = typed.drawio || "";
mime = "application/xml;charset=utf-8";
}

const url = URL.createObjectURL(new Blob([content], { type: mime }));
const url = URL.createObjectURL(new Blob([binaryContent ?? content], { type: mime }));
const link = document.createElement("a");
link.href = url;
link.download = filename;
Expand Down Expand Up @@ -933,9 +944,9 @@ function HomeView({
}}
disabled={working}
>
{(["markdown", "json", "csv", "mermaid", "drawio"] as const).map((formatName) => (
{(["markdown", "json", "csv", "mermaid", "drawio", "llm-handoff"] as const).map((formatName) => (
<option key={formatName} value={formatName}>
{formatName}
{formatName === "llm-handoff" ? "LLM handoff (ZIP)" : formatName}
</option>
))}
</select>
Expand Down Expand Up @@ -2266,7 +2277,8 @@ function defaultExportPath(format: ExportFormat): string {
json: "json",
csv: "csv",
mermaid: "mmd",
drawio: "drawio"
drawio: "drawio",
"llm-handoff": "zip"
};
return `exports/opsmineflow-export.${extensionByFormat[format]}`;
}
Expand Down
2 changes: 2 additions & 0 deletions apps/desktop/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ const DEVELOPMENT_ROUTES: Record<string, { method: "GET" | "POST"; path: string
export_drawio: { method: "POST", path: "/export/drawio" },
export_csv: { method: "POST", path: "/export/csv" },
export_json: { method: "POST", path: "/export/json" },
export_llm_handoff: { method: "POST", path: "/export/llm-handoff" },
export_preview: { method: "POST", path: "/export/preview" },
export_save: { method: "POST", path: "/export/save" }
};
Expand Down Expand Up @@ -299,6 +300,7 @@ export async function exportArtifact(format: ExportFormat) {
if (format === "json") return postJson<{ json: string }>("export_json");
if (format === "csv") return postJson<{ csv: string }>("export_csv");
if (format === "mermaid") return postJson<{ mermaid: string }>("export_mermaid");
if (format === "llm-handoff") return postJson<{ filename: string; zip_base64: string }>("export_llm_handoff");
return postJson<{ drawio: string }>("export_drawio");
}

Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,7 @@ export type ImportHistoryEntry = {
imported_at: string;
};

export type ExportFormat = "markdown" | "json" | "csv" | "mermaid" | "drawio";
export type ExportFormat = "markdown" | "json" | "csv" | "mermaid" | "drawio" | "llm-handoff";

export type ExportPreview = {
format: ExportFormat;
Expand Down
6 changes: 5 additions & 1 deletion docs/operations/RUNBOOK.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,17 @@ Automation review states are stored in the local SQLite database and included in
### 7. Export

1. Open **Home > Exports**.
2. Choose Markdown, JSON, CSV, Mermaid, or draw.io.
2. Choose Markdown, JSON, CSV, Mermaid, draw.io, or **LLM handoff (ZIP)**.
3. Choose **Preview**.
4. Review masked fields, confidential flags, and the privacy warning.
5. Choose **Save** and select the destination in Finder.

Treat export preview as the final manual checkpoint before sharing output with a client.

### Manual Mermaid handoff

`LLM handoff (ZIP)` creates a versioned, deterministic local ZIP for manual sharing with an external LLM. It contains aggregate process evidence, public JSON Schemas, and fixed Mermaid-writing constraints; it does not make any LLM, cloud, or network call. The export omits raw event rows, IDs, URLs, titles, aliases, metadata, and review notes. Activity labels and app names remain event-derived data and must be reviewed before sharing. See [the sample contract](../samples/LLM_MERMAID_HANDOFF.md).

### 8. Delete Local Analysis Data

1. Open **Settings**.
Expand Down
3 changes: 3 additions & 0 deletions docs/product/NON_GOALS.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@

LLM integration is not part of the local product and is not part of the near-term roadmap.

A user-controlled local export that they manually share elsewhere is not an
integration. It must not connect to, upload to, or accept results from an LLM.

OpsMineFlow will not implement:

- External LLM API integrations.
Expand Down
76 changes: 76 additions & 0 deletions docs/samples/LLM_MERMAID_HANDOFF.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# Manual Mermaid Handoff Bundle

OpsMineFlow can create `opsmineflow-mermaid-handoff.zip` from **Home >
Exports > LLM handoff (ZIP)**. This is a local export only. OpsMineFlow does
not connect to an LLM, does not send the ZIP anywhere, and does not import an
LLM response.

Before sharing outside the organization, use the preview and make the final
privacy decision yourself. The bundle is designed to include aggregate process
evidence, not raw event rows.

## Bundle contract

| File | Purpose |
|---|---|
| `manifest.json` | Version, producer, data fingerprint, timezone, export profile, and file hashes. |
| `process.json` | Stable node/edge IDs plus observed frequencies, durations, variants, app handoffs, review states, quality, and confidence evidence. |
| `workflow-context.md` | Fixed rules for turning only observed evidence into Mermaid Markdown. |
| `schema/*.json` | Public JSON Schemas for the two JSON files. |

The ZIP excludes raw event rows and raw case, event, session, user, device,
import-path, URL, title, alias, memo, metadata, and automation-review-note
values. Event-derived activity labels and application names remain data values
because they are necessary to describe an observed flow. `workflow-context.md`
requires a receiving LLM to treat those strings as data, never as instructions.

## Example: minimal observed data

`process.json` contains evidence like this (IDs are stable and labels are data):

```json
{
"coverage": {
"events_observed": 12,
"cases_observed": 4,
"activities_observed": 3,
"edges_observed": 2,
"variants_observed": 1,
"excluded_event_count": 0,
"exclusion_note": "This bundle observes the current local event store only."
},
"nodes": [
{"id": "activity-2103f7a4e63499ee", "activity": "受付", "frequency": 4},
{"id": "activity-0f42577b1a454307", "activity": "確認", "frequency": 4},
{"id": "activity-56a13db291f1e7c4", "activity": "完了", "frequency": 4}
],
"edges": [
{"source_node_id": "activity-2103f7a4e63499ee", "target_node_id": "activity-0f42577b1a454307", "frequency": 4},
{"source_node_id": "activity-0f42577b1a454307", "target_node_id": "activity-56a13db291f1e7c4", "frequency": 4}
]
}
```

## Example: acceptable Mermaid Markdown response

The fixed context asks for facts and limits to stay separate. An arbitrary LLM
that follows it has enough information to produce a reviewable diagram like:

````markdown
## Observed business flow

```mermaid
flowchart LR
activity-2103f7a4e63499ee["受付"] -->|4 observed transitions| activity-0f42577b1a454307["確認"]
activity-0f42577b1a454307 -->|4 observed transitions| activity-56a13db291f1e7c4["完了"]
```

## Evidence and limits

- The diagram represents 12 observed events across 4 cases.
- No exclusion was recorded in this bundle; work outside the local event store is not represented.
- The data does not identify an owner, approval rule, or cause for any transition.
````

The response must not add an approval step, role, exception, or causal claim
unless that fact appears in the bundle.
99 changes: 72 additions & 27 deletions services/local-api/src/opsmineflow_api/app.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
from __future__ import annotations

import base64
import csv
import hmac
import hashlib
import hmac
import json
import os
import platform
Expand Down Expand Up @@ -60,6 +61,7 @@

from .activitywatch import import_activitywatch_local
from .child_process import sanitized_subprocess_environment
from .llm_handoff import build_handoff_bundle
from .recording import recording_manager
from .storage import EventStore, default_store

Expand Down Expand Up @@ -923,34 +925,66 @@ def _app_usage_seconds(events: list[StandardEvent]) -> dict[str, float]:

def create_export_artifact(format_name: str, store: EventStore | None = None) -> dict[str, Any]:
active_store = store or default_store()
snapshot = create_api_snapshot(active_store)
if format_name == "markdown":
content = str(snapshot["markdown_report"])
extension = "md"
elif format_name == "json":
content = json_dumps({"snapshot": snapshot})
extension = "json"
elif format_name == "csv":
content = events_to_csv(snapshot["events"])
extension = "csv"
elif format_name == "mermaid":
content = str(snapshot["mermaid"])
extension = "mmd"
elif format_name == "drawio":
content = str(snapshot["drawio"])
extension = "drawio"
if format_name == "llm-handoff":
bundle = build_handoff_bundle(
active_store.events,
active_store.automation_reviews,
active_store.automation_review_notes,
)
content: str | bytes = bundle.content
extension = "zip"
filename = "opsmineflow-mermaid-handoff.zip"
preview = bundle.preview
warning = (
"This ZIP is a manual Mermaid handoff only. It contains aggregate evidence, "
"not raw event rows; review it before sharing outside your organization."
)
else:
raise ValueError("Export format must be markdown, json, csv, mermaid, or drawio.")

snapshot = create_api_snapshot(active_store)
if format_name == "markdown":
content = str(snapshot["markdown_report"])
extension = "md"
elif format_name == "json":
content = json_dumps({"snapshot": snapshot})
extension = "json"
elif format_name == "csv":
content = events_to_csv(snapshot["events"])
extension = "csv"
elif format_name == "mermaid":
content = str(snapshot["mermaid"])
extension = "mmd"
elif format_name == "drawio":
content = str(snapshot["drawio"])
extension = "drawio"
else:
raise ValueError("Export format must be markdown, json, csv, mermaid, drawio, or llm-handoff.")
filename = f"opsmineflow-export.{extension}"
preview = str(content)[:2000]
warning = "Review masked fields and confidential flags before sharing this export."

byte_content = content if isinstance(content, bytes) else content.encode("utf-8")
return {
"format": format_name,
"extension": extension,
"filename": f"opsmineflow-export.{extension}",
"filename": filename,
"content": content,
"byte_size": len(content.encode("utf-8")),
"preview": content[:2000],
"byte_size": len(byte_content),
"preview": preview,
"confidential_count": sum(1 for event in active_store.events if event.confidential_flag),
"warning": "Review masked fields and confidential flags before sharing this export.",
"warning": warning,
}


def export_llm_handoff_payload(store: EventStore | None = None) -> dict[str, str]:
"""Encode the locally generated ZIP for the development-only browser download path."""

artifact = create_export_artifact("llm-handoff", store=store)
content = artifact["content"]
if not isinstance(content, bytes):
raise ValueError("LLM handoff export must be a ZIP file.")
return {
"filename": str(artifact["filename"]),
"zip_base64": base64.b64encode(content).decode("ascii"),
}


Expand Down Expand Up @@ -986,10 +1020,17 @@ def save_export_artifact(
raise ValueError("Confirm replacement in the save dialog before overwriting an existing file.")
file_descriptor, temporary_path = tempfile.mkstemp(prefix=".opsmineflow-export-", dir=path.parent)
try:
with os.fdopen(file_descriptor, "w", encoding="utf-8") as export_file:
export_file.write(str(artifact["content"]))
export_file.flush()
os.fsync(export_file.fileno())
content = artifact["content"]
if isinstance(content, bytes):
with os.fdopen(file_descriptor, "wb") as export_file:
export_file.write(content)
export_file.flush()
os.fsync(export_file.fileno())
else:
with os.fdopen(file_descriptor, "w", encoding="utf-8") as export_file:
export_file.write(content)
export_file.flush()
os.fsync(export_file.fileno())
os.replace(temporary_path, path)
_fsync_directory(path.parent)
except Exception:
Expand Down Expand Up @@ -1533,6 +1574,10 @@ def export_csv_endpoint() -> dict[str, Any]:
def export_json_endpoint() -> dict[str, Any]:
return {"json": str(create_export_artifact("json")["content"])}

@app.post("/export/llm-handoff")
def export_llm_handoff_endpoint() -> dict[str, str]:
return export_llm_handoff_payload()

@app.post("/export/preview")
def export_preview_endpoint(request: ExportPreviewRequest) -> dict[str, Any]:
try:
Expand Down
1 change: 1 addition & 0 deletions services/local-api/src/opsmineflow_api/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
("POST", "/export/svg"),
("POST", "/export/csv"),
("POST", "/export/json"),
("POST", "/export/llm-handoff"),
("POST", "/export/preview"),
("POST", "/export/save"),
}
Expand Down
Loading