smart-artificial-intelligence-local 0.0b7__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.
- smart_artificial_intelligence_local-0.0b7/PKG-INFO +22 -0
- smart_artificial_intelligence_local-0.0b7/README.md +25 -0
- smart_artificial_intelligence_local-0.0b7/pyproject.toml +29 -0
- smart_artificial_intelligence_local-0.0b7/setup.cfg +4 -0
- smart_artificial_intelligence_local-0.0b7/setup.py +30 -0
- smart_artificial_intelligence_local-0.0b7/smart_artificial_intelligence_local/__init__.py +0 -0
- smart_artificial_intelligence_local-0.0b7/smart_artificial_intelligence_local/src/__init__.py +3 -0
- smart_artificial_intelligence_local-0.0b7/smart_artificial_intelligence_local/src/constants.py +28 -0
- smart_artificial_intelligence_local-0.0b7/smart_artificial_intelligence_local/src/smart_artificial_intelligence_local.py +167 -0
- smart_artificial_intelligence_local-0.0b7/smart_artificial_intelligence_local/tests/__init__.py +0 -0
- smart_artificial_intelligence_local-0.0b7/smart_artificial_intelligence_local/tests/smart_artificial_intelligence_local_test.py +128 -0
- smart_artificial_intelligence_local-0.0b7/smart_artificial_intelligence_local.egg-info/PKG-INFO +22 -0
- smart_artificial_intelligence_local-0.0b7/smart_artificial_intelligence_local.egg-info/SOURCES.txt +14 -0
- smart_artificial_intelligence_local-0.0b7/smart_artificial_intelligence_local.egg-info/dependency_links.txt +1 -0
- smart_artificial_intelligence_local-0.0b7/smart_artificial_intelligence_local.egg-info/requires.txt +2 -0
- smart_artificial_intelligence_local-0.0b7/smart_artificial_intelligence_local.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: smart-artificial-intelligence-local
|
|
3
|
+
Version: 0.0b7
|
|
4
|
+
Summary: PyPI smart-artificial-intelligence-local Python Package owned by Circlez.ai
|
|
5
|
+
Home-page: https://github.com/circles-zone/smart-artificial-intelligence-local-python-package
|
|
6
|
+
Author: Circles
|
|
7
|
+
Author-email: info@circlez.ai
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
Requires-Dist: python-sdk-remote
|
|
12
|
+
Requires-Dist: logger-local
|
|
13
|
+
Dynamic: author
|
|
14
|
+
Dynamic: author-email
|
|
15
|
+
Dynamic: classifier
|
|
16
|
+
Dynamic: description
|
|
17
|
+
Dynamic: description-content-type
|
|
18
|
+
Dynamic: home-page
|
|
19
|
+
Dynamic: requires-dist
|
|
20
|
+
Dynamic: summary
|
|
21
|
+
|
|
22
|
+
PyPI smart-artificial-intelligence-local Python Package owned by Circlez.ai
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# General
|
|
2
|
+
|
|
3
|
+
Unlike the README.md in the root directory, this is a README.md of this specific repo/package<br>
|
|
4
|
+
|
|
5
|
+
TODO Please update this README.md of this specific repo/package and not the README.md in the root directory based on the python-package-template repo<br>
|
|
6
|
+
|
|
7
|
+
## TODOs
|
|
8
|
+
|
|
9
|
+
TODO Like smart-datastore package smark-ai should allow to run prompt with multiple LLMs using ai repo instances.<br>
|
|
10
|
+
TODO use API-management to perform API-type, so if an API failed we try other APIs from the same API type.
|
|
11
|
+
TODO use cost allocation tags or equivant to measure the costs.
|
|
12
|
+
|
|
13
|
+
## Versions
|
|
14
|
+
|
|
15
|
+
[pub] 0.0.1 Initial version (change the directories, files, setup.py, .github/workflows/*.yml)<br>
|
|
16
|
+
|
|
17
|
+
## Installing instructions
|
|
18
|
+
|
|
19
|
+
It's advised to use venv<br>
|
|
20
|
+
If you have Windows, it's also advised to use Windows Subsystem for Linux<br>
|
|
21
|
+
|
|
22
|
+
## Install dependencies
|
|
23
|
+
|
|
24
|
+
Run ./download-beta-sdk.sh from the bash terminal<br>
|
|
25
|
+
Run pip install -r requirements.txt from the bash terminal<br>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# This file should be in the future instead of setup.py
|
|
2
|
+
# https://python-poetry.org/docs/pyproject
|
|
3
|
+
# https://stackoverflow.com/questions/78048223/adding-folder-with-data-with-pyproject-toml
|
|
4
|
+
# https://packaging.python.org/en/latest/guides/writing-pyproject-toml/#license
|
|
5
|
+
|
|
6
|
+
# This file is mandatory for the `poetry version patch`
|
|
7
|
+
|
|
8
|
+
# It seems we need to copy this file also to serverless-com repo, as required by dialog-workflow-python-package
|
|
9
|
+
|
|
10
|
+
[build-system]
|
|
11
|
+
requires = ["setuptools>=61.0"]
|
|
12
|
+
build-backend = "setuptools.build_meta"
|
|
13
|
+
|
|
14
|
+
[tool.pytest.ini_options]
|
|
15
|
+
pythonpath = ["."]
|
|
16
|
+
|
|
17
|
+
[tool.poetry]
|
|
18
|
+
name = "smart-artificial-intelligence-local"
|
|
19
|
+
# I believe we are still using the version from setup.py and not from here until Potery will work
|
|
20
|
+
version = "0.0.3" # https://pypi.org/project/smart-artificial-intelligence-local/
|
|
21
|
+
description = "smart-artificial-intelligence-local Python Package"
|
|
22
|
+
readme = "README.md"
|
|
23
|
+
authors = [
|
|
24
|
+
"Circlez.ai <info@circlez.ai>",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[tool.poetry.dev-dependencies]
|
|
28
|
+
pytest = "^8.0"
|
|
29
|
+
pytest-cov = "^5.0"
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import setuptools
|
|
2
|
+
|
|
3
|
+
PACKAGE_NAME = "smart-artificial-intelligence-local"
|
|
4
|
+
|
|
5
|
+
package_dir = PACKAGE_NAME.replace("-", "_")
|
|
6
|
+
|
|
7
|
+
PACKAGE_DESCRIPTION = f"PyPI {PACKAGE_NAME} Python Package owned by Circlez.ai"
|
|
8
|
+
|
|
9
|
+
setuptools.setup(
|
|
10
|
+
name=PACKAGE_NAME,
|
|
11
|
+
version="0.0b7", # https://pypi.org/project/smart-artificial-intelligence-local/
|
|
12
|
+
author="Circles",
|
|
13
|
+
author_email="info@circlez.ai",
|
|
14
|
+
description=PACKAGE_DESCRIPTION,
|
|
15
|
+
long_description=PACKAGE_DESCRIPTION,
|
|
16
|
+
long_description_content_type='text/markdown',
|
|
17
|
+
url=f"https://github.com/circles-zone/{PACKAGE_NAME}-python-package",
|
|
18
|
+
packages=setuptools.find_packages(),
|
|
19
|
+
package_data={package_dir: ['*.py']},
|
|
20
|
+
classifiers=[
|
|
21
|
+
"Programming Language :: Python :: 3",
|
|
22
|
+
"Operating System :: OS Independent",
|
|
23
|
+
],
|
|
24
|
+
# The Smart-LLM reconciler is transport-agnostic: models/judge/reaction-sink are injected
|
|
25
|
+
# callables, so strands/ollama are NOT runtime deps (the orchestrator supplies real models).
|
|
26
|
+
install_requires=[
|
|
27
|
+
'python-sdk-remote',
|
|
28
|
+
'logger-local',
|
|
29
|
+
]
|
|
30
|
+
)
|
|
File without changes
|
smart_artificial_intelligence_local-0.0b7/smart_artificial_intelligence_local/src/constants.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from logger_local.LoggerComponentEnum import LoggerComponentEnum
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ConstantsSrcSmartArtificialIntelligenceLocal:
|
|
7
|
+
"""This is a class of all the constants of SmartArtificialIntelligenceLocal"""
|
|
8
|
+
|
|
9
|
+
DEVELOPER_EMAIL_ADDRESS: str = 'tal.r@circ.zone'
|
|
10
|
+
|
|
11
|
+
SMART_ARTIFICIAL_INTELLIGENCE_LOCAL_PYTHON_CODE_COMPONENT_ID: int = 10105
|
|
12
|
+
|
|
13
|
+
SMART_ARTIFICIAL_INTELLIGENCE_LOCAL_PYTHON_COMPONENT_NAME = 'Smart Artificial Intelligence local Python package'
|
|
14
|
+
# TODO SMART_ARTIFICIAL_INTELLIGENCE_LOCAL_PYTHON_CODE_LOGGER_INIT-OBJECT
|
|
15
|
+
SMART_ARTIFICIAL_INTELLIGENCE_LOCAL_PYTHON_CODE_LOGGER_OBJECT: dict[str, Any] = {
|
|
16
|
+
'component_id': SMART_ARTIFICIAL_INTELLIGENCE_LOCAL_PYTHON_CODE_COMPONENT_ID,
|
|
17
|
+
'component_name': SMART_ARTIFICIAL_INTELLIGENCE_LOCAL_PYTHON_COMPONENT_NAME,
|
|
18
|
+
'component_category': LoggerComponentEnum.ComponentCategory.Code.value,
|
|
19
|
+
'developer_email_address': DEVELOPER_EMAIL_ADDRESS
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
# Smart-LLM orchestration modes (page-2 design).
|
|
23
|
+
MODE_JUDGE: str = 'judge' # Mode 1: run N models, an LLM-as-judge picks the best answer
|
|
24
|
+
MODE_REACTION: str = 'reaction' # Mode 2: run N models, keep all answers + log to reaction store
|
|
25
|
+
MODE_CASCADE: str = 'cascade' # Efficient: cheapest model first; escalate only when unsure
|
|
26
|
+
|
|
27
|
+
# Cascade: escalate to the next model only if the current one's confidence is below this.
|
|
28
|
+
DEFAULT_ESCALATE_CONFIDENCE: float = 0.75
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""Smart-LLM: run a prompt across several models and reconcile -- efficiently.
|
|
2
|
+
|
|
3
|
+
Three modes (page-2 "Analyzer LLM Serving" design):
|
|
4
|
+
|
|
5
|
+
* Mode CASCADE (efficient default): call the CHEAPEST model first; if it is confident, stop -- one
|
|
6
|
+
call. Only when it is unsure do we escalate to the next (different, stronger)
|
|
7
|
+
model; and only when the two DISAGREE do we invoke the judge. Easy lines cost
|
|
8
|
+
one call; the panel + judge fire just for the hard ones.
|
|
9
|
+
* Mode JUDGE: query every model, then an LLM-as-judge picks the best answer set (majority-vote
|
|
10
|
+
fallback when no judge is supplied).
|
|
11
|
+
* Mode REACTION: query every model, keep every answer, log each to a reaction store (injected sink)
|
|
12
|
+
with a flag for the selected one -- signal to improve later.
|
|
13
|
+
|
|
14
|
+
Everything is injected -- a "model" is any ``callable(prompt) -> answer`` where ``answer`` is either a
|
|
15
|
+
``list[str]`` (names) or a ``(list[str], confidence_float)`` tuple (cascade needs the confidence). The
|
|
16
|
+
judge is ``callable(prompt, [(model_name, answer)]) -> list[str]`` and the reaction sink is
|
|
17
|
+
``callable(model_name, prompt, answer, is_selected)``. So this package has no hard dependency on a
|
|
18
|
+
running model / a database: the orchestrator wires real ones, tests wire fakes.
|
|
19
|
+
|
|
20
|
+
After each ``smart_identify`` the ``last_trace`` attribute holds a decision trace (calls made, whether
|
|
21
|
+
it escalated / agreed / used the judge) so the caller can meter cost.
|
|
22
|
+
"""
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import math
|
|
26
|
+
from typing import Callable, List, Optional, Sequence, Tuple
|
|
27
|
+
|
|
28
|
+
from logger_local.MetaLogger import MetaLogger
|
|
29
|
+
|
|
30
|
+
from .constants import ConstantsSrcSmartArtificialIntelligenceLocal as Consts
|
|
31
|
+
|
|
32
|
+
Model = Callable[[str], Sequence]
|
|
33
|
+
Judge = Callable[[str, List[Tuple[str, List[str]]]], Sequence[str]]
|
|
34
|
+
ReactionSink = Callable[[str, str, List[str], bool], None]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _normalize(answer) -> Tuple[List[str], float]:
|
|
38
|
+
"""Coerce a model's return into (names, confidence). A bare list -> confidence 1.0."""
|
|
39
|
+
if (isinstance(answer, tuple) and len(answer) == 2
|
|
40
|
+
and isinstance(answer[1], (int, float)) and not isinstance(answer[1], bool)
|
|
41
|
+
and isinstance(answer[0], (list, tuple))):
|
|
42
|
+
return [str(name) for name in answer[0]], float(answer[1])
|
|
43
|
+
return [str(name) for name in answer], 1.0
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class SmartArtificialIntelligenceLocal( # type: ignore[call-arg] # MetaLogger takes object=
|
|
47
|
+
metaclass=MetaLogger,
|
|
48
|
+
object=Consts.SMART_ARTIFICIAL_INTELLIGENCE_LOCAL_PYTHON_CODE_LOGGER_OBJECT):
|
|
49
|
+
"""Reconcile several models' answers for one prompt (cascade / judge / reaction)."""
|
|
50
|
+
|
|
51
|
+
def __init__(self, models: Optional[Sequence[Model]] = None, judge: Optional[Judge] = None,
|
|
52
|
+
reaction_sink: Optional[ReactionSink] = None,
|
|
53
|
+
escalate_confidence: float = Consts.DEFAULT_ESCALATE_CONFIDENCE,
|
|
54
|
+
is_test_data: bool = False) -> None:
|
|
55
|
+
self._models = list(models or [])
|
|
56
|
+
self._judge = judge
|
|
57
|
+
self._reaction_sink = reaction_sink
|
|
58
|
+
self.escalate_confidence = escalate_confidence
|
|
59
|
+
self.is_test_data = is_test_data
|
|
60
|
+
self.last_trace: dict = {}
|
|
61
|
+
|
|
62
|
+
def smart_identify(self, prompt: str, *, mode: str = Consts.MODE_JUDGE) -> List[str]:
|
|
63
|
+
"""Return the reconciled answer (list of names) for `prompt` across the models."""
|
|
64
|
+
if not self._models:
|
|
65
|
+
self.last_trace = {"calls": 0}
|
|
66
|
+
return []
|
|
67
|
+
if mode == Consts.MODE_CASCADE:
|
|
68
|
+
return self._cascade(prompt)
|
|
69
|
+
return self._panel(prompt, mode)
|
|
70
|
+
|
|
71
|
+
# ---------------------------------------------------------------- cascade (efficient) --
|
|
72
|
+
def _cascade(self, prompt: str) -> List[str]:
|
|
73
|
+
names0, confidence0, name0, ok0 = self._invoke(0, prompt)
|
|
74
|
+
trace: dict = {"calls": 1 if ok0 else 0, "escalated": False, "agreed": None,
|
|
75
|
+
"judged": False, "answered_by": name0}
|
|
76
|
+
|
|
77
|
+
# Tier-1 confident enough (or no second tier) -> stop at one call.
|
|
78
|
+
if ok0 and (confidence0 >= self.escalate_confidence or len(self._models) < 2):
|
|
79
|
+
self.last_trace = trace
|
|
80
|
+
return names0
|
|
81
|
+
|
|
82
|
+
if len(self._models) < 2:
|
|
83
|
+
self.last_trace = trace
|
|
84
|
+
return names0 if ok0 else []
|
|
85
|
+
|
|
86
|
+
# Escalate to a different, stronger model.
|
|
87
|
+
names1, _confidence1, name1, ok1 = self._invoke(1, prompt)
|
|
88
|
+
trace["escalated"] = True
|
|
89
|
+
trace["calls"] += 1 if ok1 else 0
|
|
90
|
+
if not ok1:
|
|
91
|
+
self.last_trace = trace
|
|
92
|
+
return names0 if ok0 else []
|
|
93
|
+
if not ok0:
|
|
94
|
+
self.last_trace = trace
|
|
95
|
+
return names1
|
|
96
|
+
|
|
97
|
+
# Agree -> done (judge skipped). Disagree -> judge / reaction resolves it.
|
|
98
|
+
if set(names0) == set(names1):
|
|
99
|
+
trace["agreed"] = True
|
|
100
|
+
self.last_trace = trace
|
|
101
|
+
return names1
|
|
102
|
+
|
|
103
|
+
trace["agreed"] = False
|
|
104
|
+
answers = [(name0, names0), (name1, names1)]
|
|
105
|
+
if self._judge is not None:
|
|
106
|
+
selected = list(self._judge(prompt, answers))
|
|
107
|
+
trace["judged"] = True
|
|
108
|
+
trace["calls"] += 1
|
|
109
|
+
else:
|
|
110
|
+
selected = self._majority_vote(answers)
|
|
111
|
+
if self._reaction_sink is not None:
|
|
112
|
+
for model_name, answer in answers:
|
|
113
|
+
self._reaction_sink(model_name, prompt, answer, set(answer) == set(selected))
|
|
114
|
+
self.last_trace = trace
|
|
115
|
+
return selected
|
|
116
|
+
|
|
117
|
+
# ------------------------------------------------------------- panel (judge / reaction) --
|
|
118
|
+
def _panel(self, prompt: str, mode: str) -> List[str]:
|
|
119
|
+
answers: List[Tuple[str, List[str]]] = []
|
|
120
|
+
for index in range(len(self._models)):
|
|
121
|
+
names, _confidence, model_name, ok = self._invoke(index, prompt)
|
|
122
|
+
if ok:
|
|
123
|
+
answers.append((model_name, names))
|
|
124
|
+
if not answers:
|
|
125
|
+
self.last_trace = {"calls": 0}
|
|
126
|
+
return []
|
|
127
|
+
|
|
128
|
+
if mode == Consts.MODE_JUDGE and self._judge is not None:
|
|
129
|
+
selected = list(self._judge(prompt, answers))
|
|
130
|
+
judged = True
|
|
131
|
+
else:
|
|
132
|
+
selected = self._majority_vote(answers)
|
|
133
|
+
judged = False
|
|
134
|
+
|
|
135
|
+
if mode == Consts.MODE_REACTION and self._reaction_sink is not None:
|
|
136
|
+
for model_name, answer in answers:
|
|
137
|
+
self._reaction_sink(model_name, prompt, answer, set(answer) == set(selected))
|
|
138
|
+
|
|
139
|
+
self.last_trace = {"calls": len(answers) + (1 if judged else 0),
|
|
140
|
+
"escalated": True, "judged": judged}
|
|
141
|
+
return selected
|
|
142
|
+
|
|
143
|
+
# --------------------------------------------------------------------------- helpers --
|
|
144
|
+
def _invoke(self, index: int, prompt: str) -> Tuple[List[str], float, str, bool]:
|
|
145
|
+
"""Call model `index`; return (names, confidence, name, ok). ok=False if it raised."""
|
|
146
|
+
model = self._models[index]
|
|
147
|
+
model_name = getattr(model, "name", f"model_{index}")
|
|
148
|
+
try:
|
|
149
|
+
names, confidence = _normalize(model(prompt))
|
|
150
|
+
return names, confidence, model_name, True
|
|
151
|
+
except Exception as exception: # noqa: BLE001 -- one bad model must not sink the call
|
|
152
|
+
self.logger.warning(f"model '{model_name}' failed: {exception}") # type: ignore[attr-defined]
|
|
153
|
+
return [], 0.0, model_name, False
|
|
154
|
+
|
|
155
|
+
@staticmethod
|
|
156
|
+
def _majority_vote(answers: List[Tuple[str, List[str]]]) -> List[str]:
|
|
157
|
+
"""Keep every item that at least half of the models returned (union fallback for ties)."""
|
|
158
|
+
threshold = math.ceil(len(answers) / 2)
|
|
159
|
+
counts: dict = {}
|
|
160
|
+
order: List[str] = []
|
|
161
|
+
for _model_name, answer in answers:
|
|
162
|
+
for item in dict.fromkeys(answer): # de-dup within a model
|
|
163
|
+
if item not in counts:
|
|
164
|
+
order.append(item)
|
|
165
|
+
counts[item] = counts.get(item, 0) + 1
|
|
166
|
+
voted = [item for item in order if counts[item] >= threshold]
|
|
167
|
+
return voted or order # if nothing reaches the threshold, return the union
|
smart_artificial_intelligence_local-0.0b7/smart_artificial_intelligence_local/tests/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""Tests for the Smart-LLM multi-model reconciler (judge + reaction), no models/DB required."""
|
|
2
|
+
from smart_artificial_intelligence_local.src.constants import (
|
|
3
|
+
ConstantsSrcSmartArtificialIntelligenceLocal as Consts,
|
|
4
|
+
)
|
|
5
|
+
from smart_artificial_intelligence_local.src.smart_artificial_intelligence_local import (
|
|
6
|
+
SmartArtificialIntelligenceLocal,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _model(name, answer):
|
|
11
|
+
fn = lambda prompt: list(answer) # noqa: E731 -- tiny fake model
|
|
12
|
+
fn.name = name
|
|
13
|
+
return fn
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _cmodel(name, answer, confidence):
|
|
17
|
+
"""A fake model returning (names, confidence) -- the cascade form."""
|
|
18
|
+
fn = lambda prompt: (list(answer), confidence) # noqa: E731
|
|
19
|
+
fn.name = name
|
|
20
|
+
return fn
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _counting(name, answer, confidence):
|
|
24
|
+
"""Like _cmodel but records how many times it was called (to prove no wasted calls)."""
|
|
25
|
+
calls = {"n": 0}
|
|
26
|
+
|
|
27
|
+
def fn(prompt):
|
|
28
|
+
calls["n"] += 1
|
|
29
|
+
return list(answer), confidence
|
|
30
|
+
fn.name = name
|
|
31
|
+
fn.calls = calls
|
|
32
|
+
return fn
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_judge_mode_uses_the_judge_choice():
|
|
36
|
+
models = [_model("a", ["Dave", "Berlin"]), _model("b", ["Dave"])]
|
|
37
|
+
judge = lambda prompt, answers: ["Dave"] # noqa: E731
|
|
38
|
+
smart = SmartArtificialIntelligenceLocal(models=models, judge=judge, is_test_data=True)
|
|
39
|
+
assert smart.smart_identify("p", mode=Consts.MODE_JUDGE) == ["Dave"]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_judge_mode_without_judge_falls_back_to_majority_vote():
|
|
43
|
+
# "Dave" in 2/3 models (kept), "Berlin" in 1/3 (dropped)
|
|
44
|
+
models = [_model("a", ["Dave", "Berlin"]), _model("b", ["Dave"]), _model("c", ["Dave"])]
|
|
45
|
+
smart = SmartArtificialIntelligenceLocal(models=models, is_test_data=True)
|
|
46
|
+
assert smart.smart_identify("p") == ["Dave"]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_reaction_mode_logs_every_answer_with_selection_flag():
|
|
50
|
+
logged = []
|
|
51
|
+
sink = lambda model, prompt, answer, selected: logged.append((model, tuple(answer), selected)) # noqa: E731
|
|
52
|
+
models = [_model("a", ["Dave"]), _model("b", ["Dave"]), _model("b2", ["Nadia"])]
|
|
53
|
+
smart = SmartArtificialIntelligenceLocal(models=models, reaction_sink=sink, is_test_data=True)
|
|
54
|
+
|
|
55
|
+
result = smart.smart_identify("p", mode=Consts.MODE_REACTION)
|
|
56
|
+
assert result == ["Dave"] # majority (2/3)
|
|
57
|
+
assert len(logged) == 3 # one reaction row per model
|
|
58
|
+
assert ("a", ("Dave",), True) in logged # the selected answer is flagged
|
|
59
|
+
assert ("b2", ("Nadia",), False) in logged # a losing answer is flagged False
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_a_failing_model_is_skipped_not_fatal():
|
|
63
|
+
def boom(prompt):
|
|
64
|
+
raise RuntimeError("model down")
|
|
65
|
+
models = [boom, _model("ok", ["Dave"])]
|
|
66
|
+
smart = SmartArtificialIntelligenceLocal(models=models, is_test_data=True)
|
|
67
|
+
assert smart.smart_identify("p") == ["Dave"]
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_no_models_returns_empty():
|
|
71
|
+
assert SmartArtificialIntelligenceLocal(models=[], is_test_data=True).smart_identify("p") == []
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
# --------------------------------------------------------------------- cascade (efficiency) --
|
|
75
|
+
def test_cascade_stops_at_tier1_when_confident():
|
|
76
|
+
tier2 = _counting("cloud", ["Nadia"], 0.9)
|
|
77
|
+
models = [_cmodel("local", ["Dave"], 0.95), tier2]
|
|
78
|
+
smart = SmartArtificialIntelligenceLocal(models=models, escalate_confidence=0.75,
|
|
79
|
+
is_test_data=True)
|
|
80
|
+
result = smart.smart_identify("p", mode=Consts.MODE_CASCADE)
|
|
81
|
+
assert result == ["Dave"]
|
|
82
|
+
assert tier2.calls["n"] == 0 # never escalated -> no second call
|
|
83
|
+
assert smart.last_trace["calls"] == 1
|
|
84
|
+
assert smart.last_trace["escalated"] is False
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def test_cascade_escalates_when_unsure_and_agreement_skips_judge():
|
|
88
|
+
judge_calls = {"n": 0}
|
|
89
|
+
|
|
90
|
+
def judge(prompt, answers):
|
|
91
|
+
judge_calls["n"] += 1
|
|
92
|
+
return ["Dave"]
|
|
93
|
+
models = [_cmodel("local", ["Dave"], 0.40), _cmodel("cloud", ["Dave"], 0.90)]
|
|
94
|
+
smart = SmartArtificialIntelligenceLocal(models=models, judge=judge,
|
|
95
|
+
escalate_confidence=0.75, is_test_data=True)
|
|
96
|
+
result = smart.smart_identify("p", mode=Consts.MODE_CASCADE)
|
|
97
|
+
assert result == ["Dave"]
|
|
98
|
+
assert judge_calls["n"] == 0 # models agreed -> judge NOT invoked
|
|
99
|
+
assert smart.last_trace["escalated"] is True
|
|
100
|
+
assert smart.last_trace["agreed"] is True
|
|
101
|
+
assert smart.last_trace["calls"] == 2
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def test_cascade_invokes_judge_on_disagreement():
|
|
105
|
+
judge_calls = {"n": 0}
|
|
106
|
+
|
|
107
|
+
def judge(prompt, answers):
|
|
108
|
+
judge_calls["n"] += 1
|
|
109
|
+
return ["Nadia"]
|
|
110
|
+
models = [_cmodel("local", ["Dave"], 0.40), _cmodel("cloud", ["Nadia"], 0.90)]
|
|
111
|
+
smart = SmartArtificialIntelligenceLocal(models=models, judge=judge,
|
|
112
|
+
escalate_confidence=0.75, is_test_data=True)
|
|
113
|
+
result = smart.smart_identify("p", mode=Consts.MODE_CASCADE)
|
|
114
|
+
assert result == ["Nadia"]
|
|
115
|
+
assert judge_calls["n"] == 1 # disagreement -> judge decides
|
|
116
|
+
assert smart.last_trace["judged"] is True
|
|
117
|
+
assert smart.last_trace["calls"] == 3
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def test_cascade_logs_reaction_on_disagreement_without_judge():
|
|
121
|
+
logged = []
|
|
122
|
+
sink = lambda m, p, a, s: logged.append((m, tuple(a), s)) # noqa: E731
|
|
123
|
+
models = [_cmodel("local", ["Dave"], 0.40), _cmodel("cloud", ["Nadia"], 0.90)]
|
|
124
|
+
smart = SmartArtificialIntelligenceLocal(models=models, reaction_sink=sink,
|
|
125
|
+
escalate_confidence=0.75, is_test_data=True)
|
|
126
|
+
smart.smart_identify("p", mode=Consts.MODE_CASCADE)
|
|
127
|
+
assert len(logged) == 2 # both tiers logged on disagreement
|
|
128
|
+
assert smart.last_trace["judged"] is False
|
smart_artificial_intelligence_local-0.0b7/smart_artificial_intelligence_local.egg-info/PKG-INFO
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: smart-artificial-intelligence-local
|
|
3
|
+
Version: 0.0b7
|
|
4
|
+
Summary: PyPI smart-artificial-intelligence-local Python Package owned by Circlez.ai
|
|
5
|
+
Home-page: https://github.com/circles-zone/smart-artificial-intelligence-local-python-package
|
|
6
|
+
Author: Circles
|
|
7
|
+
Author-email: info@circlez.ai
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
Requires-Dist: python-sdk-remote
|
|
12
|
+
Requires-Dist: logger-local
|
|
13
|
+
Dynamic: author
|
|
14
|
+
Dynamic: author-email
|
|
15
|
+
Dynamic: classifier
|
|
16
|
+
Dynamic: description
|
|
17
|
+
Dynamic: description-content-type
|
|
18
|
+
Dynamic: home-page
|
|
19
|
+
Dynamic: requires-dist
|
|
20
|
+
Dynamic: summary
|
|
21
|
+
|
|
22
|
+
PyPI smart-artificial-intelligence-local Python Package owned by Circlez.ai
|
smart_artificial_intelligence_local-0.0b7/smart_artificial_intelligence_local.egg-info/SOURCES.txt
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
setup.py
|
|
4
|
+
smart_artificial_intelligence_local/__init__.py
|
|
5
|
+
smart_artificial_intelligence_local.egg-info/PKG-INFO
|
|
6
|
+
smart_artificial_intelligence_local.egg-info/SOURCES.txt
|
|
7
|
+
smart_artificial_intelligence_local.egg-info/dependency_links.txt
|
|
8
|
+
smart_artificial_intelligence_local.egg-info/requires.txt
|
|
9
|
+
smart_artificial_intelligence_local.egg-info/top_level.txt
|
|
10
|
+
smart_artificial_intelligence_local/src/__init__.py
|
|
11
|
+
smart_artificial_intelligence_local/src/constants.py
|
|
12
|
+
smart_artificial_intelligence_local/src/smart_artificial_intelligence_local.py
|
|
13
|
+
smart_artificial_intelligence_local/tests/__init__.py
|
|
14
|
+
smart_artificial_intelligence_local/tests/smart_artificial_intelligence_local_test.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
smart_artificial_intelligence_local-0.0b7/smart_artificial_intelligence_local.egg-info/top_level.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
smart_artificial_intelligence_local
|