test(fusion_accounting_bank_rec): local LLM (LM Studio/Ollama) compat smoke
Tagged 'local_llm'. Auto-detects LM Studio (:1234) or Ollama (:11434) via host.docker.internal or localhost. When running, configures the provider params and runs engine.suggest_matches end-to-end. Skips gracefully when no local LLM is present (CI / dev VM mode). Made-with: Cursor
This commit is contained in:
@@ -22,3 +22,4 @@ from . import test_migration_round_trip
|
|||||||
from . import test_coexistence
|
from . import test_coexistence
|
||||||
from . import test_bank_rec_tours
|
from . import test_bank_rec_tours
|
||||||
from . import test_performance_benchmarks
|
from . import test_performance_benchmarks
|
||||||
|
from . import test_local_llm_compat
|
||||||
|
|||||||
102
fusion_accounting_bank_rec/tests/test_local_llm_compat.py
Normal file
102
fusion_accounting_bank_rec/tests/test_local_llm_compat.py
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
"""Local LLM compatibility test (LM Studio, Ollama, etc.).
|
||||||
|
|
||||||
|
Skips if no local OpenAI-compatible LLM server is reachable. When one is
|
||||||
|
running (LM Studio at :1234, Ollama at :11434), runs an end-to-end:
|
||||||
|
|
||||||
|
1. Configure ``ir.config_parameter`` to point at the local server.
|
||||||
|
2. Trigger ``engine.suggest_matches`` with the 'openai' provider.
|
||||||
|
3. Assert the call did not crash and produced at least one suggestion.
|
||||||
|
|
||||||
|
The smoke is intentionally lenient: local models often emit malformed
|
||||||
|
JSON, in which case ``confidence_scoring`` falls back to statistical-only
|
||||||
|
ranking. We assert end-to-end happiness, not AI re-rank quality.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import socket
|
||||||
|
|
||||||
|
from odoo.tests.common import TransactionCase, tagged
|
||||||
|
|
||||||
|
from . import _factories as f
|
||||||
|
|
||||||
|
|
||||||
|
def _server_reachable(host, port, timeout=1.0):
|
||||||
|
try:
|
||||||
|
with socket.create_connection((host, port), timeout=timeout):
|
||||||
|
return True
|
||||||
|
except (OSError, socket.timeout):
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _detect_local_llm():
|
||||||
|
"""Return (base_url, model_name) tuple, or (None, None) if no server.
|
||||||
|
|
||||||
|
Tries LM Studio (:1234) and Ollama (:11434) on both
|
||||||
|
``host.docker.internal`` (so the container can reach the host) and
|
||||||
|
``localhost`` (so a non-containerised run finds the same servers).
|
||||||
|
"""
|
||||||
|
candidates = (
|
||||||
|
('host.docker.internal', 1234, 'local-model'), # LM Studio
|
||||||
|
('host.docker.internal', 11434, 'llama3.1:8b'), # Ollama
|
||||||
|
('localhost', 1234, 'local-model'),
|
||||||
|
('localhost', 11434, 'llama3.1:8b'),
|
||||||
|
)
|
||||||
|
for host, port, default_model in candidates:
|
||||||
|
if _server_reachable(host, port, timeout=0.5):
|
||||||
|
return (f'http://{host}:{port}/v1', default_model)
|
||||||
|
return (None, None)
|
||||||
|
|
||||||
|
|
||||||
|
@tagged('post_install', '-at_install', 'local_llm')
|
||||||
|
class TestLocalLLMCompat(TransactionCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
self.base_url, self.model = _detect_local_llm()
|
||||||
|
if not self.base_url:
|
||||||
|
self.skipTest(
|
||||||
|
"No local LLM server detected "
|
||||||
|
"(LM Studio :1234 / Ollama :11434)")
|
||||||
|
|
||||||
|
def test_suggest_matches_with_local_llm(self):
|
||||||
|
params = self.env['ir.config_parameter'].sudo()
|
||||||
|
prior = {
|
||||||
|
'fusion_accounting.openai_base_url': params.get_param(
|
||||||
|
'fusion_accounting.openai_base_url'),
|
||||||
|
'fusion_accounting.openai_model': params.get_param(
|
||||||
|
'fusion_accounting.openai_model'),
|
||||||
|
'fusion_accounting.openai_api_key': params.get_param(
|
||||||
|
'fusion_accounting.openai_api_key'),
|
||||||
|
'fusion_accounting.provider.bank_rec_suggest': params.get_param(
|
||||||
|
'fusion_accounting.provider.bank_rec_suggest'),
|
||||||
|
}
|
||||||
|
|
||||||
|
params.set_param('fusion_accounting.openai_base_url', self.base_url)
|
||||||
|
params.set_param('fusion_accounting.openai_model', self.model)
|
||||||
|
# Local servers ignore the key but the adapter requires *some* value.
|
||||||
|
params.set_param('fusion_accounting.openai_api_key', 'lm-studio')
|
||||||
|
params.set_param(
|
||||||
|
'fusion_accounting.provider.bank_rec_suggest', 'openai')
|
||||||
|
|
||||||
|
try:
|
||||||
|
partner = self.env['res.partner'].create(
|
||||||
|
{'name': 'Local LLM Partner'})
|
||||||
|
f.make_invoice(self.env, partner=partner, amount=750)
|
||||||
|
bank_line = f.make_bank_line(
|
||||||
|
self.env, amount=750, partner=partner,
|
||||||
|
memo='REF 12345 Local LLM test')
|
||||||
|
|
||||||
|
result = self.env['fusion.reconcile.engine'].suggest_matches(
|
||||||
|
bank_line, limit_per_line=3)
|
||||||
|
|
||||||
|
self.assertIn(bank_line.id, result)
|
||||||
|
suggestions = self.env['fusion.reconcile.suggestion'].search([
|
||||||
|
('statement_line_id', '=', bank_line.id),
|
||||||
|
])
|
||||||
|
self.assertGreater(
|
||||||
|
len(suggestions), 0,
|
||||||
|
"Local LLM run should still produce at least one suggestion "
|
||||||
|
"(statistical fallback if AI re-rank fails)")
|
||||||
|
finally:
|
||||||
|
for key, value in prior.items():
|
||||||
|
if value is not None:
|
||||||
|
params.set_param(key, value)
|
||||||
Reference in New Issue
Block a user