WP10 Überarbeitung

This commit is contained in:
Lars 2025-12-11 12:04:08 +01:00
parent 06f77fe8b7
commit f4299db347
2 changed files with 127 additions and 100 deletions

View File

@ -23,7 +23,7 @@ timeout_setting = os.getenv("MINDNET_API_TIMEOUT") or os.getenv("MINDNET_LLM_TIM
API_TIMEOUT = float(timeout_setting) if timeout_setting else 300.0
# --- PAGE SETUP ---
st.set_page_config(page_title="mindnet v2.3.2", page_icon="🧠", layout="wide")
st.set_page_config(page_title="mindnet v2.3.4", page_icon="🧠", layout="wide")
# --- CSS STYLING ---
st.markdown("""
@ -194,7 +194,7 @@ def analyze_draft_text(text: str, n_type: str):
response = requests.post(
INGEST_ANALYZE_ENDPOINT,
json={"text": text, "type": n_type},
timeout=15 # Erhöhtes Timeout für Suche
timeout=15
)
response.raise_for_status()
return response.json()
@ -225,7 +225,7 @@ def submit_feedback(query_id, node_id, score, comment=None):
def render_sidebar():
with st.sidebar:
st.title("🧠 mindnet")
st.caption("v2.3.3 | WP-10b (Intelligence)")
st.caption("v2.3.4 | WP-10b (Intelligence)")
mode = st.radio("Modus", ["💬 Chat", "📝 Manueller Editor"], index=0)
st.divider()
st.subheader("⚙️ Settings")
@ -243,7 +243,7 @@ def render_draft_editor(msg):
qid = msg.get('query_id', str(uuid.uuid4()))
key_base = f"draft_{qid}"
# 1. Init
# 1. Init (Nur beim allerersten Laden)
if f"{key_base}_init" not in st.session_state:
meta, body = parse_markdown_draft(msg["content"])
@ -263,19 +263,89 @@ def render_draft_editor(msg):
# Metadata
c1, c2 = st.columns([2, 1])
with c1:
new_title = st.text_input("Titel", value=st.session_state.get(f"{key_base}_title", ""), key=f"{key_base}_inp_title")
# Titel immer aus State lesen/schreiben
new_title = st.text_input("Titel", key=f"{key_base}_inp_title", value=st.session_state.get(f"{key_base}_title", ""))
with c2:
known_types = ["concept", "project", "decision", "experience", "journal", "person", "value", "goal", "principle", "default"]
curr_type = st.session_state.get(f"{key_base}_type", "default")
if curr_type not in known_types: known_types.append(curr_type)
new_type = st.selectbox("Typ", known_types, index=known_types.index(curr_type), key=f"{key_base}_sel_type")
new_tags = st.text_input("Tags (kommagetrennt)", value=st.session_state.get(f"{key_base}_tags", ""), key=f"{key_base}_inp_tags")
new_tags = st.text_input("Tags (kommagetrennt)", key=f"{key_base}_inp_tags", value=st.session_state.get(f"{key_base}_tags", ""))
# Tabs (Jetzt mit "Intelligence")
# Tabs
tab_edit, tab_intel, tab_view = st.tabs(["✏️ Inhalt", "🧠 Intelligence", "👁️ Vorschau"])
# Live Reassembly für alle Tabs
# --- TAB 1: EDITOR ---
with tab_edit:
# WICHTIG: Das Text-Area ist an session_state gebunden via 'key'.
current_body = st.text_area(
"Body",
key=f"{key_base}_txt_body", # Master-Key
value=st.session_state.get(f"{key_base}_body", ""),
height=500,
label_visibility="collapsed"
)
# Sync zurück zum generischen Key für andere Tabs
st.session_state[f"{key_base}_body"] = current_body
# --- TAB 2: INTELLIGENCE ---
with tab_intel:
st.info("Klicke auf 'Analysieren', um Verknüpfungen für den AKTUELLEN Text zu finden.")
if st.button("🔍 Analyse starten", key=f"{key_base}_analyze"):
with st.spinner("Analysiere..."):
# Wir nehmen explizit den Text aus dem Widget-State
text_to_analyze = st.session_state[f"{key_base}_txt_body"]
analysis = analyze_draft_text(text_to_analyze, new_type)
if "error" in analysis:
st.error(f"Fehler: {analysis['error']}")
else:
suggestions = analysis.get("suggestions", [])
st.session_state[f"{key_base}_suggestions"] = suggestions
if not suggestions:
st.warning("Keine Vorschläge gefunden.")
suggestions = st.session_state.get(f"{key_base}_suggestions", [])
if suggestions:
st.write(f"**{len(suggestions)} Vorschläge:**")
for idx, sugg in enumerate(suggestions):
link_text = sugg.get('suggested_markdown', '')
# Check: Ist der Link schon im Text?
is_inserted = link_text in st.session_state[f"{key_base}_txt_body"]
# Card Styling
card_style = "border-left: 3px solid #28a745;" if is_inserted else "border-left: 3px solid #1a73e8;"
bg_color = "#e6fffa" if is_inserted else "#ffffff"
st.markdown(f"""
<div style="{card_style} background-color: {bg_color}; padding: 10px; margin-bottom: 8px; border-radius: 4px; box-shadow: 0 1px 3px rgba(0,0,0,0.1);">
<b>{sugg.get('target_title', 'Unbekannt')}</b> <small>({sugg.get('type', 'semantic')})</small><br>
<i>{sugg.get('reason', 'N/A')}</i><br>
<code>{link_text}</code>
</div>
""", unsafe_allow_html=True)
# Button Logik (Toggle)
if is_inserted:
if st.button(f"❌ Entfernen", key=f"del_{idx}_{key_base}"):
new_text = st.session_state[f"{key_base}_txt_body"].replace(link_text, "").strip()
st.session_state[f"{key_base}_txt_body"] = new_text
st.session_state[f"{key_base}_body"] = new_text
st.rerun()
else:
if st.button(f" Einfügen", key=f"add_{idx}_{key_base}"):
old_text = st.session_state[f"{key_base}_txt_body"]
new_text = f"{old_text}\n\n{link_text}"
st.session_state[f"{key_base}_txt_body"] = new_text
st.session_state[f"{key_base}_body"] = new_text
st.rerun()
# --- TAB 3: PREVIEW & SAVE ---
# Reassemble Metadata & Body (Always use latest state)
final_tags_list = [t.strip() for t in new_tags.split(",") if t.strip()]
final_meta = {
"id": "generated_on_save",
@ -285,57 +355,10 @@ def render_draft_editor(msg):
"tags": final_tags_list
}
# --- TAB 1: EDITOR ---
with tab_edit:
new_body = st.text_area(
"Body",
value=st.session_state.get(f"{key_base}_body", ""),
height=500,
key=f"{key_base}_txt_body",
label_visibility="collapsed"
)
# Wir nehmen den aktuellsten Body aus dem State
final_body_content = st.session_state.get(f"{key_base}_txt_body", "")
final_doc = build_markdown_doc(final_meta, final_body_content)
# --- TAB 2: INTELLIGENCE (WP-11 Features) ---
with tab_intel:
st.info("Klicke auf 'Analysieren', um Verknüpfungen zu finden.")
if st.button("🔍 Analyse starten", key=f"{key_base}_analyze"):
with st.spinner("Analysiere Text und suche Verknüpfungen..."):
current_text = st.session_state[f"{key_base}_body"]
# API Call
analysis = analyze_draft_text(current_text, new_type)
if "error" in analysis:
st.error(f"Fehler: {analysis['error']}")
else:
suggestions = analysis.get("suggestions", [])
st.session_state[f"{key_base}_suggestions"] = suggestions
if not suggestions:
st.warning("Keine offensichtlichen Verknüpfungen gefunden.")
# Anzeige der Vorschläge
suggestions = st.session_state.get(f"{key_base}_suggestions", [])
if suggestions:
st.markdown(f"**{len(suggestions)} Vorschläge gefunden:**")
for idx, sugg in enumerate(suggestions):
with st.container():
st.markdown(f"""
<div class="suggestion-card">
<b>{sugg.get('target_title', 'Unbekannt')}</b> <small>({sugg.get('type', 'semantic')})</small><br>
<i>Grund: {sugg.get('reason', 'N/A')}</i><br>
<code>{sugg.get('suggested_markdown', '')}</code>
</div>
""", unsafe_allow_html=True)
if st.button(" Einfügen", key=f"{key_base}_add_{idx}"):
current_body = st.session_state[f"{key_base}_body"]
updated_body = f"{current_body}\n\n{sugg['suggested_markdown']}"
st.session_state[f"{key_base}_body"] = updated_body
st.toast(f"Link zu '{sugg.get('target_title', '?')}' eingefügt!")
st.rerun()
# --- TAB 3: PREVIEW ---
final_doc = build_markdown_doc(final_meta, st.session_state.get(f"{key_base}_body", ""))
with tab_view:
st.markdown('<div class="preview-box">', unsafe_allow_html=True)
st.markdown(final_doc)
@ -343,24 +366,19 @@ def render_draft_editor(msg):
st.markdown("---")
# Actions (SAVE & EXPORT)
# Save Action
b1, b2 = st.columns([1, 1])
with b1:
# Echter Save Button (Ruft API auf)
if st.button("💾 Speichern & Indizieren", type="primary", key=f"{key_base}_save"):
with st.spinner("Speichere im Vault..."):
# Generiere Filename
safe_title = re.sub(r'[^a-zA-Z0-9]', '-', new_title).lower()[:30]
fname = f"{datetime.now().strftime('%Y%m%d')}-{safe_title}.md"
# Wir holen den aktuellsten Stand aus dem State (inklusive eingefügter Links)
latest_body = st.session_state.get(f"{key_base}_body", "")
latest_doc = build_markdown_doc(final_meta, latest_body)
result = save_draft_to_vault(latest_doc, filename=fname)
# Hier der entscheidende Call mit dem aktuellen Dokument
result = save_draft_to_vault(final_doc, filename=fname)
if "error" in result:
st.error(f"Fehler beim Speichern: {result['error']}")
st.error(f"Fehler: {result['error']}")
else:
st.success(f"Gespeichert: {result.get('file_path')}")
st.balloons()
@ -447,6 +465,7 @@ def render_manual_editor():
else:
st.success(f"Gespeichert: {res.get('file_path')}")
# --- MAIN ---
mode, top_k, explain = render_sidebar()
if mode == "💬 Chat":
render_chat_interface(top_k, explain)

View File

@ -1,21 +1,27 @@
"""
app/routers/ingest.py - DEBUG VERSION
app/routers/ingest.py
API-Endpunkte für WP-11 (Discovery & Persistence).
"""
import os
import time
import logging
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
from typing import Optional, List, Dict, Any
from typing import Optional, Dict, Any
from app.core.ingestion import IngestionService
# Fallback: Falls DiscoveryService noch fehlt, nutzen wir Ingest Service Features oder Mock
# Wir gehen hier davon aus, dass wir alles im IngestionService oder Router machen können,
# um Importfehler zu vermeiden.
from app.core.retriever import Retriever
from app.models.dto import QueryRequest
logger = logging.getLogger(__name__)
router = APIRouter()
# --- DTOs ---
class AnalyzeRequest(BaseModel):
text: str
type: str = "concept"
@ -37,43 +43,36 @@ class SaveResponse(BaseModel):
async def analyze_draft(req: AnalyzeRequest):
"""
WP-11 Intelligence: Liefert Link-Vorschläge.
DEBUG MODE: Threshold gesenkt, Logging erhöht.
Implementiert direkt hier, um Abhängigkeiten zu reduzieren.
"""
try:
retriever = Retriever()
suggestions = []
query_text = req.text[:400]
logger.info(f"ANALYZING TEXT: '{query_text}' (Type: {req.type})")
if not query_text.strip():
return {"suggestions": []}
# Wir suchen
hits_result = await retriever.search(QueryRequest(query=query_text, top_k=5, mode="hybrid"))
logger.info(f"RETRIEVER FOUND: {len(hits_result.results)} raw hits")
# 1. Semantic Search
# Safe async call check
if hasattr(retriever.search, '__await__'):
hits_result = await retriever.search(QueryRequest(query=query_text, top_k=5, mode="hybrid"))
else:
hits_result = await retriever.search(QueryRequest(query=query_text, top_k=5, mode="hybrid"))
seen_titles = set()
for hit in hits_result.results:
# Titel holen
title = hit.payload.get("title") or hit.payload.get("note_id") or hit.node_id
# Logging für jeden Treffer
logger.info(f" -> CHECK HIT: {title} | Score: {hit.total_score:.4f}")
if not title or title in seen_titles:
continue
# Titel ermitteln
title = hit.payload.get("note_id") or hit.node_id
if not title or title in seen_titles: continue
seen_titles.add(title)
# Edge Logic
edge_kind = "related_to"
if req.type == "project": edge_kind = "depends_on"
if req.type == "decision": edge_kind = "references"
# --- ÄNDERUNG: THRESHOLD GESENKT ---
# War vorher 0.65. Jetzt 0.3 für Tests.
if hit.total_score > 0.3:
# Score Threshold
if hit.total_score > 0.4: # Etwas toleranter
suggestions.append({
"target_title": title,
"target_id": hit.node_id,
@ -81,19 +80,19 @@ async def analyze_draft(req: AnalyzeRequest):
"reason": f"Semantisch ähnlich ({hit.total_score:.2f})",
"type": "semantic"
})
else:
logger.info(f" -> SKIPPED (Score too low)")
logger.info(f"RETURNING {len(suggestions)} SUGGESTIONS")
return {"suggestions": suggestions}
except Exception as e:
logger.error(f"Analyze failed: {e}", exc_info=True)
raise HTTPException(status_code=500, detail=f"Analysis failed: {str(e)}")
# Kein 500er werfen, lieber leere Liste, damit UI nicht crasht
return {"suggestions": [], "error": str(e)}
@router.post("/save", response_model=SaveResponse)
async def save_note(req: SaveRequest):
"""WP-11 Persistence"""
"""
WP-11 Persistence: Speichert Markdown physisch und indiziert es sofort.
"""
try:
vault_root = os.getenv("MINDNET_VAULT_ROOT", "./vault")
abs_vault_root = os.path.abspath(vault_root)
@ -106,19 +105,28 @@ async def save_note(req: SaveRequest):
final_filename = f"draft_{int(time.time())}.md"
ingest_service = IngestionService()
logger.info(f"Saving {final_filename}")
result = await ingest_service.save_and_index(
markdown_content=req.markdown_content,
filename=final_filename
)
logger.info(f"Saving {final_filename} to {req.folder}")
# --- AWAIT WICHTIG! ---
# Wir rufen save_and_index auf (so hieß es in meiner IngestionService Implementierung)
# Wenn deine Methode create_from_text heißt, ändere es hier entsprechend.
# Ich nutze hier save_and_index als Standard aus WP-11.
if hasattr(ingest_service, 'save_and_index'):
result = await ingest_service.save_and_index(req.markdown_content, final_filename)
elif hasattr(ingest_service, 'create_from_text'):
# Fallback falls du die alte Version hast
result = await ingest_service.create_from_text(req.markdown_content, final_filename, abs_vault_root, req.folder)
else:
raise RuntimeError("IngestionService hat weder save_and_index noch create_from_text")
if result.get("status") == "error":
raise HTTPException(status_code=500, detail=result.get("error"))
return SaveResponse(
status="success",
file_path=result.get("file_path", "unknown"),
file_path=result.get("file_path") or result.get("path", "unknown"),
note_id=result.get("note_id", "unknown"),
stats=result.get("stats", {})
)