loreguard-cli 0.14.1__tar.gz → 0.14.5__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/PKG-INFO +1 -1
- {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/loreguard_entry.py +3 -0
- {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/pyproject.toml +1 -1
- {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/src/intent_classifier.py +38 -23
- {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/src/nli.py +4 -0
- {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/src/tui/screens/main.py +9 -3
- {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/src/tui/screens/running.py +11 -3
- loreguard_cli-0.14.5/tests/test_intent_classifier.py +48 -0
- {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/uv.lock +1 -1
- {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/.claude/skills/llama-cpp-troubleshooting/SKILL.md +0 -0
- {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/.env.example +0 -0
- {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/.github/workflows/release.yml +0 -0
- {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/.gitignore +0 -0
- {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/LICENSE +0 -0
- {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/README.md +0 -0
- {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/THIRD_PARTY_NOTICES.md +0 -0
- {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/loreguard.spec +0 -0
- {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/scripts/build.py +0 -0
- {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/sdk/API.md +0 -0
- {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/sdk/csharp/LoreguardSDK.cs +0 -0
- {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/sdk/gdscript/LoreguardSDK.gd +0 -0
- {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/sdk/javascript/loreguard-sdk.js +0 -0
- {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/sdk/python/loreguard_sdk.py +0 -0
- {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/src/__init__.py +0 -0
- {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/src/__main__.py +0 -0
- {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/src/chunk_detector.py +0 -0
- {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/src/cli.py +0 -0
- {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/src/config.py +0 -0
- {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/src/dialogue_act_classifier.py +0 -0
- {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/src/hf_discovery.py +0 -0
- {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/src/http_server.py +0 -0
- {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/src/llama_server.py +0 -0
- {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/src/llm.py +0 -0
- {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/src/main.py +0 -0
- {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/src/models_registry.py +0 -0
- {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/src/npc_chat.py +0 -0
- {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/src/runtime.py +0 -0
- {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/src/steam.py +0 -0
- {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/src/term_ui.py +0 -0
- {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/src/tui/__init__.py +0 -0
- {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/src/tui/app.py +0 -0
- {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/src/tui/modals/__init__.py +0 -0
- {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/src/tui/modals/auth_menu.py +0 -0
- {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/src/tui/modals/npc_chat.py +0 -0
- {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/src/tui/modals/token_input.py +0 -0
- {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/src/tui/modals/unified_palette.py +0 -0
- {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/src/tui/screens/__init__.py +0 -0
- {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/src/tui/screens/auth.py +0 -0
- {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/src/tui/screens/model_select.py +0 -0
- {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/src/tui/screens/nli_setup.py +0 -0
- {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/src/tui/styles.py +0 -0
- {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/src/tui/widgets/__init__.py +0 -0
- {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/src/tui/widgets/banner.py +0 -0
- {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/src/tui/widgets/footer.py +0 -0
- {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/src/tui/widgets/hardware_info.py +0 -0
- {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/src/tui/widgets/npc_chat.py +0 -0
- {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/src/tui/widgets/server_monitor.py +0 -0
- {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/src/tui/widgets/status_panel.py +0 -0
- {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/src/tunnel.py +0 -0
- {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/src/wizard.py +0 -0
- {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/templates/llama31-no-tools.jinja +0 -0
- {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/tests/test_nli_hhem.py +0 -0
- {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/tests/test_websocket_timeout.py +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Intent Classification service for adaptive retrieval (ADR-0010).
|
|
1
|
+
"""Intent Classification service for adaptive retrieval (ADR-0010).
|
|
2
2
|
|
|
3
3
|
This module provides zero-shot intent classification for the NPC dialogue pipeline.
|
|
4
4
|
It uses DeBERTa-v3-large-zeroshot to classify user messages into retrieval strategy categories:
|
|
@@ -40,16 +40,28 @@ class IntentResult:
|
|
|
40
40
|
# DeBERTa-v3-large is state-of-the-art for zero-shot classification
|
|
41
41
|
DEFAULT_INTENT_MODEL = "MoritzLaurer/DeBERTa-v3-large-zeroshot-v2.0"
|
|
42
42
|
|
|
43
|
-
# Intent
|
|
44
|
-
#
|
|
45
|
-
#
|
|
46
|
-
#
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
43
|
+
# Intent label descriptions for zero-shot classification.
|
|
44
|
+
# Keep these mutually exclusive and concrete to reduce confusion between:
|
|
45
|
+
# - social greetings vs factual requests
|
|
46
|
+
# - working-memory questions vs retrieval questions
|
|
47
|
+
INTENT_LABEL_DESCRIPTIONS = {
|
|
48
|
+
IntentLabel.NO_RETRIEVAL: (
|
|
49
|
+
"a greeting, acknowledgement, farewell, or social small talk "
|
|
50
|
+
"(examples: hi, hello, hey, yo, thanks, okay, bye) without asking "
|
|
51
|
+
"for specific factual information"
|
|
52
|
+
),
|
|
53
|
+
IntentLabel.WORKING_MEMORY: (
|
|
54
|
+
"a question about the NPC's own current state, feelings, recent "
|
|
55
|
+
"experiences, or personal memory that can be answered from working memory"
|
|
56
|
+
),
|
|
57
|
+
IntentLabel.LIGHT_RETRIEVAL: (
|
|
58
|
+
"a request for one specific factual detail (number, fee, date, name, "
|
|
59
|
+
"location, status, or single-file fact) that needs light retrieval"
|
|
60
|
+
),
|
|
61
|
+
IntentLabel.FULL_RETRIEVAL: (
|
|
62
|
+
"a complex request requiring multiple facts, synthesis, planning, "
|
|
63
|
+
"comparison, or multi-step reasoning across sources"
|
|
64
|
+
),
|
|
53
65
|
}
|
|
54
66
|
|
|
55
67
|
# Promise detection hypothesis for follow-up triggers (ADR-0020)
|
|
@@ -156,32 +168,35 @@ class IntentClassifier:
|
|
|
156
168
|
|
|
157
169
|
start_time = time.time()
|
|
158
170
|
|
|
159
|
-
#
|
|
160
|
-
|
|
161
|
-
hypotheses = list(INTENT_HYPOTHESES.values())
|
|
171
|
+
# Candidate labels are descriptive intent classes.
|
|
172
|
+
candidate_labels = list(INTENT_LABEL_DESCRIPTIONS.values())
|
|
162
173
|
|
|
163
|
-
# Run zero-shot classification
|
|
164
|
-
#
|
|
174
|
+
# Run zero-shot classification.
|
|
175
|
+
# Using a consistent hypothesis template tends to be more stable than
|
|
176
|
+
# passing full natural-language hypotheses as labels.
|
|
165
177
|
result = self._classifier(
|
|
166
178
|
query,
|
|
167
|
-
candidate_labels=
|
|
168
|
-
hypothesis_template="{}",
|
|
179
|
+
candidate_labels=candidate_labels,
|
|
180
|
+
hypothesis_template="This user message is {}.",
|
|
169
181
|
multi_label=False,
|
|
170
182
|
)
|
|
171
183
|
|
|
172
184
|
latency_ms = int((time.time() - start_time) * 1000)
|
|
173
185
|
|
|
174
|
-
# Map the winning
|
|
175
|
-
|
|
186
|
+
# Map the winning description back to intent label
|
|
187
|
+
winning_description = result["labels"][0]
|
|
176
188
|
confidence = result["scores"][0]
|
|
177
189
|
|
|
178
|
-
# Find the intent that corresponds to the winning
|
|
190
|
+
# Find the intent that corresponds to the winning description
|
|
179
191
|
intent = IntentLabel.FULL_RETRIEVAL # Default
|
|
180
|
-
for label,
|
|
181
|
-
if
|
|
192
|
+
for label, description in INTENT_LABEL_DESCRIPTIONS.items():
|
|
193
|
+
if description == winning_description:
|
|
182
194
|
intent = label
|
|
183
195
|
break
|
|
184
196
|
|
|
197
|
+
# Log full score distribution for tuning/debugging.
|
|
198
|
+
label_score_pairs = list(zip(result["labels"], result["scores"]))
|
|
199
|
+
logger.debug("Intent score distribution: %s", label_score_pairs)
|
|
185
200
|
logger.info(f"Intent classification: {intent.value} (confidence={confidence:.2f}, latency={latency_ms}ms)")
|
|
186
201
|
|
|
187
202
|
return IntentResult(
|
|
@@ -123,6 +123,10 @@ class NLIService:
|
|
|
123
123
|
self._model_path,
|
|
124
124
|
trust_remote_code=True,
|
|
125
125
|
)
|
|
126
|
+
# HHEMv2 custom class may lack all_tied_weights_keys (needed by
|
|
127
|
+
# newer transformers for .to() / .eval()). Patch if missing.
|
|
128
|
+
if not hasattr(self._model, "_tied_weights_keys"):
|
|
129
|
+
self._model._tied_weights_keys = []
|
|
126
130
|
self._model.to(self._device)
|
|
127
131
|
self._model.eval()
|
|
128
132
|
|
|
@@ -546,11 +546,17 @@ class MainScreen(Screen):
|
|
|
546
546
|
intent_classifier = None
|
|
547
547
|
|
|
548
548
|
# Load dialogue act classifier (filler selection) - run in thread pool
|
|
549
|
-
self._update_status("Loading dialogue act model...", log=False)
|
|
550
|
-
self._log("Loading dialogue act classifier...")
|
|
551
549
|
dialogue_act_classifier = None
|
|
550
|
+
enable_dialogue_act = os.getenv("LOREGUARD_DIALOGUE_ACT_ENABLED", "true").lower() == "true"
|
|
551
|
+
if not enable_dialogue_act:
|
|
552
|
+
self._log("Dialogue act classifier disabled via LOREGUARD_DIALOGUE_ACT_ENABLED")
|
|
553
|
+
else:
|
|
554
|
+
self._update_status("Loading dialogue act model...", log=False)
|
|
555
|
+
self._log("Loading dialogue act classifier...")
|
|
552
556
|
try:
|
|
553
|
-
if
|
|
557
|
+
if not enable_dialogue_act:
|
|
558
|
+
pass # Skip loading
|
|
559
|
+
elif is_dialogue_act_model_available():
|
|
554
560
|
dialogue_act_classifier = DialogueActClassifier()
|
|
555
561
|
loop = asyncio.get_event_loop()
|
|
556
562
|
with concurrent.futures.ThreadPoolExecutor() as pool:
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import concurrent.futures
|
|
5
|
+
import os
|
|
5
6
|
from typing import TYPE_CHECKING, Optional
|
|
6
7
|
|
|
7
8
|
from textual.app import ComposeResult
|
|
@@ -247,10 +248,17 @@ class RunningScreen(Screen):
|
|
|
247
248
|
|
|
248
249
|
# Load Dialogue Act Classifier
|
|
249
250
|
dialogue_act_classifier = None
|
|
250
|
-
|
|
251
|
-
|
|
251
|
+
enable_dialogue_act = os.getenv("LOREGUARD_DIALOGUE_ACT_ENABLED", "true").lower() == "true"
|
|
252
|
+
if not enable_dialogue_act:
|
|
253
|
+
self._update_status("dialogue_act", "Dialogue Act", "Disabled", "info")
|
|
254
|
+
self._log("Dialogue act classifier disabled via LOREGUARD_DIALOGUE_ACT_ENABLED", "info")
|
|
255
|
+
else:
|
|
256
|
+
self._update_status("dialogue_act", "Dialogue Act", "Loading...", "info")
|
|
257
|
+
self._log("Loading Dialogue Act classifier...", "info")
|
|
252
258
|
try:
|
|
253
|
-
if
|
|
259
|
+
if not enable_dialogue_act:
|
|
260
|
+
pass # Skip loading
|
|
261
|
+
elif is_dialogue_act_model_available():
|
|
254
262
|
dialogue_act_classifier = DialogueActClassifier()
|
|
255
263
|
loop = asyncio.get_event_loop()
|
|
256
264
|
with concurrent.futures.ThreadPoolExecutor() as pool:
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from src.intent_classifier import (
|
|
2
|
+
INTENT_LABEL_DESCRIPTIONS,
|
|
3
|
+
IntentClassifier,
|
|
4
|
+
IntentLabel,
|
|
5
|
+
)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class _FakeClassifier:
|
|
9
|
+
def __init__(self, labels, scores):
|
|
10
|
+
self.labels = labels
|
|
11
|
+
self.scores = scores
|
|
12
|
+
self.calls = []
|
|
13
|
+
|
|
14
|
+
def __call__(self, query, **kwargs):
|
|
15
|
+
self.calls.append((query, kwargs))
|
|
16
|
+
return {"labels": self.labels, "scores": self.scores}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_classify_uses_descriptive_labels_and_maps_winner():
|
|
20
|
+
winner = INTENT_LABEL_DESCRIPTIONS[IntentLabel.NO_RETRIEVAL]
|
|
21
|
+
second = INTENT_LABEL_DESCRIPTIONS[IntentLabel.LIGHT_RETRIEVAL]
|
|
22
|
+
fake = _FakeClassifier(labels=[winner, second], scores=[0.88, 0.12])
|
|
23
|
+
|
|
24
|
+
classifier = IntentClassifier(model_path="dummy-model")
|
|
25
|
+
classifier._classifier = fake
|
|
26
|
+
|
|
27
|
+
result = classifier.classify("hi")
|
|
28
|
+
|
|
29
|
+
assert result.intent == IntentLabel.NO_RETRIEVAL
|
|
30
|
+
assert result.confidence == 0.88
|
|
31
|
+
assert result.latency_ms >= 0
|
|
32
|
+
|
|
33
|
+
assert len(fake.calls) == 1
|
|
34
|
+
_, kwargs = fake.calls[0]
|
|
35
|
+
assert kwargs["candidate_labels"] == list(INTENT_LABEL_DESCRIPTIONS.values())
|
|
36
|
+
assert kwargs["hypothesis_template"] == "This user message is {}."
|
|
37
|
+
assert kwargs["multi_label"] is False
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_classify_with_fallback_returns_full_retrieval_on_error():
|
|
41
|
+
classifier = IntentClassifier(model_path="dummy-model")
|
|
42
|
+
classifier._classifier = None
|
|
43
|
+
|
|
44
|
+
result = classifier.classify_with_fallback("hi")
|
|
45
|
+
|
|
46
|
+
assert result.intent == IntentLabel.FULL_RETRIEVAL
|
|
47
|
+
assert result.confidence == 0.0
|
|
48
|
+
assert result.latency_ms == 0
|
{loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/.claude/skills/llama-cpp-troubleshooting/SKILL.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|