Skip to content

Commit a29ff65

Browse files
authored
Add example AI Modular Input app (#60)
* Add ai_modinput_app * Finish work on ai_modinput_input * PR fixes #1 * PR fixes #2 * PR fixes #3 * Remove unnecessary `--force-rebuild` flag from `make docker-up` * PR fixes #n * Add missing newline * Fix README * Finish fixing ai_modinput_app * PR fixes
1 parent 8070585 commit a29ff65

11 files changed

Lines changed: 1718 additions & 4 deletions

File tree

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -280,4 +280,4 @@ $RECYCLE.BIN/
280280
.vscode/
281281
docs/_build/
282282
!*.conf.spec
283-
local.meta
283+
**/metadata/local.meta

docker-compose.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ services:
3232

3333
- "./examples/ai_custom_search_app:/opt/splunk/etc/apps/ai_custom_search_app"
3434
- "./examples/ai_custom_alert_app:/opt/splunk/etc/apps/ai_custom_alert_app"
35+
- "./examples/ai_modinput_app:/opt/splunk/etc/apps/ai_modinput_app"
3536

3637
- "./splunklib:/opt/splunk/etc/apps/eventing_app/bin/splunklib"
3738
- "./splunklib:/opt/splunk/etc/apps/generating_app/bin/splunklib"
@@ -42,6 +43,7 @@ services:
4243
- "./splunklib:/opt/splunk/etc/apps/ai_agentic_test_local_tools_app/bin/lib/splunklib"
4344
- "./splunklib:/opt/splunk/etc/apps/ai_custom_search_app/bin/lib/splunklib"
4445
- "./splunklib:/opt/splunk/etc/apps/ai_custom_alert_app/bin/lib/splunklib"
46+
- "./splunklib:/opt/splunk/etc/apps/ai_modinput_app/bin/lib/splunklib"
4547

4648
- "./tests:/opt/splunk/etc/apps/ai_agentic_test_app/bin/lib/tests"
4749
- "./tests:/opt/splunk/etc/apps/ai_agentic_test_local_tools_app/bin/lib/tests"

examples/ai_modinput_app/README.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# AI Modular Input App
2+
3+
## Setup
4+
5+
1. Set `disabled = 0` to enable the modular input in `./local/inputs/inputs.conf`.
6+
2. Restart Splunk.
7+
3. Verify our modular input entry is listed in Splunk Web -> Settings -> Data inputs.
8+
4. Look for the enriched events by searching `index="main" sourcetype="ai_modinput_app:weather"`.
9+
10+
```txt
11+
{
12+
date: 2012-01-04
13+
human_readable: On January 4, 2012, it was rainy with 20.3 mm of precipitation, temperatures ranged from 5.6°C to 12.2°C, and there was a light wind of 4.7 m/s.
14+
It was probably not a great day to go outside for most people, due to the rainy weather.
15+
precipitation: 20.3
16+
temp_max: 12.2
17+
temp_min: 5.6
18+
weather: rain
19+
wind: 4.7
20+
}
21+
```
22+
23+
## Troubleshooting
24+
25+
- See if there are any debug logs from the app
26+
27+
```spl
28+
index="main" sourcetype="ai_modinput_app:debug_log"
29+
```
30+
31+
- See if there's anything about the app in the logs
32+
33+
```spl
34+
index="_internal" ai_modinput_app
35+
```
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[agentic_weather://<name>]
2+
; Path to file to read the weather logs from
3+
csv_file_path = <string>
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
# Copyright 2011-2026 Splunk, Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License"): you may
4+
# not use this file except in compliance with the License. You may obtain
5+
# a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12+
# License for the specific language governing permissions and limitations
13+
# under the License.
14+
15+
import asyncio
16+
import csv
17+
import json
18+
import os
19+
import sys
20+
from _collections_abc import dict_items
21+
from typing import final, override
22+
23+
# ! NOTE: This insert is only needed for splunk-sdk-python CI/CD to work.
24+
# ! Remove this if you're modifying this example locally.
25+
sys.path.insert(0, "/splunklib-deps")
26+
27+
# Include all 3rd party dependencies from <app_name>/bin/lib/
28+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "lib"))
29+
30+
from setup_logging import setup_logging # pyright: ignore[reportImplicitRelativeImport]
31+
32+
from splunklib.ai import OpenAIModel
33+
from splunklib.ai.agent import Agent
34+
from splunklib.ai.messages import HumanMessage
35+
from splunklib.modularinput.argument import Argument
36+
from splunklib.modularinput.event import Event
37+
from splunklib.modularinput.event_writer import EventWriter
38+
from splunklib.modularinput.input_definition import InputDefinition
39+
from splunklib.modularinput.scheme import Scheme
40+
from splunklib.modularinput.script import Script
41+
42+
# BUG: For some reason the process is started with its trust store path overridden with
43+
# one that might not exist on the filesystem. In such case we unset the env, which
44+
# causes the default Certificate Authorities to be used instead.
45+
CA_TRUST_STORE = "/opt/splunk/openssl/cert.pem"
46+
if os.environ.get("SSL_CERT_FILE") == CA_TRUST_STORE and not os.path.exists(
47+
CA_TRUST_STORE
48+
):
49+
del os.environ["SSL_CERT_FILE"]
50+
51+
52+
LLM_MODEL = OpenAIModel(
53+
model="gpt-4o-mini",
54+
base_url="https://api.openai.com/v1",
55+
# To store API keys, consider secret storage:
56+
# https://dev.splunk.com/enterprise/docs/developapps/manageknowledge/secretstorage/secretstoragepython
57+
api_key="<super_secret_key>",
58+
)
59+
60+
APP_NAME = "ai_modinput_app"
61+
logger = setup_logging(APP_NAME)
62+
63+
64+
@final
65+
class AgenticWeatherModInput(Script):
66+
@override
67+
def get_scheme(self) -> Scheme: # pyright: ignore[reportIncompatibleMethodOverride]
68+
scheme = Scheme("Agentic Weather")
69+
70+
csv_file_path = Argument(
71+
name="csv_file_path",
72+
title="CSV file path",
73+
data_type=Argument.data_type_string,
74+
description="Path to file to read the weather logs from",
75+
required_on_create=True,
76+
required_on_edit=True,
77+
)
78+
scheme.add_argument(csv_file_path)
79+
return scheme
80+
81+
@override
82+
def stream_events(self, inputs: InputDefinition, ew: EventWriter) -> None:
83+
input_items: dict_items[str, dict[str, str]] = inputs.inputs.items() # pyright: ignore[reportUnknownVariableType]
84+
for input_name, input_params in input_items:
85+
logger.info(f"Beginning agentic enrichment for {input_name}.")
86+
logger.debug(f"{input_params=}")
87+
88+
csv_file_path = input_params.get("csv_file_path", "")
89+
output_index = input_params.get("index", "")
90+
output_sourcetype = input_params.get("sourcetype", "")
91+
try:
92+
weather_events: list[dict[str, str | int]] = []
93+
with open(csv_file_path) as csv_file:
94+
logger.info(f"Parsing search results from {csv_file_path}")
95+
reader = csv.DictReader(csv_file)
96+
weather_events += list(reader)
97+
98+
for weather_event in weather_events:
99+
weather_event["human_readable"] = asyncio.run(
100+
self.invoke_agent(json.dumps(weather_event))
101+
)
102+
logger.debug(f"{weather_event=}")
103+
104+
event = Event(
105+
stanza=csv_file_path,
106+
index=output_index,
107+
sourcetype=output_sourcetype,
108+
data=json.dumps(weather_event),
109+
)
110+
ew.write_event(event)
111+
except Exception as e:
112+
logger.exception(e, stack_info=True)
113+
114+
logger.debug(f"Finishing enrichment for {input_name} at {csv_file_path}")
115+
116+
async def invoke_agent(self, data_json: str) -> str:
117+
if not self.service:
118+
raise AssertionError("No Splunk connection available")
119+
120+
logger.info(f"Invoking {LLM_MODEL.model} at {LLM_MODEL.base_url}")
121+
async with Agent(
122+
model=LLM_MODEL,
123+
system_prompt="You're an expert meteorologist.",
124+
service=self.service,
125+
) as agent:
126+
prompt = (
127+
f"Parse {data_json=} into a into a short, human-readable sentence. "
128+
+ "Was it a good day to go outside if you're human?"
129+
)
130+
response = await agent.invoke([HumanMessage(role="user", content=prompt)])
131+
logger.debug(f"{response=}")
132+
return response.messages[-1].content
133+
134+
135+
if __name__ == "__main__":
136+
sys.exit(AgenticWeatherModInput().run(sys.argv))
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Copyright © 2011-2026 Splunk, Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License"): you may
4+
# not use this file except in compliance with the License. You may obtain
5+
# a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12+
# License for the specific language governing permissions and limitations
13+
# under the License.
14+
15+
import logging
16+
import logging.handlers
17+
import os
18+
19+
20+
def setup_logging(app_name: str) -> logging.Logger:
21+
"""To see logs from this logger, run this SPL in Splunk:
22+
`index=_internal source="*/<app_name>.log"`"""
23+
SPLUNK_HOME: str = os.environ.get("SPLUNK_HOME", os.path.join("/opt", "splunk"))
24+
LOG_FILE: str = os.path.join(SPLUNK_HOME, "var", "log", "splunk", f"{app_name}.log")
25+
26+
logger = logging.getLogger(app_name)
27+
logger.setLevel(logging.DEBUG)
28+
29+
handler = logging.handlers.RotatingFileHandler(
30+
LOG_FILE, maxBytes=1024 * 1024, backupCount=5
31+
)
32+
handler.setFormatter(
33+
logging.Formatter(f"%(asctime)s %(levelname)s [{app_name}] %(message)s")
34+
)
35+
logger.addHandler(handler)
36+
37+
return logger
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
[id]
2+
name = ai_modinput_app
3+
version = 0.1.0
4+
5+
[package]
6+
id = ai_modinput_app
7+
check_for_updates = False
8+
9+
[install]
10+
is_configured = 0
11+
state = enabled
12+
13+
[ui]
14+
is_visible = 1
15+
label = [EXAMPLE] AI Modular Input App
16+
17+
[launcher]
18+
description = Leverage AI integrations to enrich incoming modular input data
19+
version = 0.1.0
20+
author = Splunk
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
[agentic_weather]
2+
python.required = 3.13
3+
4+
[monitor://$SPLUNK_HOME/var/log/splunk/ai_modinput_app.log]
5+
index = main
6+
sourcetype = ai_modinput_app:debug_log
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
[agentic_weather://weather.csv]
2+
; Set to 0 to enable
3+
disabled = 1
4+
csv_file_path = /opt/splunk/etc/apps/ai_modinput_app/weather.csv
5+
index = main
6+
sourcetype = ai_modinput_app:weather

0 commit comments

Comments
 (0)