fix advisor and drop down refresh
This commit is contained in:
@@ -176,16 +176,24 @@ function inferredProfileType(row) {
|
|||||||
}
|
}
|
||||||
function profileDiscoveryDetails(row) {
|
function profileDiscoveryDetails(row) {
|
||||||
const d = row.discovery || {};
|
const d = row.discovery || {};
|
||||||
|
const detailId = `profile-discovery:${row.stream_id || row.stream_name || ''}:${d.field_count || 0}`;
|
||||||
if (!d.field_count) {
|
if (!d.field_count) {
|
||||||
const common = (row.common_fields || []).slice(0,8).map(item => `${item.field} ${(Number(item.coverage || 0)*100).toFixed(0)}%/${item.unique_values}`).join(', ');
|
const common = (row.common_fields || []).slice(0,8).map(item => `${item.field} ${(Number(item.coverage || 0)*100).toFixed(0)}%/${item.unique_values}`).join(', ');
|
||||||
return `<details><summary>waiting for discovery data</summary><p>This row was produced before profile discovery metadata was added. Wait for the next monitor cycle after updating, or re-apply/edit the profile.</p><p><b>Observed common fields</b><br>${esc(common || '-')}</p></details>`;
|
return `<details data-detail-id="${esc(detailId)}"><summary>waiting for discovery data</summary><p>This row was produced before profile discovery metadata was added. Wait for the next monitor cycle after updating, or re-apply/edit the profile.</p><p><b>Observed common fields</b><br>${esc(common || '-')}</p></details>`;
|
||||||
}
|
}
|
||||||
const selected = d.selected_fields || {};
|
const selected = d.selected_fields || {};
|
||||||
const top = (d.top_fields || []).slice(0,8).map(item => `${item.field} ${(Number(item.coverage || 0)*100).toFixed(0)}%/${item.unique_values}`).join(', ');
|
const top = (d.top_fields || []).slice(0,8).map(item => `${item.field} ${(Number(item.coverage || 0)*100).toFixed(0)}%/${item.unique_values}`).join(', ');
|
||||||
const shared = (d.shared_fields || row.shared_fields || []).slice(0,8).map(item => `${item.field} (${item.streams} streams)`).join(', ');
|
const shared = (d.shared_fields || row.shared_fields || []).slice(0,8).map(item => `${item.field} (${item.streams} streams)`).join(', ');
|
||||||
const rejected = (d.rejected_fields || []).slice(0,8).map(item => `${item.field}: ${item.reason}`).join('; ');
|
const rejected = (d.rejected_fields || []).slice(0,8).map(item => `${item.field}: ${item.reason}`).join('; ');
|
||||||
const reasons = (d.reasons || []).join('; ');
|
const reasons = (d.reasons || []).join('; ');
|
||||||
return `<details><summary>${esc(d.field_count)} fields analyzed</summary><p><b>Selected</b><br>Entity: ${esc((selected.entity || []).join(', ') || '-')}<br>Time: ${esc((selected.time || []).join(', ') || '-')}<br>Categorical: ${esc((selected.categorical || []).join(', ') || '-')}<br>Numeric: ${esc((selected.numeric || []).join(', ') || '-')}</p><p><b>Shared across streams</b><br>${esc(shared || '-')}</p><p><b>Top fields</b><br>${esc(top || '-')}</p><p><b>Rejected</b><br>${esc(rejected || '-')}</p><p>${esc(reasons || '')}</p></details>`;
|
return `<details data-detail-id="${esc(detailId)}"><summary>${esc(d.field_count)} fields analyzed</summary><p><b>Selected</b><br>Entity: ${esc((selected.entity || []).join(', ') || '-')}<br>Time: ${esc((selected.time || []).join(', ') || '-')}<br>Categorical: ${esc((selected.categorical || []).join(', ') || '-')}<br>Numeric: ${esc((selected.numeric || []).join(', ') || '-')}</p><p><b>Shared across streams</b><br>${esc(shared || '-')}</p><p><b>Top fields</b><br>${esc(top || '-')}</p><p><b>Rejected</b><br>${esc(rejected || '-')}</p><p>${esc(reasons || '')}</p></details>`;
|
||||||
|
}
|
||||||
|
function profileAdvisorLabel(row, advisor) {
|
||||||
|
const rowAdvisor = row.profile_advisor || {};
|
||||||
|
if (rowAdvisor.status === 'heuristic' && rowAdvisor.error) return 'heuristic (advisor error)';
|
||||||
|
if (rowAdvisor.status) return rowAdvisor.status;
|
||||||
|
if (advisor.enabled && advisor.status === 'error') return 'heuristic (advisor error)';
|
||||||
|
return advisor.enabled ? (advisor.status || 'checking') : 'heuristic';
|
||||||
}
|
}
|
||||||
function drawTrend(history) {
|
function drawTrend(history) {
|
||||||
const canvas=document.getElementById('trendChart'), ctx=canvas.getContext('2d'), ratio=window.devicePixelRatio||1, cw=canvas.clientWidth, ch=canvas.clientHeight;
|
const canvas=document.getElementById('trendChart'), ctx=canvas.getContext('2d'), ratio=window.devicePixelRatio||1, cw=canvas.clientWidth, ch=canvas.clientHeight;
|
||||||
@@ -308,7 +316,8 @@ async function refresh() {
|
|||||||
`MCP aggregate events: ${esc(mcp.aggregate_events || 0)}`,
|
`MCP aggregate events: ${esc(mcp.aggregate_events || 0)}`,
|
||||||
`MCP raw sample events: ${esc(mcp.raw_events_fetched || mcp.events_fetched || 0)}`,
|
`MCP raw sample events: ${esc(mcp.raw_events_fetched || mcp.events_fetched || 0)}`,
|
||||||
`MCP coverage: ${esc(mcp.coverage_status || 'unknown')}${mcp.partial_streams ? ` (${esc(mcp.partial_streams)} partial)` : ''}${mcp.truncated_streams ? ` (${esc(mcp.truncated_streams)} truncated)` : ''}${mcp.sample_limited_streams ? ` (${esc(mcp.sample_limited_streams)} sample capped)` : ''}`,
|
`MCP coverage: ${esc(mcp.coverage_status || 'unknown')}${mcp.partial_streams ? ` (${esc(mcp.partial_streams)} partial)` : ''}${mcp.truncated_streams ? ` (${esc(mcp.truncated_streams)} truncated)` : ''}${mcp.sample_limited_streams ? ` (${esc(mcp.sample_limited_streams)} sample capped)` : ''}`,
|
||||||
mcp.coverage_warning ? `<span class="sev-high">${esc(mcp.coverage_warning)}</span>` : ''
|
mcp.coverage_warning ? `<span class="sev-high">${esc(mcp.coverage_warning)}</span>` : '',
|
||||||
|
advisor.status === 'error' ? `<span class="sev-high">Profile advisor error: ${esc(advisor.error || 'unknown error')}</span>` : ''
|
||||||
].filter(Boolean).join('<br>');
|
].filter(Boolean).join('<br>');
|
||||||
document.getElementById('health').innerHTML = [
|
document.getElementById('health').innerHTML = [
|
||||||
metric('Enabled streams', enabledStreams.length), metric('Streams missing profile', streamsMissingProfile), metric('MCP aggregate events', mcp.aggregate_events || 0), metric('MCP raw sample', mcp.raw_events_fetched || mcp.events_fetched || 0), metric('Partial streams', mcp.partial_streams || 0), metric('Sample capped', mcp.sample_limited_streams || 0), metric('Truncated streams', mcp.truncated_streams || 0), metric('Correlated entities', correlations.length)
|
metric('Enabled streams', enabledStreams.length), metric('Streams missing profile', streamsMissingProfile), metric('MCP aggregate events', mcp.aggregate_events || 0), metric('MCP raw sample', mcp.raw_events_fetched || mcp.events_fetched || 0), metric('Partial streams', mcp.partial_streams || 0), metric('Sample capped', mcp.sample_limited_streams || 0), metric('Truncated streams', mcp.truncated_streams || 0), metric('Correlated entities', correlations.length)
|
||||||
@@ -324,7 +333,7 @@ async function refresh() {
|
|||||||
{label:'Events', key:'events'},
|
{label:'Events', key:'events'},
|
||||||
{label:'Type', render:r => esc(inferredProfileType(r))},
|
{label:'Type', render:r => esc(inferredProfileType(r))},
|
||||||
{label:'Confidence', key:'confidence'},
|
{label:'Confidence', key:'confidence'},
|
||||||
{label:'Advisor', render:r => esc((r.profile_advisor || {}).status || (advisor.enabled ? advisor.status : 'heuristic'))},
|
{label:'Advisor', render:r => esc(profileAdvisorLabel(r, advisor))},
|
||||||
{label:'Profile', render:r => r.profile_exists ? 'exists' : 'new'},
|
{label:'Profile', render:r => r.profile_exists ? 'exists' : 'new'},
|
||||||
{label:'Entity fields', render:r => esc((r.entity_fields || []).join(', ') || '-')},
|
{label:'Entity fields', render:r => esc((r.entity_fields || []).join(', ') || '-')},
|
||||||
{label:'Time', key:'timestamp_field'},
|
{label:'Time', key:'timestamp_field'},
|
||||||
|
|||||||
@@ -7,6 +7,31 @@ from urllib import request
|
|||||||
from .models import BlockCandidate, Finding
|
from .models import BlockCandidate, Finding
|
||||||
|
|
||||||
|
|
||||||
|
def _json_object_from_text(text: str) -> dict[str, object]:
|
||||||
|
text = text.strip()
|
||||||
|
if text.startswith("```"):
|
||||||
|
lines = text.splitlines()
|
||||||
|
if lines and lines[0].startswith("```"):
|
||||||
|
lines = lines[1:]
|
||||||
|
if lines and lines[-1].strip() == "```":
|
||||||
|
lines = lines[:-1]
|
||||||
|
text = "\n".join(lines).strip()
|
||||||
|
try:
|
||||||
|
payload = json.loads(text)
|
||||||
|
return payload if isinstance(payload, dict) else {}
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
pass
|
||||||
|
start = text.find("{")
|
||||||
|
end = text.rfind("}")
|
||||||
|
if start == -1 or end == -1 or end <= start:
|
||||||
|
return {}
|
||||||
|
try:
|
||||||
|
payload = json.loads(text[start:end + 1])
|
||||||
|
return payload if isinstance(payload, dict) else {}
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
def ollama_summary(
|
def ollama_summary(
|
||||||
findings: list[Finding],
|
findings: list[Finding],
|
||||||
candidates: list[BlockCandidate],
|
candidates: list[BlockCandidate],
|
||||||
@@ -121,6 +146,6 @@ def ollama_profile_advice(suggestions: list[dict[str, object]], model: str | Non
|
|||||||
req = request.Request(f"{host}/api/generate", data=body, method="POST", headers={"Content-Type": "application/json"})
|
req = request.Request(f"{host}/api/generate", data=body, method="POST", headers={"Content-Type": "application/json"})
|
||||||
with request.urlopen(req, timeout=selected_timeout) as response:
|
with request.urlopen(req, timeout=selected_timeout) as response:
|
||||||
data = json.loads(response.read().decode("utf-8"))
|
data = json.loads(response.read().decode("utf-8"))
|
||||||
payload = json.loads(str(data.get("response", "{}")))
|
payload = _json_object_from_text(str(data.get("response", "{}")))
|
||||||
profiles = payload.get("profiles", [])
|
profiles = payload.get("profiles", [])
|
||||||
return profiles if isinstance(profiles, list) else []
|
return profiles if isinstance(profiles, list) else []
|
||||||
|
|||||||
@@ -264,6 +264,8 @@ def build_status(
|
|||||||
profile_advisor_status = {"enabled": True, "status": "ok", "model": str(runtime_values.get("profile_advisor_model", "") or "qwen3:8b")}
|
profile_advisor_status = {"enabled": True, "status": "ok", "model": str(runtime_values.get("profile_advisor_model", "") or "qwen3:8b")}
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
profile_advisor_status = {"enabled": True, "status": "error", "error": str(exc), "model": str(runtime_values.get("profile_advisor_model", "") or "qwen3:8b")}
|
profile_advisor_status = {"enabled": True, "status": "error", "error": str(exc), "model": str(runtime_values.get("profile_advisor_model", "") or "qwen3:8b")}
|
||||||
|
for suggestion in profile_suggestions:
|
||||||
|
suggestion.setdefault("profile_advisor", {"status": "heuristic", "error": str(exc)})
|
||||||
intel_ips = sorted(
|
intel_ips = sorted(
|
||||||
{
|
{
|
||||||
ip
|
ip
|
||||||
|
|||||||
23
tests/test_llm.py
Normal file
23
tests/test_llm.py
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import unittest
|
||||||
|
|
||||||
|
from fgai.llm import _json_object_from_text
|
||||||
|
|
||||||
|
|
||||||
|
class LlmTests(unittest.TestCase):
|
||||||
|
def test_json_object_from_text_accepts_markdown_wrapped_json(self):
|
||||||
|
payload = _json_object_from_text(
|
||||||
|
'```json\n{"profiles":[{"stream_id":"windows","entity_fields":["username"]}]}\n```'
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(payload["profiles"][0]["stream_id"], "windows")
|
||||||
|
|
||||||
|
def test_json_object_from_text_extracts_object_from_extra_text(self):
|
||||||
|
payload = _json_object_from_text(
|
||||||
|
'Here is the profile:\n{"profiles":[{"stream_id":"firewall"}]}\nDone.'
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(payload["profiles"][0]["stream_id"], "firewall")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -137,6 +137,8 @@ class MonitorTests(unittest.TestCase):
|
|||||||
advisor = status["capabilities"]["profile_advisor"]
|
advisor = status["capabilities"]["profile_advisor"]
|
||||||
self.assertEqual(advisor["status"], "error")
|
self.assertEqual(advisor["status"], "error")
|
||||||
self.assertIn("timeout", advisor["error"])
|
self.assertIn("timeout", advisor["error"])
|
||||||
|
self.assertEqual(status["profile_suggestions"][0]["profile_advisor"]["status"], "heuristic")
|
||||||
|
self.assertIn("timeout", status["profile_suggestions"][0]["profile_advisor"]["error"])
|
||||||
|
|
||||||
def test_cached_status_with_error_keeps_last_good_dashboard_data(self):
|
def test_cached_status_with_error_keeps_last_good_dashboard_data(self):
|
||||||
with tempfile.TemporaryDirectory() as tmp:
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
|||||||
Reference in New Issue
Block a user