Skip to content

Commit 1e12d6b

Browse files
committed
V1.1.1 Update
1 parent 8386ba2 commit 1e12d6b

25 files changed

+2877
-484
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<img src="images/AIFT Logo - White Text.png" alt="AIFT Logo" width="400">
33
</p>
44

5-
# AIFT — AI Forensic Triage V1.1
5+
# AIFT — AI Forensic Triage V1.1.1
66

77
**Automated Windows forensic triage, powered by AI.**
88

app/ai_providers.py

Lines changed: 102 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -99,23 +99,55 @@ def analyze_with_attachments(
9999
max_tokens=max_tokens,
100100
)
101101

102+
def _prepare_csv_attachments(
103+
self,
104+
attachments: list[Mapping[str, str]] | None,
105+
*,
106+
supports_file_attachments: bool = True,
107+
) -> list[dict[str, str]] | None:
108+
"""Apply shared CSV-attachment preflight checks and normalization."""
109+
if not bool(getattr(self, "attach_csv_as_file", False)):
110+
return None
111+
if not attachments:
112+
return None
113+
if getattr(self, "_csv_attachment_supported", None) is False:
114+
return None
115+
if not supports_file_attachments:
116+
if hasattr(self, "_csv_attachment_supported"):
117+
setattr(self, "_csv_attachment_supported", False)
118+
return None
102119

103-
def _resolve_api_key(config_key: str, env_var: str) -> str:
120+
normalized_attachments = _normalize_attachment_inputs(attachments)
121+
if not normalized_attachments:
122+
return None
123+
return normalized_attachments
124+
125+
126+
def _normalize_api_key_value(value: Any) -> str:
127+
"""Normalize API key-like values from config/env sources."""
128+
if value is None:
129+
return ""
130+
return str(value).strip()
131+
132+
133+
def _resolve_api_key(config_key: Any, env_var: str) -> str:
104134
"""Return the API key from config, falling back to an environment variable."""
105-
if config_key:
106-
return config_key
107-
return os.environ.get(env_var, "")
135+
normalized_config_key = _normalize_api_key_value(config_key)
136+
if normalized_config_key:
137+
return normalized_config_key
138+
return _normalize_api_key_value(os.environ.get(env_var, ""))
108139

109140

110-
def _resolve_api_key_candidates(config_key: str, env_vars: tuple[str, ...]) -> str:
141+
def _resolve_api_key_candidates(config_key: Any, env_vars: tuple[str, ...]) -> str:
111142
"""Return API key from config, falling back across multiple environment variables."""
112-
if config_key:
113-
return config_key
143+
normalized_config_key = _normalize_api_key_value(config_key)
144+
if normalized_config_key:
145+
return normalized_config_key
114146

115147
for env_var in env_vars:
116-
value = os.environ.get(env_var, "")
117-
if value:
118-
return value
148+
normalized_value = _normalize_api_key_value(os.environ.get(env_var, ""))
149+
if normalized_value:
150+
return normalized_value
119151
return ""
120152

121153

@@ -660,18 +692,19 @@ def __init__(
660692
"anthropic SDK is not installed. Install it with `pip install anthropic`."
661693
) from error
662694

663-
if not api_key:
695+
normalized_api_key = _normalize_api_key_value(api_key)
696+
if not normalized_api_key:
664697
raise AIProviderError(
665698
"Claude API key is not configured. "
666699
"Set `ai.claude.api_key` in config.yaml or the ANTHROPIC_API_KEY environment variable."
667700
)
668701

669702
self._anthropic = anthropic
670-
self.api_key = api_key
703+
self.api_key = normalized_api_key
671704
self.model = model
672705
self.attach_csv_as_file = bool(attach_csv_as_file)
673706
self._csv_attachment_supported: bool | None = None
674-
self.client = anthropic.Anthropic(api_key=api_key)
707+
self.client = anthropic.Anthropic(api_key=normalized_api_key)
675708
logger.info("Initialized Claude provider with model %s", model)
676709

677710
def analyze(
@@ -751,14 +784,7 @@ def _request_with_csv_attachments(
751784
max_tokens: int,
752785
attachments: list[Mapping[str, str]] | None,
753786
) -> str | None:
754-
if not self.attach_csv_as_file:
755-
return None
756-
if not attachments:
757-
return None
758-
if self._csv_attachment_supported is False:
759-
return None
760-
761-
normalized_attachments = _normalize_attachment_inputs(attachments)
787+
normalized_attachments = self._prepare_csv_attachments(attachments)
762788
if not normalized_attachments:
763789
return None
764790

@@ -905,18 +931,19 @@ def __init__(
905931
"openai SDK is not installed. Install it with `pip install openai`."
906932
) from error
907933

908-
if not api_key:
934+
normalized_api_key = _normalize_api_key_value(api_key)
935+
if not normalized_api_key:
909936
raise AIProviderError(
910937
"OpenAI API key is not configured. "
911938
"Set `ai.openai.api_key` in config.yaml or the OPENAI_API_KEY environment variable."
912939
)
913940

914941
self._openai = openai
915-
self.api_key = api_key
942+
self.api_key = normalized_api_key
916943
self.model = model
917944
self.attach_csv_as_file = bool(attach_csv_as_file)
918945
self._csv_attachment_supported: bool | None = None
919-
self.client = openai.OpenAI(api_key=api_key)
946+
self.client = openai.OpenAI(api_key=normalized_api_key)
920947
logger.info("Initialized OpenAI provider with model %s", model)
921948

922949
def analyze(
@@ -1065,17 +1092,10 @@ def _request_with_csv_attachments(
10651092
max_tokens: int,
10661093
attachments: list[Mapping[str, str]] | None,
10671094
) -> str | None:
1068-
if not self.attach_csv_as_file:
1069-
return None
1070-
if not attachments:
1071-
return None
1072-
if self._csv_attachment_supported is False:
1073-
return None
1074-
if not hasattr(self.client, "files") or not hasattr(self.client, "responses"):
1075-
self._csv_attachment_supported = False
1076-
return None
1077-
1078-
normalized_attachments = _normalize_attachment_inputs(attachments)
1095+
normalized_attachments = self._prepare_csv_attachments(
1096+
attachments,
1097+
supports_file_attachments=hasattr(self.client, "files") and hasattr(self.client, "responses"),
1098+
)
10791099
if not normalized_attachments:
10801100
return None
10811101

@@ -1176,22 +1196,23 @@ def __init__(
11761196
"openai SDK is not installed. Install it with `pip install openai`."
11771197
) from error
11781198

1179-
if not api_key:
1199+
normalized_api_key = _normalize_api_key_value(api_key)
1200+
if not normalized_api_key:
11801201
raise AIProviderError(
11811202
"Kimi API key is not configured. "
11821203
"Set `ai.kimi.api_key` in config.yaml or the MOONSHOT_API_KEY environment variable."
11831204
)
11841205

11851206
self._openai = openai
1186-
self.api_key = api_key
1207+
self.api_key = normalized_api_key
11871208
self.model = _normalize_kimi_model_name(model)
11881209
self.base_url = _normalize_openai_compatible_base_url(
11891210
base_url=base_url,
11901211
default_base_url=DEFAULT_KIMI_BASE_URL,
11911212
)
11921213
self.attach_csv_as_file = bool(attach_csv_as_file)
11931214
self._csv_attachment_supported: bool | None = None
1194-
self.client = openai.OpenAI(api_key=api_key, base_url=self.base_url)
1215+
self.client = openai.OpenAI(api_key=normalized_api_key, base_url=self.base_url)
11951216
logger.info("Initialized Kimi provider at %s with model %s", self.base_url, self.model)
11961217

11971218
def analyze(
@@ -1295,17 +1316,10 @@ def _request_with_csv_attachments(
12951316
max_tokens: int,
12961317
attachments: list[Mapping[str, str]] | None,
12971318
) -> str | None:
1298-
if not self.attach_csv_as_file:
1299-
return None
1300-
if not attachments:
1301-
return None
1302-
if self._csv_attachment_supported is False:
1303-
return None
1304-
if not hasattr(self.client, "files") or not hasattr(self.client, "responses"):
1305-
self._csv_attachment_supported = False
1306-
return None
1307-
1308-
normalized_attachments = _normalize_attachment_inputs(attachments)
1319+
normalized_attachments = self._prepare_csv_attachments(
1320+
attachments,
1321+
supports_file_attachments=hasattr(self.client, "files") and hasattr(self.client, "responses"),
1322+
)
13091323
if not normalized_attachments:
13101324
return None
13111325

@@ -1381,16 +1395,18 @@ def __init__(
13811395
"openai SDK is not installed. Install it with `pip install openai`."
13821396
) from error
13831397

1398+
normalized_api_key = _normalize_api_key_value(api_key) or "not-needed"
1399+
13841400
self._openai = openai
13851401
self.base_url = _normalize_openai_compatible_base_url(
13861402
base_url=base_url,
13871403
default_base_url=DEFAULT_LOCAL_BASE_URL,
13881404
)
13891405
self.model = model
1390-
self.api_key = api_key
1406+
self.api_key = normalized_api_key
13911407
self.attach_csv_as_file = bool(attach_csv_as_file)
13921408
self._csv_attachment_supported: bool | None = None
1393-
self.client = openai.OpenAI(api_key=api_key, base_url=self.base_url)
1409+
self.client = openai.OpenAI(api_key=normalized_api_key, base_url=self.base_url)
13941410
logger.info("Initialized local provider at %s with model %s", self.base_url, model)
13951411

13961412
def analyze(
@@ -1486,13 +1502,18 @@ def _request() -> str:
14861502
cleaned_attachment_response = _strip_leading_reasoning_blocks(attachment_response)
14871503
return cleaned_attachment_response or attachment_response.strip()
14881504

1505+
prompt_for_completion = self._build_chat_completion_prompt(
1506+
user_prompt=user_prompt,
1507+
attachments=attachments,
1508+
)
1509+
14891510
try:
14901511
stream = self.client.chat.completions.create(
14911512
model=self.model,
14921513
max_tokens=max_tokens,
14931514
messages=[
14941515
{"role": "system", "content": system_prompt},
1495-
{"role": "user", "content": user_prompt},
1516+
{"role": "user", "content": prompt_for_completion},
14961517
],
14971518
stream=True,
14981519
)
@@ -1611,12 +1632,17 @@ def _request_non_stream(
16111632
return cleaned_attachment_response
16121633
return attachment_response.strip()
16131634

1635+
prompt_for_completion = self._build_chat_completion_prompt(
1636+
user_prompt=user_prompt,
1637+
attachments=attachments,
1638+
)
1639+
16141640
response = self.client.chat.completions.create(
16151641
model=self.model,
16161642
max_tokens=max_tokens,
16171643
messages=[
16181644
{"role": "system", "content": system_prompt},
1619-
{"role": "user", "content": user_prompt},
1645+
{"role": "user", "content": prompt_for_completion},
16201646
],
16211647
)
16221648
text = _extract_openai_text(response)
@@ -1639,24 +1665,32 @@ def _request_non_stream(
16391665
f"{reason_detail}. This can happen with reasoning-only outputs or very low token limits."
16401666
)
16411667

1668+
def _build_chat_completion_prompt(
1669+
self,
1670+
user_prompt: str,
1671+
attachments: list[Mapping[str, str]] | None,
1672+
) -> str:
1673+
prompt_for_completion = user_prompt
1674+
if attachments and self.attach_csv_as_file:
1675+
prompt_for_completion, inlined_attachment_data = _inline_attachment_data_into_prompt(
1676+
user_prompt=user_prompt,
1677+
attachments=attachments,
1678+
)
1679+
if inlined_attachment_data:
1680+
logger.info("Local attachment fallback inlined attachment data into prompt.")
1681+
return prompt_for_completion
1682+
16421683
def _request_with_csv_attachments(
16431684
self,
16441685
system_prompt: str,
16451686
user_prompt: str,
16461687
max_tokens: int,
16471688
attachments: list[Mapping[str, str]] | None,
16481689
) -> str | None:
1649-
if not self.attach_csv_as_file:
1650-
return None
1651-
if not attachments:
1652-
return None
1653-
if self._csv_attachment_supported is False:
1654-
return None
1655-
if not hasattr(self.client, "files") or not hasattr(self.client, "responses"):
1656-
self._csv_attachment_supported = False
1657-
return None
1658-
1659-
normalized_attachments = _normalize_attachment_inputs(attachments)
1690+
normalized_attachments = self._prepare_csv_attachments(
1691+
attachments,
1692+
supports_file_attachments=hasattr(self.client, "files") and hasattr(self.client, "responses"),
1693+
)
16601694
if not normalized_attachments:
16611695
return None
16621696

@@ -1728,7 +1762,7 @@ def create_provider(config: dict[str, Any]) -> AIProvider:
17281762
if not isinstance(claude_config, dict):
17291763
raise ValueError("Invalid configuration: `ai.claude` must be a dictionary.")
17301764
api_key = _resolve_api_key(
1731-
str(claude_config.get("api_key", "")),
1765+
claude_config.get("api_key", ""),
17321766
"ANTHROPIC_API_KEY",
17331767
)
17341768
return ClaudeProvider(
@@ -1742,7 +1776,7 @@ def create_provider(config: dict[str, Any]) -> AIProvider:
17421776
if not isinstance(openai_config, dict):
17431777
raise ValueError("Invalid configuration: `ai.openai` must be a dictionary.")
17441778
api_key = _resolve_api_key(
1745-
str(openai_config.get("api_key", "")),
1779+
openai_config.get("api_key", ""),
17461780
"OPENAI_API_KEY",
17471781
)
17481782
return OpenAIProvider(
@@ -1758,7 +1792,7 @@ def create_provider(config: dict[str, Any]) -> AIProvider:
17581792
return LocalProvider(
17591793
base_url=str(local_config.get("base_url", DEFAULT_LOCAL_BASE_URL)),
17601794
model=str(local_config.get("model", DEFAULT_LOCAL_MODEL)),
1761-
api_key=str(local_config.get("api_key", "not-needed")),
1795+
api_key=_normalize_api_key_value(local_config.get("api_key", "not-needed")) or "not-needed",
17621796
attach_csv_as_file=bool(local_config.get("attach_csv_as_file", True)),
17631797
)
17641798

@@ -1767,7 +1801,7 @@ def create_provider(config: dict[str, Any]) -> AIProvider:
17671801
if not isinstance(kimi_config, dict):
17681802
raise ValueError("Invalid configuration: `ai.kimi` must be a dictionary.")
17691803
api_key = _resolve_api_key_candidates(
1770-
str(kimi_config.get("api_key", "")),
1804+
kimi_config.get("api_key", ""),
17711805
("MOONSHOT_API_KEY", "KIMI_API_KEY"),
17721806
)
17731807
return KimiProvider(

0 commit comments

Comments
 (0)