Capture ThinkStorm project: codebase state, workflows, and access control policies
This commit is contained in:
@@ -0,0 +1,243 @@
|
||||
"""
|
||||
OpenGist Service Adapter
|
||||
Syncs canonical idea dossiers, research synthesis, provenance logs, and work-track output artifacts to OpenGist.
|
||||
"""
|
||||
|
||||
import json
|
||||
import time
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any, List, Optional
|
||||
from .base import BaseServiceAdapter, ServiceHealth
|
||||
from ..config import BASE_DIR, config
|
||||
|
||||
ARTIFACTS_DIR = BASE_DIR / "data" / "artifacts"
|
||||
ARTIFACTS_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
class OpenGistAdapter(BaseServiceAdapter):
|
||||
def __init__(self, endpoint: str = "https://gist.labyricorn.com", api_token: str = ""):
|
||||
super().__init__(service_id="opengist", endpoint=endpoint, api_key=api_token or config.services.opengist_api_token)
|
||||
self.internal_endpoint = getattr(config.services, "opengist_internal_url", "http://10.138.2.48:6157") or "http://10.138.2.48:6157"
|
||||
|
||||
async def check_health(self) -> ServiceHealth:
|
||||
start = time.time()
|
||||
for test_url in [f"{self.internal_endpoint}/api/gists/public", f"{self.endpoint}/"]:
|
||||
try:
|
||||
req = urllib.request.Request(test_url, headers={"User-Agent": "ThinkStorm-Orchestrator/0.1"})
|
||||
loop = asyncio.get_running_loop()
|
||||
def fetch():
|
||||
with urllib.request.urlopen(req, timeout=5.0) as resp:
|
||||
return resp.status
|
||||
status = await loop.run_in_executor(None, fetch)
|
||||
elapsed = int((time.time() - start) * 1000)
|
||||
return ServiceHealth(
|
||||
service_id=self.service_id,
|
||||
healthy=True,
|
||||
endpoint=self.endpoint,
|
||||
message=f"OpenGist online (HTTP {status})",
|
||||
response_time_ms=elapsed
|
||||
)
|
||||
except Exception as e:
|
||||
continue
|
||||
|
||||
elapsed = int((time.time() - start) * 1000)
|
||||
return ServiceHealth(
|
||||
service_id=self.service_id,
|
||||
healthy=False,
|
||||
endpoint=self.endpoint,
|
||||
message="OpenGist connection error",
|
||||
response_time_ms=elapsed
|
||||
)
|
||||
|
||||
def get_effective_token(self) -> str:
|
||||
"""Retrieves configured API token from instance, DB configuration, or environment."""
|
||||
if self.api_key:
|
||||
return self.api_key
|
||||
try:
|
||||
from ..database import get_db
|
||||
with get_db() as conn:
|
||||
row = conn.execute("SELECT api_key_raw FROM service_configurations WHERE id = 'opengist'").fetchone()
|
||||
if row and row["api_key_raw"]:
|
||||
return row["api_key_raw"]
|
||||
except Exception:
|
||||
pass
|
||||
return config.services.opengist_api_token or ""
|
||||
|
||||
async def persist_work_track_outputs(
|
||||
self,
|
||||
idea_id: str,
|
||||
track_name: str,
|
||||
outputs: Dict[str, str]
|
||||
) -> Dict[str, Any]:
|
||||
"""Persists Work Track deliverables to local dossier and OpenGist."""
|
||||
idea_dir = ARTIFACTS_DIR / idea_id
|
||||
out_subdir = idea_dir / "outputs" / track_name.lower().replace(" ", "-")
|
||||
out_subdir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
for filename, content in outputs.items():
|
||||
file_path = out_subdir / filename
|
||||
file_path.write_text(content, encoding="utf-8")
|
||||
|
||||
token = self.get_effective_token()
|
||||
gist_url = None
|
||||
if token:
|
||||
loop = asyncio.get_running_loop()
|
||||
def sync_track():
|
||||
try:
|
||||
files_payload = {
|
||||
f"{track_name.lower().replace(' ', '-')}_{k}": {"content": v}
|
||||
for k, v in outputs.items()
|
||||
}
|
||||
data = {
|
||||
"description": f"ThinkStorm Deliverables - {idea_id} - {track_name}",
|
||||
"public": True,
|
||||
"visibility": "public",
|
||||
"files": files_payload
|
||||
}
|
||||
req_data = json.dumps(data).encode("utf-8")
|
||||
api_target = f"{self.internal_endpoint}/api/gists"
|
||||
req = urllib.request.Request(
|
||||
api_target,
|
||||
data=req_data,
|
||||
headers={
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {token}",
|
||||
"User-Agent": "ThinkStorm-Orchestrator/0.1"
|
||||
},
|
||||
method="POST"
|
||||
)
|
||||
with urllib.request.urlopen(req, timeout=8.0) as resp:
|
||||
res = json.loads(resp.read().decode("utf-8"))
|
||||
return res.get("html_url") or f"{self.endpoint}/{res.get('id')}"
|
||||
except Exception as e:
|
||||
print(f"[OpenGist] Track sync notice: {e}")
|
||||
return None
|
||||
|
||||
try:
|
||||
gist_url = await loop.run_in_executor(None, sync_track)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return {"status": "SUCCESS", "path": str(out_subdir), "gist_url": gist_url}
|
||||
|
||||
async def persist_idea_artifact(
|
||||
self,
|
||||
idea_id: str,
|
||||
title: str,
|
||||
summary: str,
|
||||
original_text: str,
|
||||
categories: List[str],
|
||||
tags: List[str],
|
||||
lifecycle_state: str,
|
||||
research_docs: Dict[str, str],
|
||||
outputs: Dict[str, str],
|
||||
provenance_runs: List[Dict[str, Any]],
|
||||
existing_gist_id: Optional[str] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Persists structured idea artifacts to local durable store and syncs with OpenGist."""
|
||||
# 1. Prepare local disk artifact tree
|
||||
idea_dir = ARTIFACTS_DIR / idea_id
|
||||
idea_dir.mkdir(parents=True, exist_ok=True)
|
||||
(idea_dir / "research").mkdir(parents=True, exist_ok=True)
|
||||
(idea_dir / "provenance").mkdir(parents=True, exist_ok=True)
|
||||
(idea_dir / "outputs").mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# 2. Write idea.md
|
||||
cat_str = ", ".join(categories) if categories else "General"
|
||||
tag_str = ", ".join(f"#{t}" for t in tags) if tags else "None"
|
||||
idea_md = (
|
||||
f"# {idea_id}: {title}\n\n"
|
||||
f"**Lifecycle State:** `{lifecycle_state}` \n"
|
||||
f"**Categories:** {cat_str} \n"
|
||||
f"**Tags:** {tag_str} \n\n"
|
||||
f"## Summary\n{summary}\n\n"
|
||||
f"## Original Submission\n> {original_text.strip()}\n"
|
||||
)
|
||||
(idea_dir / "idea.md").write_text(idea_md, encoding="utf-8")
|
||||
|
||||
# 3. Write metadata.json
|
||||
meta = {
|
||||
"id": idea_id,
|
||||
"title": title,
|
||||
"summary": summary,
|
||||
"categories": categories,
|
||||
"tags": tags,
|
||||
"lifecycle_state": lifecycle_state,
|
||||
"updated_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
||||
}
|
||||
(idea_dir / "metadata.json").write_text(json.dumps(meta, indent=2), encoding="utf-8")
|
||||
|
||||
# 4. Write research docs
|
||||
for filename, content in research_docs.items():
|
||||
safe_name = filename.replace("/", "_")
|
||||
(idea_dir / "research" / safe_name).write_text(content, encoding="utf-8")
|
||||
|
||||
# 5. Write outputs
|
||||
for filename, content in outputs.items():
|
||||
out_path = idea_dir / "outputs" / filename
|
||||
out_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
out_path.write_text(content, encoding="utf-8")
|
||||
|
||||
# 6. Write provenance runs
|
||||
for run in provenance_runs:
|
||||
run_id = run.get("id", f"run-{int(time.time())}")
|
||||
(idea_dir / "provenance" / f"{run_id}.json").write_text(json.dumps(run, indent=2), encoding="utf-8")
|
||||
|
||||
# 7. Attempt OpenGist API sync if token configured
|
||||
gist_id = existing_gist_id or idea_id.lower()
|
||||
gist_url = f"{self.endpoint}/{gist_id}"
|
||||
|
||||
token = self.get_effective_token()
|
||||
if token:
|
||||
loop = asyncio.get_running_loop()
|
||||
def sync_remote():
|
||||
try:
|
||||
files_payload = {
|
||||
"idea.md": {"content": idea_md},
|
||||
"metadata.json": {"content": json.dumps(meta, indent=2)}
|
||||
}
|
||||
for k, v in research_docs.items():
|
||||
safe_k = k.replace("/", "_")
|
||||
files_payload[safe_k] = {"content": v}
|
||||
|
||||
data = {
|
||||
"description": f"ThinkStorm Dossier - {idea_id}: {title}",
|
||||
"public": True,
|
||||
"visibility": "public",
|
||||
"files": files_payload
|
||||
}
|
||||
req_data = json.dumps(data).encode("utf-8")
|
||||
api_target = f"{self.internal_endpoint}/api/gists"
|
||||
req = urllib.request.Request(
|
||||
api_target,
|
||||
data=req_data,
|
||||
headers={
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {token}",
|
||||
"User-Agent": "ThinkStorm-Orchestrator/0.1"
|
||||
},
|
||||
method="POST"
|
||||
)
|
||||
with urllib.request.urlopen(req, timeout=8.0) as resp:
|
||||
res = json.loads(resp.read().decode("utf-8"))
|
||||
remote_id = res.get("id") or gist_id
|
||||
remote_url = res.get("html_url") or f"{self.endpoint}/{remote_id}"
|
||||
return remote_id, remote_url
|
||||
except Exception as e:
|
||||
print(f"[OpenGist] Remote sync notice: {e}")
|
||||
return gist_id, f"{self.endpoint}/{gist_id}"
|
||||
|
||||
try:
|
||||
remote_id, remote_url = await loop.run_in_executor(None, sync_remote)
|
||||
gist_id = remote_id or gist_id
|
||||
gist_url = remote_url or f"{self.endpoint}/{gist_id}"
|
||||
except Exception as e:
|
||||
print(f"[OpenGist] Async executor exception: {e}")
|
||||
|
||||
return {
|
||||
"opengist_id": gist_id,
|
||||
"opengist_url": gist_url,
|
||||
"local_path": str(idea_dir)
|
||||
}
|
||||
Reference in New Issue
Block a user