Skip to content

Commit 88a048b

Browse files
committed
Add McLaren NVIDIA demo app
1 parent 51ce645 commit 88a048b

36 files changed

Lines changed: 1744 additions & 14 deletions

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ USER splunk
2323

2424
WORKDIR /tmp/sdk
2525

26-
RUN /bin/bash -c "LD_LIBRARY_PATH=/opt/splunk/lib /opt/splunk/bin/python3.13 -m pip install '.[openai]' --target=/splunklib-deps"
26+
RUN /bin/bash -c "LD_LIBRARY_PATH=/opt/splunk/lib /opt/splunk/bin/python3.13 -m pip install '.[openai,bedrock]' --target=/splunklib-deps"
2727

2828
USER ${ANSIBLE_USER}
2929
WORKDIR /opt/splunk

Makefile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,3 +116,7 @@ docker-splunk-restart:
116116
.PHONY: docker-tail-python-log
117117
docker-tail-python-log:
118118
docker exec -it $(CONTAINER_NAME) sudo tail $(SPLUNK_HOME)/var/log/splunk/python.log
119+
120+
.PHONY: bedrock-login
121+
bedrock-login:
122+
./scripts/bedrock-login.sh

compose.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ services:
1111
- SPLUNK_HEC_TOKEN=11111111-1111-1111-1111-1111111111113
1212
- SPLUNK_PASSWORD=changed!
1313
- SPLUNK_APPS_URL=https://github.com/splunk/sdk-app-collection/releases/latest/download/sdkappcollection.tgz
14+
- TZ=Europe/Warsaw
1415
ports:
1516
- "8000:8000"
1617
- "8088:8088"
@@ -33,6 +34,7 @@ services:
3334
- "./examples/ai_custom_search_app:/opt/splunk/etc/apps/ai_custom_search_app"
3435
- "./examples/ai_custom_alert_app:/opt/splunk/etc/apps/ai_custom_alert_app"
3536
- "./examples/ai_modinput_app:/opt/splunk/etc/apps/ai_modinput_app"
37+
- "~/Projects/mclaren-demo-app:/opt/splunk/etc/apps/mclaren_demo_app"
3638

3739
- "./splunklib:/opt/splunk/etc/apps/eventing_app/bin/splunklib"
3840
- "./splunklib:/opt/splunk/etc/apps/generating_app/bin/splunklib"
@@ -44,6 +46,9 @@ services:
4446
- "./splunklib:/opt/splunk/etc/apps/ai_custom_search_app/bin/lib/splunklib"
4547
- "./splunklib:/opt/splunk/etc/apps/ai_custom_alert_app/bin/lib/splunklib"
4648
- "./splunklib:/opt/splunk/etc/apps/ai_modinput_app/bin/lib/splunklib"
49+
- "./splunklib:/opt/splunk/etc/apps/mclaren_demo_app/bin/lib/splunklib"
4750

4851
- "./tests:/opt/splunk/etc/apps/ai_agentic_test_app/bin/lib/tests"
4952
- "./tests:/opt/splunk/etc/apps/ai_agentic_test_local_tools_app/bin/lib/tests"
53+
54+
- "~/.aws:/home/splunk/.aws:ro"

examples/ai_modinput_app/bin/agentic_weather.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,6 @@
2020
from _collections_abc import dict_items
2121
from typing import final, override
2222

23-
from splunklib.ai.messages import AIMessage, ContentBlock, TextBlock
24-
2523
# ! NOTE: This insert is only needed for splunk-sdk-python CI/CD to work.
2624
# ! Remove this if you're modifying this example locally.
2725
sys.path.insert(0, "/splunklib-deps")
@@ -33,6 +31,7 @@
3331

3432
from splunklib.ai import OpenAIModel
3533
from splunklib.ai.agent import Agent
34+
from splunklib.ai.messages import AIMessage, ContentBlock, TextBlock
3635
from splunklib.modularinput.argument import Argument
3736
from splunklib.modularinput.event import Event
3837
from splunklib.modularinput.event_writer import EventWriter
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Running
6+
7+
```bash
8+
# Start Splunk (from the splunk-sdk-python root):
9+
docker compose up -d --build
10+
11+
# Start the F1 2025 data collector (separate Docker container):
12+
./start-collector.sh
13+
14+
# After Splunk is up, enable all three modinput stanzas:
15+
docker exec splunk /opt/splunk/bin/splunk http-event-collector enable -uri https://localhost:8089 -auth admin:changed! -server-cert-cn SplunkServerDefaultCert
16+
curl -sk -u admin:changed! https://localhost:8089/services/data/inputs/turn_coach/turn_coach/enable -d '' -X POST
17+
curl -sk -u admin:changed! https://localhost:8089/services/data/inputs/pit_strategy/pit_strategy/enable -d '' -X POST
18+
curl -sk -u admin:changed! https://localhost:8089/services/data/inputs/race_summary/race_summary/enable -d '' -X POST
19+
```
20+
21+
The `docker-compose.yml` in the repo root mounts `examples/mclaren_demo_app` directly into
22+
the Splunk container as `/opt/splunk/etc/apps/mclaren_demo_app` — no deploy step needed.
23+
24+
## Commands
25+
26+
```bash
27+
# Lint: ruff fix + format + basedpyright (run from splunk-sdk-python root)
28+
make lint
29+
30+
# Tail the advisor log
31+
docker exec splunk tail -f /opt/splunk/var/log/splunk/mclaren_demo_app.log
32+
33+
# Run a modular input manually (LD_LIBRARY_PATH required for libssl inside container)
34+
docker exec splunk bash -c "LD_LIBRARY_PATH=/opt/splunk/lib /opt/splunk/bin/python3.13 /opt/splunk/etc/apps/mclaren_demo_app/bin/turn_coach.py"
35+
36+
# Restart Splunk (picks up config changes without full container rebuild)
37+
make docker-splunk-restart
38+
39+
# Full container rebuild (needed after Dockerfile or compose.yml changes)
40+
make docker-refresh
41+
```
42+
43+
No test suite. `make lint` (from the repo root) is the only automated gate.
44+
Run it after every code change before considering work done.
45+
46+
## Architecture
47+
48+
Three **modular inputs** poll `index=main source=f1_2025` via oneshot searches and
49+
emit structured JSON advice to `index=main`.
50+
51+
```
52+
F1 2025 game → UDP → f1-2025 collector (Docker :8501)
53+
→ HEC → Splunk Enterprise (:8088) → index=main source=f1_2025
54+
55+
Modular inputs (bin/*.py, each a splunklib.modularinput.Script subclass):
56+
turn_coach.py — triggers on sector change (LapData.sector field)
57+
pit_strategy.py — triggers on lap change (LapData.current_lap_num field)
58+
race_summary.py — triggers on race end (FinalClassificationData result_status=3)
59+
60+
Each input → splunklib.ai.Agent (LangChain backend) → LLM → JSON event → index=main
61+
```
62+
63+
**LLM connector:** `bedrock_connector.py` creates an `AnthropicBedrockModel` that
64+
authenticates via the `ssc-ci-cd_bedrock-inference-role` AWS profile using SigV4
65+
(no stored credentials — `credential_process` in `~/.aws/config` calls `dev-login`).
66+
The fallback is `circuit_connector.py` which uses the Cisco Circuit AI gateway (OpenAI-compatible).
67+
All three modinputs import one of these connectors to get their `_LLM_MODEL`.
68+
69+
**Dashboard:** Single `advisor` dashboard (`local/data/ui/views/advisor.xml`) with three
70+
side-by-side panels (Turn Coach, Pit Strategy, Race Debrief). Each panel has its own
71+
JS/CSS file under `appserver/static/`. Dashboard loads via Splunk Simple XML extension
72+
(`script=` / `stylesheet=` attributes).
73+
74+
**Events:** All three sourcetypes use `KV_MODE = json` (see `default/props.conf`).
75+
JS polls via `service.oneshotSearch()` (Splunk Web SDK, authenticated) every 3 seconds.
76+
77+
**Dependencies:** Runtime deps are bundled into `bin/lib/` (volume-mounted live into
78+
Splunk). `bin/lib/splunklib` is a symlink to the repo's `splunklib/` (set up in
79+
`docker-compose.yml`). All other 3rd-party packages (`langchain*`, `httpx`, `pydantic`,
80+
`boto3`/`botocore`, etc.) must be installed into `bin/lib/` before the modinputs can run.
81+
82+
## Key Constraints
83+
84+
- `bin/lib/` is the only dependency path available inside the Splunk container.
85+
- AWS session tokens from `dev-login` expire ~1h; refresh by running any `aws` command
86+
via `dev-login` or logging in again.
87+
- After any JS/CSS/conf change, hit `http://localhost:8000/en-GB/debug/refresh` then hard-reload
88+
the dashboard. This reloads all app confs and bumps static asset cache in one shot.
89+
- `basedpyright` config in the root `pyproject.toml` silences `splunklib` type stubs —
90+
use `# pyright: ignore[reportMissingImports]` for splunklib imports in bin/ files.
91+
- Line length limit is 100 chars (ruff `E501` is ignored, but keep lines readable).
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
[turn_coach://<name>]
2+
python.required = 3.13
3+
placeholder = <string>
4+
5+
[pit_strategy://<name>]
6+
python.required = 3.13
7+
placeholder = <string>
8+
9+
[race_summary://<name>]
10+
python.required = 3.13
11+
placeholder = <string>
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
:root {
2+
--color-accent: #e8440a;
3+
--color-bg: #111;
4+
--color-border: #2a2a2a;
5+
--color-border-subtle: #222;
6+
--color-text: #e0e0e0;
7+
--color-text-dim: #aaa;
8+
--color-text-muted: #666;
9+
--color-text-new: #fff;
10+
}
11+
12+
body[data-tc-theme="light"] {
13+
--color-bg: #f5f5f5;
14+
--color-border: #d0d0d0;
15+
--color-border-subtle: #e0e0e0;
16+
--color-text: #1a1a1a;
17+
--color-text-dim: #444;
18+
--color-text-muted: #888;
19+
--color-text-new: #000;
20+
}
21+
22+
[id$="-panel"] {
23+
display: flex;
24+
flex-direction: column;
25+
max-height: 75vh;
26+
font-family: "Splunk Platform Sans", sans-serif;
27+
}
28+
29+
[id$="-header"] {
30+
display: flex;
31+
flex-shrink: 0;
32+
justify-content: space-between;
33+
align-items: center;
34+
padding: 10px 16px;
35+
border-bottom: 1px solid var(--color-border);
36+
background: var(--color-bg);
37+
cursor: default;
38+
}
39+
40+
.advisor-collapse-btn {
41+
background: none;
42+
border: none;
43+
color: var(--color-text-muted);
44+
font-size: 14px;
45+
padding: 0 8px 0 0;
46+
cursor: pointer;
47+
line-height: 1;
48+
transition: transform 0.2s ease;
49+
}
50+
51+
.advisor-collapse-btn.collapsed {
52+
transform: rotate(-90deg);
53+
}
54+
55+
[id$="-feed"].collapsed {
56+
display: none;
57+
}
58+
59+
[id$="-header"] h1 {
60+
font-size: 1em;
61+
color: var(--color-text-dim);
62+
letter-spacing: 2px;
63+
text-transform: uppercase;
64+
}
65+
66+
[id$="-feed"] {
67+
padding: 12px 16px;
68+
display: flex;
69+
flex-direction: column;
70+
overflow-y: auto;
71+
flex: 1;
72+
min-height: 0;
73+
background: var(--color-bg);
74+
}
75+
76+
[id$="-empty"] {
77+
color: var(--color-text-muted);
78+
text-align: center;
79+
margin-top: 10px;
80+
margin-bottom: 10px;
81+
font-size: 12px;
82+
}
83+
84+
[class$="-controls"] {
85+
display: flex;
86+
gap: 8px;
87+
align-items: center;
88+
}
89+
90+
.advisor-btn {
91+
background: none;
92+
border: 1px solid #333;
93+
color: #666;
94+
font-size: 11px;
95+
padding: 3px 8px;
96+
cursor: pointer;
97+
letter-spacing: 1px;
98+
}
99+
100+
.advisor-btn:hover {
101+
border-color: #666;
102+
color: #aaa;
103+
}
104+
105+
.advisor-btn:disabled {
106+
opacity: 0.3;
107+
cursor: not-allowed;
108+
}
109+
110+
.advisor-message {
111+
padding: 10px 0;
112+
border-bottom: 1px solid var(--color-border-subtle);
113+
animation: advisor-fadein 0.3s ease;
114+
}
115+
116+
.advisor-message:last-child {
117+
border-bottom: none;
118+
}
119+
120+
.advisor-message-header {
121+
font-size: 10px;
122+
color: var(--color-text-muted);
123+
margin-bottom: 4px;
124+
letter-spacing: 1px;
125+
}
126+
127+
.advisor-label {
128+
color: var(--color-accent);
129+
}
130+
131+
.advisor-advice {
132+
color: var(--color-text);
133+
line-height: 1.5;
134+
font-size: 14px;
135+
}
136+
137+
.advisor-message.new .advisor-advice {
138+
color: var(--color-text-new);
139+
}
140+
141+
#dl-feed .advisor-advice {
142+
font-family: monospace;
143+
font-size: 12px;
144+
white-space: pre-wrap;
145+
word-break: break-all;
146+
}
147+
148+
@keyframes advisor-fadein {
149+
from {
150+
opacity: 0;
151+
transform: translateY(-4px);
152+
}
153+
154+
to {
155+
opacity: 1;
156+
transform: translateY(0);
157+
}
158+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
window.advisorShared = {
2+
// Creates a message element. Pass time="" to omit the time span (e.g. debug log uses label=time).
3+
createMessage({ label, time = "", advice }) {
4+
const msgHeader = document.createElement("div");
5+
msgHeader.className = "advisor-message-header";
6+
7+
const labelSpan = document.createElement("span");
8+
labelSpan.className = "advisor-label";
9+
labelSpan.textContent = label;
10+
msgHeader.appendChild(labelSpan);
11+
12+
if (time) {
13+
const timeSpan = document.createElement("span");
14+
timeSpan.style.marginLeft = "8px";
15+
timeSpan.textContent = time;
16+
msgHeader.appendChild(timeSpan);
17+
}
18+
19+
const adviceDiv = document.createElement("div");
20+
adviceDiv.className = "advisor-advice";
21+
adviceDiv.textContent = advice;
22+
23+
const msg = document.createElement("div");
24+
msg.className = "advisor-message";
25+
msg.appendChild(msgHeader);
26+
msg.appendChild(adviceDiv);
27+
28+
return msg;
29+
},
30+
31+
insertMessage(feed, emptyId, msg, isNew) {
32+
document.getElementById(emptyId)?.remove();
33+
34+
if (isNew) {
35+
msg.classList.add("new");
36+
feed.prepend(msg);
37+
setTimeout(() => msg.classList.remove("new"), 2000);
38+
} else {
39+
feed.append(msg);
40+
}
41+
},
42+
};
43+
44+
require(["splunkjs/mvc/simplexml/ready!"], () => {
45+
document.addEventListener("click", (e) => {
46+
const btn = e.target.closest(".advisor-collapse-btn");
47+
if (!btn) return;
48+
const header = btn.closest("[id$='-header']");
49+
const feed = header?.nextElementSibling;
50+
if (!feed) return;
51+
const collapsed = feed.classList.toggle("collapsed");
52+
btn.classList.toggle("collapsed", collapsed);
53+
});
54+
});

0 commit comments

Comments
 (0)