From 6a32fa321f88461017636eb6aa41428f3c45dab1 Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Fri, 19 Jun 2026 21:10:45 +0000 Subject: [PATCH] OPTEMA AI --- core/__pycache__/forms.cpython-311.pyc | Bin 2335 -> 3928 bytes core/__pycache__/urls.cpython-311.pyc | Bin 529 -> 635 bytes core/__pycache__/views.cpython-311.pyc | Bin 46264 -> 75899 bytes core/forms.py | 35 ++ core/templates/core/_nav.html | 1 + core/templates/core/case_detail.html | 5 +- core/templates/core/web_intelligence.html | 142 +++++ core/urls.py | 3 +- core/views.py | 614 +++++++++++++++++++++- requirements.txt | 2 + 10 files changed, 778 insertions(+), 24 deletions(-) create mode 100644 core/templates/core/web_intelligence.html diff --git a/core/__pycache__/forms.cpython-311.pyc b/core/__pycache__/forms.cpython-311.pyc index ca0a07bb2164de3be70416d3105ac2c782729591..376d29a1480fb7fbc00c6c3097694885f6d68e8a 100644 GIT binary patch delta 1709 zcmZvc&rcgi6vubhYwsHWSYvQnhX$5k38r9_QgTS5B1!?7wh)CxQMsVSGr+>KYi4&# zv8vjYa-b5I$d%dyN^_#BK$WWG5cPkk1jrJrmD)qqOAnQd>O<8Y+BfUkEkDNEk7nL` z^PPFmGxJs4X^(y#i8L|Le)x7|^{?QQ=&S7NlN;j$95cnBz+(mpm|YFf0NVC^46_ek zt<4KaoMTeK?{ecD`_1zg+%IgP8(gIK`BQ$BWqV3L`9EX3OHcV^>C*sVTT0cyaCaY# zpc>?LK#C&+v>yVP96**yPia~pH>uK0DnOFdeU+k(qL*TjVu+%P!dtYb^qa6z@VWvR zv+y;Vmm!RP{{!#^gPD02G4mY9VBacVk3uZskk7OE8#W&KQDB!l5cXI|9llyS5cgo- zeq-)@z?&x&>H#Q-nmm)(u<$5!sZ{t}RfFfY6+yL)){|fmkD^FIv0WeH4x0C%YWuWu zv=x|x_&K={YI)4ehfyp1RSCD{5njyb7S=R%8S7a*X^{1N{DF~E*JQ;~YUgBm&cjL=gj| zc-OBfi&%3-f;ViHVB|KfD<3UrSYNhQ+`y)ama&zW9^QXXHtqFAOk@Q6P^(btd}zh8 za^~%!p>#T(@{wVxIzn*^kdMyhus)WN$L?k1ckz}@j{=&_nW~{Hn*4L|b6GW2+14!^ zQUeo=OnEHh`i!L|P-|?|4J)=~WR3NlhAj*SvKfTjFu^*)WZ^?avvGa#H@(H_fo8ui zz2wLFkJ)Ej>4fjP^~?vZ3k`8+kB!-C1^)|L^_PbHodwrt6D`H%1)41+n-}Y8=lf_` zRup2JGE$XI$U*)$sP<>o1JiI%ynB@bb*Q+piHX}}S_-kuP1RbtaNodb*d&V=Y*Nq~ z_l`~uW;LwnVAt~3E;%ddhIXiFm}m8R89irPK0a>bF_x9y#RljwHZ?pFN1 z>rEA=hFIv7@x?+_ib_z71Wp-1m7+LNi8G<(cJEHwDMULwnQI{Pi6_$sGQCvB!xLoI#28o~?-)+Bo$g2K+P2G5mnSpoyw+I}_7!}xlwOJ-+?i%&7UVDD%ii7oQ);hX;`G-32MH6PO#lD@ delta 246 zcmca1H(!WvIWI340}wc-n`UigoyaG_=rU2=nJtAam_d_$<1}X`M$OH)n4U1QDFGD~ zsZ8!;liYllMU9bBc`^%IC!-Qjpqo)>vJ|_qvN}iwGl~`DwBhNr0p!L4*{DPyrD-AVLpBD1r#ErOK0qISP5eOm?6kQ0wGuJ}nhw o6+q!%95%W6DWy57c11=&E+Y^Z$4@@O7swmF6YprdF{fCl;p`>ld*B1*-&ss&$c7>lblMd@s$&F`1iD zgQ#p5E2#}yWj8!Q|R jE*ChZW_Yj2y})8~k;Uc;i_Hg4R(_@iZV)Wu2U-CDj-pWQ delta 266 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 diff --git a/core/forms.py b/core/forms.py index 243e755..c489b45 100644 --- a/core/forms.py +++ b/core/forms.py @@ -38,3 +38,38 @@ class ProblemCaseForm(forms.ModelForm): if urgency < 1 or urgency > 5: raise forms.ValidationError("Urgensi harus berada pada skala 1 sampai 5.") return urgency + + +class WebIntelligenceForm(forms.Form): + query = forms.CharField( + label="Topik atau masalah", + required=False, + max_length=180, + widget=forms.TextInput(attrs={ + "class": "form-control", + "placeholder": "Contoh: kerugian BUMN Indonesia", + }), + ) + url = forms.URLField( + label="URL sumber data", + max_length=500, + widget=forms.URLInput(attrs={ + "class": "form-control", + "placeholder": "https://...", + }), + ) + api_key = forms.CharField( + label="OpenAI API Key", + required=False, + widget=forms.PasswordInput(attrs={ + "class": "form-control", + "placeholder": "Opsional — isi untuk analisis AI", + "autocomplete": "off", + }, render_value=False), + ) + + def clean_url(self): + source_url = self.cleaned_data["url"].strip() + if not source_url.lower().startswith(("http://", "https://")): + raise forms.ValidationError("URL harus diawali http:// atau https://.") + return source_url diff --git a/core/templates/core/_nav.html b/core/templates/core/_nav.html index c3b431c..0f0723e 100644 --- a/core/templates/core/_nav.html +++ b/core/templates/core/_nav.html @@ -10,6 +10,7 @@ diff --git a/core/templates/core/case_detail.html b/core/templates/core/case_detail.html index 2da5b3e..1bad7bd 100644 --- a/core/templates/core/case_detail.html +++ b/core/templates/core/case_detail.html @@ -14,7 +14,7 @@ {% endif %}
-

Analisis Tersimpan · {{ problem.get_business_area_display }}

+

Analisis Tersimpan · {{ detected_category|default:problem.get_business_area_display }}

{{ problem.title }}

{{ problem.description }}

@@ -92,6 +92,7 @@
Root Cause Analysis

2. Root Cause (Akar Masalah)

+

Akar masalah di bawah diturunkan dari Problem utama dan data terdeteksi pada bagian 1, sehingga bukan daftar generik yang berdiri sendiri.

{% for cause in problem.root_causes.all %}
@@ -109,7 +110,7 @@
Solution & Decision Scoring

3. Rekomendasi Solusi & Prediksi Sukses

-

Rumus: Decision Score = Impact×0.4 + Efficiency×0.3 + Speed×0.2 + LowRisk×0.1

+

Setiap rekomendasi menargetkan akar masalah pada bagian 2. Rumus: Decision Score = Impact×0.4 + Efficiency×0.3 + Speed×0.2 + LowRisk×0.1

diff --git a/core/templates/core/web_intelligence.html b/core/templates/core/web_intelligence.html new file mode 100644 index 0000000..b9d1a6e --- /dev/null +++ b/core/templates/core/web_intelligence.html @@ -0,0 +1,142 @@ +{% extends "base.html" %} + +{% block title %}{{ page_title }}{% endblock %} + +{% block content %} +{% include "core/_nav.html" %} + +
+
+ + +
+
+
+

OPTEMA AI Web Intelligence

+

⚡ Analisis Masalah Berbasis Data Internet

+

Masukkan topik dan URL sumber publik. OPTEMA akan mengambil konten halaman, menampilkan data yang dibaca, lalu menyusun Problem Detection, Root Cause, Solusi, Risiko, Action Plan, dan KPI jika OpenAI API Key tersedia.

+
+ Scrape URL + Problem Detection + KPI +
+
+
+
+
+ Live URL Analysis + 6 Output +
+
+

Format Analisis

+

Internet Data → Insight → Action

+

Cocok untuk artikel berita, laporan, pengumuman, atau sumber data publik berbasis HTML.

+
+
+
+
+
+
+ +
+
+ {% if messages %} +
+ {% for message in messages %} + + {% endfor %} +
+ {% endif %} + +
+
+
+
+ 🌐 +
+

Input Sumber Data

+

API key tidak disimpan. Jika dikosongkan, halaman tetap menampilkan hasil scraping data internet.

+
+
+
+ {% csrf_token %} + {{ form.non_field_errors }} + +
+ + {{ form.query }} + {% for error in form.query.errors %}
{{ error }}
{% endfor %} +
+ +
+ + {{ form.url }} + {% for error in form.url.errors %}
{{ error }}
{% endfor %} +
+ +
+ + {{ form.api_key }} +
Gunakan API key pribadi Anda hanya untuk sesi analisis ini.
+ {% for error in form.api_key.errors %}
{{ error }}
{% endfor %} +
+ + +

Sumber lokal seperti localhost diblokir untuk keamanan.

+ +
+
+ +
+ {% if source_data %} +
+
✅ Data Internet Berhasil Dibaca
+
+

Data Internet

+
+
Domain{{ source_data.domain }}
+
Paragraf{{ source_data.paragraph_count }}
+
+

Judul

+
{{ source_data.title }}
+ + +

Preview dibatasi 5.000 karakter; analisis AI memakai maksimal 10.000 karakter pertama.

+
+
+ {% else %} +
+

Belum ada data internet.

+

Masukkan URL sumber data publik untuk mulai membaca konten halaman.

+
+ {% endif %} + + {% if ai_result %} +
+
🤖 OPTEMA AI
+
+

Hasil Analisis

+
{{ ai_result|linebreaksbr }}
+
+
+ {% elif analysis_error %} +
+
+

Status Analisis AI

+ +
+
+ {% endif %} +
+
+
+
+
+ +
+
+ OPTEMA AI — Web Intelligence + Problem Detection · Root Cause · Solusi · Risiko · Action Plan · KPI +
+
+{% endblock %} diff --git a/core/urls.py b/core/urls.py index 3e11c39..983adf4 100644 --- a/core/urls.py +++ b/core/urls.py @@ -1,9 +1,10 @@ from django.urls import path -from .views import case_detail, case_list, home +from .views import case_detail, case_list, home, web_intelligence urlpatterns = [ path("", home, name="home"), path("cases/", case_list, name="case_list"), + path("web-intelligence/", web_intelligence, name="web_intelligence"), path("cases//", case_detail, name="case_detail"), ] diff --git a/core/views.py b/core/views.py index 188d8d3..fe399a0 100644 --- a/core/views.py +++ b/core/views.py @@ -1,27 +1,229 @@ +import json +import logging import re from decimal import Decimal +from html.parser import HTMLParser +from urllib.parse import urlparse from django.contrib import messages from django.db import transaction from django.db.models import Prefetch from django.shortcuts import get_object_or_404, redirect, render -from .forms import ProblemCaseForm +import requests + +try: + from bs4 import BeautifulSoup +except ImportError: # pragma: no cover - production can use requirements.txt + BeautifulSoup = None + +from .forms import ProblemCaseForm, WebIntelligenceForm from .models import ActionPlanStep, ProblemCase, RootCause, SolutionOption +logger = logging.getLogger(__name__) + + + +WEB_INTELLIGENCE_OPENAI_URL = "https://api.openai.com/v1/chat/completions" +WEB_INTELLIGENCE_MODEL = "gpt-4o-mini" +WEB_INTELLIGENCE_DISPLAY_CHARS = 5000 +WEB_INTELLIGENCE_PROMPT_CHARS = 10000 + + +class _ParagraphHTMLParser(HTMLParser): + def __init__(self): + super().__init__() + self.title_parts = [] + self.paragraphs = [] + self._tag_stack = [] + self._current = [] + self._capture_title = False + self._capture_paragraph = False + self._skip_depth = 0 + + def handle_starttag(self, tag, attrs): + tag = tag.lower() + if tag in {"script", "style", "noscript", "svg"}: + self._skip_depth += 1 + return + if self._skip_depth: + return + if tag == "title": + self._capture_title = True + elif tag in {"p", "li"}: + self._capture_paragraph = True + self._current = [] + self._tag_stack.append(tag) + + def handle_endtag(self, tag): + tag = tag.lower() + if tag in {"script", "style", "noscript", "svg"} and self._skip_depth: + self._skip_depth -= 1 + return + if tag == "title": + self._capture_title = False + elif self._capture_paragraph and self._tag_stack and tag == self._tag_stack[-1]: + paragraph = " ".join(" ".join(self._current).split()) + if len(paragraph) > 30: + self.paragraphs.append(paragraph) + self._current = [] + self._tag_stack.pop() + self._capture_paragraph = bool(self._tag_stack) + + def handle_data(self, data): + if self._skip_depth: + return + clean = " ".join((data or "").split()) + if not clean: + return + if self._capture_title: + self.title_parts.append(clean) + if self._capture_paragraph: + self._current.append(clean) + + +def _is_blocked_source_url(source_url): + parsed = urlparse(source_url) + hostname = (parsed.hostname or "").lower() + if parsed.scheme not in {"http", "https"} or not hostname: + return True + return hostname in {"localhost", "127.0.0.1", "0.0.0.0", "::1"} or hostname.endswith(".local") + + +def _extract_html_content(html): + if BeautifulSoup is not None: + soup = BeautifulSoup(html, "html.parser") + for tag in soup(["script", "style", "noscript", "svg", "form", "nav", "footer"]): + tag.decompose() + title = soup.title.get_text(" ", strip=True) if soup.title else "" + paragraphs = [ + node.get_text(" ", strip=True) + for node in soup.find_all(["p", "li"]) + ] + else: + parser = _ParagraphHTMLParser() + parser.feed(html) + title = " ".join(parser.title_parts).strip() + paragraphs = parser.paragraphs + + cleaned = [] + seen = set() + for paragraph in paragraphs: + paragraph = " ".join((paragraph or "").split()) + if len(paragraph) < 30: + continue + key = paragraph[:120].lower() + if key in seen: + continue + seen.add(key) + cleaned.append(paragraph) + if len(cleaned) >= 50: + break + return title, "\n".join(cleaned) + + +def _fetch_web_source(source_url): + if _is_blocked_source_url(source_url): + raise ValueError("URL sumber tidak valid atau mengarah ke alamat lokal.") + + response = requests.get( + source_url, + headers={"User-Agent": "Mozilla/5.0 (compatible; OPTEMA-AI/1.0)"}, + timeout=20, + ) + response.raise_for_status() + + title, content = _extract_html_content(response.text) + if not content: + raise ValueError("Konten teks tidak ditemukan pada URL tersebut. Coba sumber artikel HTML lain.") + + parsed = urlparse(source_url) + return { + "title": title or parsed.netloc, + "content": content, + "content_preview": content[:WEB_INTELLIGENCE_DISPLAY_CHARS], + "domain": parsed.netloc, + "url": source_url, + "paragraph_count": content.count("\n") + 1, + } + + +def _build_web_intelligence_prompt(query, content): + topic = query.strip() or "Masalah dari sumber data internet" + return f""" +Topik: {topic} + +Data internet: +{content[:WEB_INTELLIGENCE_PROMPT_CHARS]} + +Analisis dengan format berikut: +1. Problem Detection +2. Root Cause +3. Solusi +4. Risiko +5. Action Plan +6. KPI + +Instruksi: +- Gunakan bahasa Indonesia yang jelas dan ringkas. +- Dasarkan analisis pada data yang tersedia di sumber. +- Jangan mengarang angka baru; jika data kurang, tulis asumsi/kebutuhan data tambahan. +- Buat solusi yang nyambung langsung dengan root cause. +""".strip() + + +def _call_openai_web_intelligence(api_key, query, content): + prompt = _build_web_intelligence_prompt(query, content) + payload = { + "model": WEB_INTELLIGENCE_MODEL, + "messages": [ + {"role": "system", "content": "Anda adalah OPTEMA AI, analis problem solving berbasis data internet."}, + {"role": "user", "content": prompt}, + ], + "temperature": 0.2, + } + response = requests.post( + WEB_INTELLIGENCE_OPENAI_URL, + headers={ + "Authorization": f"Bearer {api_key}", + "Content-Type": "application/json", + }, + data=json.dumps(payload), + timeout=45, + ) + if response.status_code == 401: + raise ValueError("OpenAI API Key tidak valid. Periksa kembali key yang dimasukkan.") + response.raise_for_status() + data = response.json() + return data["choices"][0]["message"]["content"].strip() + + def _clamp(value, minimum=1, maximum=100): return max(minimum, min(maximum, int(value))) +def _keyword_in_text(text, keyword): + text = (text or "").lower() + keyword = (keyword or "").lower().strip() + if not keyword: + return False + pattern = r"(?= 10_000 else None +NON_MONEY_QUANTITY_WORDS = ( + "jiwa", "orang", "penduduk", "populasi", "korban", "pengguna", + "penyalahguna", "entitas", "unit", "pelanggan", "remaja", "siswa", + "mahasiswa", "pasien", "kasus", +) + + +def _followed_by_non_money_quantity(text, end_index): + tail = text[end_index:end_index + 40].lower() + return re.match(r"\s*(?:" + "|".join(NON_MONEY_QUANTITY_WORDS) + r")\b", tail) is not None + + def _extract_money_amounts(text): patterns = [ - r"(?:rp\.?|idr)\s*([0-9][0-9.,]*)\s*(miliar|milyar|juta|jt|ribu|rb)?", - r"(?:uang|dana|budget|anggaran|biaya|modal|tabungan|cash|kas|hutang|utang|gaji)\s*(?:hanya|ada|sekitar|kurang lebih|maksimal|max|sebesar|:|=)?\s*(?:rp\.?|idr)?\s*([0-9][0-9.,]*)\s*(miliar|milyar|juta|jt|ribu|rb)?", - r"([0-9][0-9.,]*)\s*(miliar|milyar|juta|jt|ribu|rb)\b", + (r"(?:rp\.?|idr)\s*([0-9][0-9.,]*)\s*(triliun|trilyun|trillion|miliar|milyar|juta|jt|ribu|rb)?", False), + (r"(?:uang|dana|budget|anggaran|biaya|modal|tabungan|cash|kas|hutang|utang|gaji)\s*(?:hanya|ada|sekitar|kurang lebih|maksimal|max|sebesar|:|=)?\s*(?:rp\.?|idr)?\s*([0-9][0-9.,]*)\s*(triliun|trilyun|trillion|miliar|milyar|juta|jt|ribu|rb)?", False), + (r"([0-9][0-9.,]*)\s*(triliun|trilyun|trillion|miliar|milyar|juta|jt|ribu|rb)\b", True), ] amounts = [] - for pattern in patterns: + for pattern, needs_quantity_guard in patterns: for match in re.finditer(pattern, text, flags=re.IGNORECASE): + if needs_quantity_guard and _followed_by_non_money_quantity(text, match.end()): + continue amount = _money_to_idr(match.group(1), match.group(2) if len(match.groups()) > 1 else None) if amount: amounts.append(amount) @@ -173,6 +392,22 @@ def _extract_percentages(text): return sorted(set(percentages)) +RAW_DATA_PATTERN = re.compile( + r"(?:rp\.?|idr)?\s*[0-9]+(?:[,.][0-9]+)?\s*(?:triliun|trilyun|trillion|miliar|milyar|juta|jt|ribu|rb|tahun|thn|bulan|bln|semester|%|persen|percent)?", + re.IGNORECASE, +) + + +def extract_data(text): + raw = " ".join((text or "").lower().split()) + values = [] + for match in RAW_DATA_PATTERN.finditer(raw): + token = " ".join(match.group(0).split()) + if token and any(char.isdigit() for char in token): + values.append(token) + return values + + def _extract_case_data(description): text = description.lower() money_amounts = _extract_money_amounts(text) @@ -182,6 +417,7 @@ def _extract_case_data(description): "duration_years": _extract_duration_years(text), "countries": _extract_countries(text), "percentages": _extract_percentages(text), + "raw_data": extract_data(description), } @@ -196,6 +432,51 @@ def _case_constraint_note(case_data): return ", ".join(parts) if parts else "data yang tertulis di pertanyaan" +def _matching_playbook(description, category): + for playbook in PROBLEM_PLAYBOOKS: + if playbook["kategori"] == category and _has_any(description, playbook["keywords"]): + return playbook + return None + + +def _render_playbook_analysis(playbook, constraint_note): + problem_label = playbook["problem"] + cause_profiles = [ + ( + factor, + score, + why_chain.format(problem=problem_label, constraint=constraint_note), + ) + for factor, score, why_chain in playbook["causes"] + ] + cause_titles = [factor for factor, _, _ in playbook["causes"]] + options = [] + for index, option in enumerate(playbook["solutions"]): + target_cause = option.get("target_cause") or cause_titles[min(index, len(cause_titles) - 1)] + options.append({ + "title": option["title"], + "impact": option["impact"], + "efficiency": option["efficiency"], + "speed": option["speed"], + "low_risk": option["low_risk"], + "success_rate": option["success_rate"], + "rationale": option["rationale"].format( + problem=problem_label, + constraint=constraint_note, + target_cause=target_cause, + ), + }) + steps = [ + ( + day, + title, + task.format(problem=problem_label, constraint=constraint_note), + ) + for day, title, task in playbook["steps"] + ] + return cause_profiles, options, steps + + def _education_cost_rows(case_data, fallback_years=None): years = case_data.get("duration_years") or fallback_years if not years: @@ -221,19 +502,32 @@ def _education_cost_rows(case_data, fallback_years=None): return rows -def _build_case_insights(description, category=None): +def _build_case_insights(description, category=None, top_solution=None): case_data = _extract_case_data(description) budget = case_data.get("budget") years = case_data.get("duration_years") countries = case_data.get("countries") percentages = case_data.get("percentages") or [] + raw_data = case_data.get("raw_data") or [] + detected_sector = category or detect_sector(description) + playbook = _matching_playbook(description, detected_sector) + problem_label = playbook["problem"] if playbook else f"Masalah kategori {detected_sector}" detected = [ - {"label": "Sektor/Kategori", "value": category or "Umum", "note": "hasil deteksi keyword dari knowledge database OPTEMA"}, + {"label": "Sektor/Kategori", "value": detected_sector, "note": "hasil deteksi keyword dari knowledge database OPTEMA"}, + {"label": "Problem utama", "value": problem_label, "note": "anchor yang dipakai ulang oleh Root Cause dan Rekomendasi"}, + {"label": "Data mentah", "value": ", ".join(raw_data[:8]) if raw_data else "Belum ada angka", "note": "hasil extract_data seperti angka, persen, budget, dan durasi" if raw_data else "tambahkan angka seperti 30%, 3 tahun, atau Rp200 juta"}, {"label": "Dana/budget", "value": _format_idr(budget) if budget else "Belum disebut", "note": "angka terbesar yang terdeteksi sebagai uang" if budget else "tambahkan nominal agar kalkulasi lebih presisi"}, {"label": "Target waktu", "value": _format_years(years) if years else "Belum disebut", "note": "diambil dari kata tahun/bulan/semester" if years else "tambahkan deadline atau durasi target"}, {"label": "Persentase/rasio", "value": ", ".join(f"{value:g}%" for value in percentages) if percentages else "Belum disebut", "note": "persen yang muncul di pertanyaan" if percentages else "tambahkan rasio seperti turun 30% atau target naik 20%"}, {"label": "Opsi/negara", "value": ", ".join(countries) if countries else "Belum spesifik", "note": "opsi yang muncul di pertanyaan" if countries else "tambahkan opsi yang ingin dibandingkan"}, ] + if top_solution: + top_score = decision_score(top_solution.impact, top_solution.efficiency, top_solution.speed, top_solution.low_risk) + detected.append({ + "label": "Skor keputusan terbaik", + "value": f"{float(top_score):.1f}/100", + "note": f"{top_solution.title}: impact {top_solution.impact}, efficiency {top_solution.efficiency}, speed {top_solution.speed}, low risk {top_solution.low_risk}", + }) calculations = [] if budget and years: months = years * 12 @@ -255,7 +549,7 @@ def _build_case_insights(description, category=None): calculations.append({"label": "Target waktu terdeteksi", "formula": f"Durasi target = {_format_years(years)}", "result": "Masukkan nominal dana/biaya agar bisa dihitung budget per bulan dan gap biaya."}) comparisons = [] - if category == "Pendidikan": + if detected_sector == "Pendidikan": for row in _education_cost_rows(case_data, fallback_years=years): if budget: fit = f"Budget menutup ±{row['coverage']}% dari estimasi minimum; gap minimum {_format_idr(row['shortfall_low'])}." if row["shortfall_low"] else f"Budget melewati estimasi minimum; sisa aman sekitar {_format_idr(row['surplus_low'])}." @@ -264,7 +558,7 @@ def _build_case_insights(description, category=None): comparisons.append({"option": row["country"], "estimate": _format_idr_range(row["total_low"], row["total_high"]), "budget_fit": fit, "note": row["note"]}) recommendation = "" - if category == "Pendidikan" and comparisons and budget: + if detected_sector == "Pendidikan" and comparisons and budget: rows = _education_cost_rows(case_data, fallback_years=years) best = min(rows, key=lambda item: item["total_low"]) recommendation = ( @@ -272,12 +566,17 @@ def _build_case_insights(description, category=None): if best["shortfall_low"] else f"Opsi paling aman secara angka awal adalah {best['country']} karena estimasi minimumnya masih masuk budget." ) - elif category and category != "Pendidikan": - category_item = next((item for item in ANALYSIS_DATABASE if item["kategori"] == category), None) - if category_item: - primary_cause = category_item["penyebab"][0] - primary_solution = category_item["solusi"][0] - recommendation = f"Sektor terdeteksi: {category}. Dengan konteks {_case_constraint_note(case_data)}, validasi dulu akar masalah utama: {primary_cause}. Prioritas solusi awal: {primary_solution}." + elif detected_sector and detected_sector != "Pendidikan": + if playbook: + primary_cause = playbook["causes"][0][0] + primary_solution = playbook["solutions"][0]["title"] + recommendation = f"Benang merah analisis: Problem Detection = {problem_label}; Root Cause utama = {primary_cause}; Rekomendasi prioritas = {primary_solution}. Semua skor solusi di bawah harus dibaca sebagai cara menutup akar masalah tersebut dengan konteks {_case_constraint_note(case_data)}." + else: + category_item = next((item for item in ANALYSIS_DATABASE if item["kategori"] == detected_sector), None) + if category_item: + primary_cause = category_item["penyebab"][0] + primary_solution = category_item["solusi"][0] + recommendation = f"Sektor terdeteksi: {detected_sector}. Dengan konteks {_case_constraint_note(case_data)}, validasi dulu akar masalah utama: {primary_cause}. Prioritas solusi awal: {primary_solution}." elif calculations: recommendation = f"Analisis disesuaikan dengan {_case_constraint_note(case_data)}; gunakan angka ini sebagai batas saat memilih solusi." @@ -286,7 +585,7 @@ def _build_case_insights(description, category=None): missing.append("Nominal dana/biaya/budget belum jelas.") if not years: missing.append("Target durasi atau deadline belum jelas.") - if category == "Pendidikan" and not countries: + if detected_sector == "Pendidikan" and not countries: missing.append("Negara/kampus pembanding belum disebut.") return { "detected": detected, @@ -427,6 +726,26 @@ ANALYSIS_DATABASE = [ "impact_label": "Budget ketat", "priority": 50, }, + { + "kategori": "Kesehatan Publik", + "keyword": [ + "narkoba", "penyalahguna", "penyalahgunaan", "zat terlarang", + "adiksi", "overdosis", "rehabilitasi", "bnn", "remaja", + "kesehatan mental", "populasi usia produktif", "korban narkoba", + ], + "penyebab": [ + "Pencegahan belum tepat sasaran ke kelompok rentan", + "Deteksi dini dan akses rehabilitasi belum kuat", + "Koordinasi data lintas lembaga belum terpadu", + ], + "solusi": [ + "Program pencegahan berbasis sekolah dan komunitas berisiko", + "Perluas screening, rehabilitasi, dan aftercare", + "Dashboard lintas lembaga untuk intervensi wilayah prioritas", + ], + "impact_label": "Kesehatan/sosial", + "priority": 38, + }, SECTOR_DATABASE["pemerintah"], SECTOR_DATABASE["bisnis"], SECTOR_DATABASE["keuangan"], @@ -466,13 +785,207 @@ ANALYSIS_DATABASE = [ }, ] + +PROBLEM_PLAYBOOKS = [ + { + "kategori": "Pemerintah", + "keywords": [ + "bumn", "kerugian", "rugi", "inefisiensi", "salah kelola", + "konflik kepentingan", "pengawasan", "komisaris", "penugasan pemerintah", + "skala keekonomian", "subsidi", "proyek strategis", "beban proyek", + ], + "problem": "Kerugian dan inefisiensi tata kelola BUMN/instansi", + "impact_label": "Publik/anggaran", + "causes": [ + ( + "Inefisiensi operasional dan portofolio proyek", + 96, + "Problem Detection membaca {problem} dengan konteks {constraint}. Ini mengarah ke biaya, proyek, dan proses yang belum dikontrol sebagai portofolio bernilai ekonomi.", + ), + ( + "Pengawasan dan akuntabilitas tata kelola lemah", + 92, + "Input menyebut sinyal salah kelola, konflik kepentingan, atau lemahnya pengawasan; akar ini menjelaskan mengapa kerugian dapat berulang walau masalah finansial sudah terlihat.", + ), + ( + "Beban penugasan tidak selaras skala keekonomian", + 86, + "Jika ada mandat publik, subsidi, atau penugasan pemerintah, keputusan perlu memisahkan biaya layanan publik dari kesehatan finansial entitas.", + ), + ( + "Early warning kinerja belum memicu intervensi cepat", + 78, + "Angka pada Problem Detection perlu dijadikan trigger tindakan; tanpa dashboard dan ambang batas stop-loss, masalah terlambat ditangani.", + ), + ], + "solutions": [ + { + "title": "Audit biaya dan portofolio proyek bermasalah", + "impact": 94, + "efficiency": 84, + "speed": 76, + "low_risk": 72, + "success_rate": 86, + "target_cause": "Inefisiensi operasional dan portofolio proyek", + "rationale": "Menjawab akar masalah {target_cause} dengan memetakan unit economics, membekukan proyek rugi, dan menetapkan stop-loss berdasarkan {constraint}.", + }, + { + "title": "Perkuat governance: komisaris, audit, dan konflik kepentingan", + "impact": 90, + "efficiency": 78, + "speed": 70, + "low_risk": 84, + "success_rate": 82, + "target_cause": "Pengawasan dan akuntabilitas tata kelola lemah", + "rationale": "Menargetkan {target_cause}; solusi ini membuat keputusan manajemen, pengawasan komisaris, dan audit memiliki owner, bukti, serta konsekuensi yang jelas.", + }, + { + "title": "Pisahkan mandat publik dari target komersial", + "impact": 86, + "efficiency": 82, + "speed": 68, + "low_risk": 80, + "success_rate": 79, + "target_cause": "Beban penugasan tidak selaras skala keekonomian", + "rationale": "Menjawab {target_cause} dengan membuat kontrak kinerja, kompensasi PSO/subsidi, dan batas kerugian yang transparan untuk setiap penugasan.", + }, + { + "title": "Dashboard early warning dan review kinerja bulanan", + "impact": 80, + "efficiency": 88, + "speed": 84, + "low_risk": 86, + "success_rate": 84, + "target_cause": "Early warning kinerja belum memicu intervensi cepat", + "rationale": "Mengubah angka pada {problem} menjadi alarm operasional: rugi, cash burn, deviasi proyek, dan efisiensi dipantau sebelum membesar.", + }, + ], + "steps": [ + (1, "Kunci problem dan angka baseline", "Tetapkan baseline {problem}: daftar entitas/proyek, nilai rugi, inefisiensi, subsidi, dan periode pengukuran dari {constraint}."), + (2, "Audit portofolio rugi", "Pisahkan rugi karena operasi, proyek, tata kelola, dan mandat publik; beri status stop, turnaround, merge, atau lanjut bersyarat."), + (3, "Tetapkan owner governance", "Tentukan owner keputusan, komisaris pengawas, audit internal, dan batas konflik kepentingan untuk setiap tindakan korektif."), + (4, "Jalankan quick win efisiensi", "Potong biaya/proyek yang paling jelas bocor sambil menjaga layanan publik wajib tetap berjalan."), + (5, "Decision gate bulanan", "Bandingkan hasil dengan baseline; scale tindakan yang menurunkan kerugian dan eskalasi entitas yang tidak membaik."), + ], + }, + { + "kategori": "Kesehatan Publik", + "keywords": [ + "narkoba", "penyalahguna", "penyalahgunaan", "zat terlarang", + "adiksi", "overdosis", "rehabilitasi", "remaja", "kesehatan mental", + "populasi usia produktif", "korban narkoba", + ], + "problem": "Kenaikan penyalahgunaan narkoba pada kelompok rentan", + "impact_label": "Kesehatan/sosial", + "causes": [ + ( + "Pencegahan belum tepat sasaran ke remaja dan usia produktif", + 95, + "Problem Detection membaca {problem} dengan konteks {constraint}. Sinyal remaja/usia produktif berarti pencegahan harus diarahkan ke segmen rentan, bukan kampanye umum saja.", + ), + ( + "Deteksi dini dan akses rehabilitasi belum kuat", + 90, + "Dampak kesehatan fisik dan mental tidak selesai dengan penindakan; perlu jalur screening, konseling, rehabilitasi, dan aftercare yang mudah diakses.", + ), + ( + "Koordinasi data lintas lembaga belum terpadu", + 84, + "Masalah populasi dan tren tahunan membutuhkan data gabungan sekolah, keluarga, layanan kesehatan, sosial, dan penegakan hukum agar intervensi tepat wilayah.", + ), + ( + "Dukungan keluarga, sekolah, dan reintegrasi sosial masih lemah", + 78, + "Risiko kambuh naik jika lingkungan setelah intervensi tidak diperbaiki; akar ini menghubungkan aspek kesehatan, sosial, dan pendidikan.", + ), + ], + "solutions": [ + { + "title": "Program pencegahan tertarget di sekolah dan komunitas rentan", + "impact": 92, + "efficiency": 84, + "speed": 78, + "low_risk": 86, + "success_rate": 87, + "target_cause": "Pencegahan belum tepat sasaran ke remaja dan usia produktif", + "rationale": "Menjawab {target_cause}; fokus pada kelompok yang disebut di Problem Detection agar edukasi, peer mentor, dan deteksi awal tidak menyebar terlalu umum.", + }, + { + "title": "Perluas screening, rehabilitasi, dan aftercare", + "impact": 90, + "efficiency": 76, + "speed": 72, + "low_risk": 78, + "success_rate": 82, + "target_cause": "Deteksi dini dan akses rehabilitasi belum kuat", + "rationale": "Menargetkan {target_cause}; solusi ini mengubah kasus terdeteksi menjadi jalur bantuan yang berkelanjutan, bukan hanya kampanye atau razia.", + }, + { + "title": "Dashboard lintas lembaga untuk wilayah prioritas", + "impact": 84, + "efficiency": 86, + "speed": 82, + "low_risk": 82, + "success_rate": 80, + "target_cause": "Koordinasi data lintas lembaga belum terpadu", + "rationale": "Menghubungkan data {problem} dengan keputusan lapangan: wilayah, sekolah, fasilitas kesehatan, dan komunitas mana yang harus diprioritaskan.", + }, + ], + "steps": [ + (1, "Segmentasi kelompok rentan", "Pecah {problem} berdasarkan usia, wilayah, sekolah/komunitas, dan tren dari {constraint}."), + (2, "Bangun jalur deteksi dan rujukan", "Tetapkan alur screening aman, konseling awal, rujukan rehabilitasi, serta perlindungan privasi."), + (3, "Pilot pencegahan tertarget", "Jalankan pilot di 3-5 wilayah/sekolah berisiko dengan materi, peer mentor, dan dukungan keluarga."), + (4, "Satukan dashboard", "Gabungkan indikator tren, jangkauan edukasi, rujukan rehabilitasi, relapse, dan kasus wilayah prioritas."), + (5, "Evaluasi outcome", "Bandingkan perubahan awareness, jumlah rujukan, retensi rehabilitasi, dan penurunan kasus di wilayah pilot."), + ], + }, + { + "kategori": "Bisnis", + "keywords": ["omzet", "penjualan", "jualan", "pelanggan", "customer", "produk", "marketing", "promosi"], + "problem": "Penjualan atau pertumbuhan bisnis tidak mencapai target", + "impact_label": "Besar", + "causes": [ + ("Segmen pelanggan belum tepat", 92, "Problem Detection membaca {problem} dengan konteks {constraint}. Jika pelanggan tidak tepat, promosi dan produk akan terlihat tidak efektif."), + ("Pesan marketing dan channel akuisisi kurang tajam", 86, "Akar ini menghubungkan problem penjualan dengan cara pasar menemukan dan memahami penawaran."), + ("Value produk belum sesuai kebutuhan utama pasar", 80, "Jika feedback pelanggan tidak cocok dengan value produk, solusi harus menguji ulang offer sebelum scale."), + ], + "solutions": [ + {"title": "Riset pelanggan dan repositioning offer", "impact": 88, "efficiency": 84, "speed": 78, "low_risk": 82, "success_rate": 84, "target_cause": "Segmen pelanggan belum tepat", "rationale": "Menjawab {target_cause}; validasi ulang siapa pembeli paling siap dan ubah offer berdasarkan bukti dari {constraint}."}, + {"title": "Eksperimen channel marketing terukur", "impact": 84, "efficiency": 78, "speed": 86, "low_risk": 76, "success_rate": 80, "target_cause": "Pesan marketing dan channel akuisisi kurang tajam", "rationale": "Menargetkan {target_cause} dengan eksperimen kecil pada pesan, channel, dan biaya per lead sebelum scale."}, + {"title": "Perbaiki paket produk berdasarkan feedback", "impact": 80, "efficiency": 82, "speed": 72, "low_risk": 84, "success_rate": 78, "target_cause": "Value produk belum sesuai kebutuhan utama pasar", "rationale": "Menghubungkan problem penjualan ke produk; paket, harga, dan bukti manfaat diperbaiki agar sesuai kebutuhan pelanggan."}, + ], + "steps": [ + (1, "Tentukan metrik target", "Tetapkan baseline {problem}: omzet, leads, conversion, repeat order, dan batas biaya dari {constraint}."), + (2, "Interview pelanggan", "Validasi 10-20 pelanggan/prospek untuk menemukan segmen dan pain point paling kuat."), + (3, "Uji pesan dan offer", "Jalankan eksperimen kecil pada 2-3 channel dengan budget dan target conversion jelas."), + (4, "Perbaiki paket", "Update bundling, harga, garansi, atau proof sesuai feedback paling sering."), + (5, "Scale yang terbukti", "Naikkan budget hanya pada channel/offer yang melewati target biaya dan conversion."), + ], + }, +] + +KNOWLEDGE = { + item["kategori"]: { + "keyword": item.get("keyword", []), + "root": item.get("penyebab", []), + "solution": item.get("solusi", []), + } + for item in ANALYSIS_DATABASE + if item["kategori"] != "Umum" +} + + +def detect_sector(text): + return _detect_problem_category(text or "")["kategori"] + + def _detect_problem_category(description): text = description.lower() best_item = None best_score = -1 for item in ANALYSIS_DATABASE: - matches = sum(1 for keyword in item["keyword"] if keyword in text) + matches = sum(1 for keyword in item["keyword"] if _keyword_in_text(text, keyword)) if matches == 0: continue @@ -509,7 +1022,7 @@ def _infer_business_area(description): text = description.lower() kategori = _detect_problem_category(description)["kategori"] - if kategori in {"Pendidikan", "Pemerintah", "Umum"}: + if kategori in {"Pendidikan", "Pemerintah", "Kesehatan Publik", "Umum"}: return ProblemCase.AREA_OTHER if kategori == "Keuangan": return ProblemCase.AREA_FINANCE @@ -547,7 +1060,7 @@ def _write_analysis_records(problem, financial_impact, cause_profiles, options, for option in options: scored.append({ **option, - "decision_score": _decision_score( + "decision_score": decision_score( option["impact"], option["efficiency"], option["speed"], @@ -627,6 +1140,12 @@ def _build_category_analysis(problem, category): solutions = category["solusi"] case_data = _extract_case_data(problem.description) constraint_note = _case_constraint_note(case_data) + playbook = _matching_playbook(problem.description, kategori) + if playbook: + cause_profiles, options, steps = _render_playbook_analysis(playbook, constraint_note) + _write_analysis_records(problem, playbook.get("impact_label", category.get("impact_label", "Sedang")), cause_profiles, options, steps) + return + cause_profiles = [ (cause, _clamp(92 - index * 8), f"Input terdeteksi sebagai kategori {kategori} dengan konteks: {constraint_note}. Penyebab ini perlu divalidasi karena dapat langsung memengaruhi target, batasan, dan keputusan berikutnya.") for index, cause in enumerate(causes) @@ -742,6 +1261,58 @@ def case_list(request): }) +def web_intelligence(request): + form = WebIntelligenceForm(request.POST or None) + source_data = None + ai_result = None + analysis_error = None + + if request.method == "POST": + if form.is_valid(): + query = form.cleaned_data.get("query", "") + source_url = form.cleaned_data["url"] + api_key = (form.cleaned_data.get("api_key") or "").strip() + + try: + source_data = _fetch_web_source(source_url) + if api_key: + try: + ai_result = _call_openai_web_intelligence(api_key, query, source_data["content"]) + except ValueError as exc: + analysis_error = str(exc) + except requests.Timeout: + analysis_error = "Permintaan ke OpenAI timeout. Coba ulang beberapa saat lagi." + except requests.RequestException as exc: + logger.warning("OpenAI web intelligence request failed for %s: %s", source_data["domain"], exc) + analysis_error = "Analisis OpenAI gagal diproses. Periksa API key, quota, atau koneksi." + except Exception: + logger.exception("Unexpected OpenAI web intelligence error") + analysis_error = "Analisis OpenAI gagal diproses. Coba ulang atau gunakan sumber yang lebih ringkas." + else: + analysis_error = "Data internet berhasil diambil. Masukkan OpenAI API Key untuk menjalankan analisis AI." + except ValueError as exc: + messages.error(request, str(exc)) + except requests.Timeout: + messages.error(request, "URL sumber terlalu lama merespons. Coba URL lain atau ulangi nanti.") + except requests.RequestException as exc: + logger.warning("Web intelligence fetch failed for %s: %s", source_url, exc) + messages.error(request, "URL sumber tidak bisa diakses. Pastikan halaman publik dan dapat dibuka dari internet.") + except Exception: + logger.exception("Unexpected web intelligence fetch error") + messages.error(request, "Terjadi kesalahan saat membaca sumber data internet.") + else: + messages.error(request, "Mohon periksa input. URL sumber data wajib valid.") + + return render(request, "core/web_intelligence.html", { + "form": form, + "source_data": source_data, + "ai_result": ai_result, + "analysis_error": analysis_error, + "page_title": "Web Intelligence — OPTEMA AI", + "meta_description": "Analisis masalah berbasis data internet dengan OPTEMA AI: scrape sumber URL, deteksi problem, root cause, solusi, risiko, action plan, dan KPI.", + }) + + def case_detail(request, pk): action_steps = Prefetch("solutions__action_steps", queryset=ActionPlanStep.objects.all()) problem = get_object_or_404( @@ -750,11 +1321,12 @@ def case_detail(request, pk): ) top_solution = problem.solutions.first() category = _detect_problem_category(problem.description) - case_insights = _build_case_insights(problem.description, category["kategori"]) + case_insights = _build_case_insights(problem.description, category["kategori"], top_solution=top_solution) return render(request, "core/case_detail.html", { "problem": problem, "top_solution": top_solution, "case_insights": case_insights, + "detected_category": category["kategori"], "page_title": f"{problem.title} — Analisis OPTEMA AI", "meta_description": f"Analisis prioritas, akar masalah, solusi berskor, kalkulasi data, dan action plan untuk {problem.title}.", }) diff --git a/requirements.txt b/requirements.txt index e22994c..c98c8b9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,5 @@ Django==5.2.7 mysqlclient==2.2.7 python-dotenv==1.1.1 +requests==2.32.3 +beautifulsoup4==4.12.3