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.
Files changed (63) hide show
  1. {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/PKG-INFO +1 -1
  2. {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/loreguard_entry.py +3 -0
  3. {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/pyproject.toml +1 -1
  4. {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/src/intent_classifier.py +38 -23
  5. {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/src/nli.py +4 -0
  6. {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/src/tui/screens/main.py +9 -3
  7. {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/src/tui/screens/running.py +11 -3
  8. loreguard_cli-0.14.5/tests/test_intent_classifier.py +48 -0
  9. {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/uv.lock +1 -1
  10. {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/.claude/skills/llama-cpp-troubleshooting/SKILL.md +0 -0
  11. {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/.env.example +0 -0
  12. {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/.github/workflows/release.yml +0 -0
  13. {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/.gitignore +0 -0
  14. {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/LICENSE +0 -0
  15. {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/README.md +0 -0
  16. {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/THIRD_PARTY_NOTICES.md +0 -0
  17. {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/loreguard.spec +0 -0
  18. {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/scripts/build.py +0 -0
  19. {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/sdk/API.md +0 -0
  20. {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/sdk/csharp/LoreguardSDK.cs +0 -0
  21. {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/sdk/gdscript/LoreguardSDK.gd +0 -0
  22. {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/sdk/javascript/loreguard-sdk.js +0 -0
  23. {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/sdk/python/loreguard_sdk.py +0 -0
  24. {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/src/__init__.py +0 -0
  25. {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/src/__main__.py +0 -0
  26. {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/src/chunk_detector.py +0 -0
  27. {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/src/cli.py +0 -0
  28. {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/src/config.py +0 -0
  29. {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/src/dialogue_act_classifier.py +0 -0
  30. {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/src/hf_discovery.py +0 -0
  31. {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/src/http_server.py +0 -0
  32. {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/src/llama_server.py +0 -0
  33. {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/src/llm.py +0 -0
  34. {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/src/main.py +0 -0
  35. {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/src/models_registry.py +0 -0
  36. {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/src/npc_chat.py +0 -0
  37. {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/src/runtime.py +0 -0
  38. {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/src/steam.py +0 -0
  39. {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/src/term_ui.py +0 -0
  40. {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/src/tui/__init__.py +0 -0
  41. {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/src/tui/app.py +0 -0
  42. {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/src/tui/modals/__init__.py +0 -0
  43. {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/src/tui/modals/auth_menu.py +0 -0
  44. {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/src/tui/modals/npc_chat.py +0 -0
  45. {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/src/tui/modals/token_input.py +0 -0
  46. {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/src/tui/modals/unified_palette.py +0 -0
  47. {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/src/tui/screens/__init__.py +0 -0
  48. {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/src/tui/screens/auth.py +0 -0
  49. {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/src/tui/screens/model_select.py +0 -0
  50. {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/src/tui/screens/nli_setup.py +0 -0
  51. {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/src/tui/styles.py +0 -0
  52. {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/src/tui/widgets/__init__.py +0 -0
  53. {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/src/tui/widgets/banner.py +0 -0
  54. {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/src/tui/widgets/footer.py +0 -0
  55. {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/src/tui/widgets/hardware_info.py +0 -0
  56. {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/src/tui/widgets/npc_chat.py +0 -0
  57. {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/src/tui/widgets/server_monitor.py +0 -0
  58. {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/src/tui/widgets/status_panel.py +0 -0
  59. {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/src/tunnel.py +0 -0
  60. {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/src/wizard.py +0 -0
  61. {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/templates/llama31-no-tools.jinja +0 -0
  62. {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/tests/test_nli_hhem.py +0 -0
  63. {loreguard_cli-0.14.1 → loreguard_cli-0.14.5}/tests/test_websocket_timeout.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: loreguard-cli
3
- Version: 0.14.1
3
+ Version: 0.14.5
4
4
  Summary: Local inference client for Loreguard NPCs
5
5
  Project-URL: Homepage, https://loreguard.com
6
6
  Project-URL: Documentation, https://github.com/beyond-logic-labs/loreguard-cli#readme
@@ -3,6 +3,9 @@
3
3
 
4
4
  Imports src as a proper package so relative imports inside src/__main__.py work.
5
5
  """
6
+ import multiprocessing
7
+ multiprocessing.freeze_support() # Required for PyInstaller on macOS/Windows
8
+
6
9
  from src.__main__ import main
7
10
 
8
11
  main()
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "loreguard-cli"
7
- version = "0.14.1"
7
+ version = "0.14.5"
8
8
  description = "Local inference client for Loreguard NPCs"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -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 hypothesis templates for zero-shot classification
44
- # Each intent maps to a hypothesis that DeBERTa will evaluate
45
- # NOTE: Hypotheses must be specific to avoid misclassification of mixed-intent messages
46
- # (e.g., "hey, how are you? what's the ISP fee?" should match LIGHT_RETRIEVAL, not WORKING_MEMORY)
47
- # TODO: Move hypotheses to backend for centralized control (see loreguard-engine issue)
48
- INTENT_HYPOTHESES = {
49
- IntentLabel.NO_RETRIEVAL: "This is a greeting, farewell, or a vague question about availability without asking for any specific information.",
50
- IntentLabel.WORKING_MEMORY: "This asks about the person's current life, recent experiences, what they've been up to, or how things are going for them.",
51
- IntentLabel.LIGHT_RETRIEVAL: "This asks for a specific fact, number, price, fee, date, location, or procedure that requires looking up information.",
52
- IntentLabel.FULL_RETRIEVAL: "This is a complex question requiring analysis of multiple topics or understanding relationships between different pieces of information.",
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
- # Get candidate labels and hypotheses
160
- labels = list(INTENT_HYPOTHESES.keys())
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
- # The pipeline will evaluate each hypothesis against the query
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=hypotheses,
168
- hypothesis_template="{}", # Use hypotheses directly
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 hypothesis back to intent label
175
- winning_hypothesis = result["labels"][0]
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 hypothesis
190
+ # Find the intent that corresponds to the winning description
179
191
  intent = IntentLabel.FULL_RETRIEVAL # Default
180
- for label, hypothesis in INTENT_HYPOTHESES.items():
181
- if hypothesis == winning_hypothesis:
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 is_dialogue_act_model_available():
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
- self._update_status("dialogue_act", "Dialogue Act", "Loading...", "info")
251
- self._log("Loading Dialogue Act classifier...", "info")
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 is_dialogue_act_model_available():
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
@@ -600,7 +600,7 @@ wheels = [
600
600
 
601
601
  [[package]]
602
602
  name = "loreguard-cli"
603
- version = "0.14.0"
603
+ version = "0.14.5"
604
604
  source = { editable = "." }
605
605
  dependencies = [
606
606
  { name = "aiofiles" },
File without changes
File without changes