idc-wrapper 0.2.0__py3-none-any.whl
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.
- idc/__init__.py +25 -0
- idc/agent.py +143 -0
- idc/journal.py +48 -0
- idc/langgraph_adapter.py +390 -0
- idc/mock_backend.py +145 -0
- idc/test_agent.py +49 -0
- idc/test_langgraph_adapter.py +109 -0
- idc_agent.py +139 -0
- idc_wrapper-0.2.0.dist-info/METADATA +261 -0
- idc_wrapper-0.2.0.dist-info/RECORD +14 -0
- idc_wrapper-0.2.0.dist-info/WHEEL +5 -0
- idc_wrapper-0.2.0.dist-info/entry_points.txt +2 -0
- idc_wrapper-0.2.0.dist-info/licenses/LICENSE +21 -0
- idc_wrapper-0.2.0.dist-info/top_level.txt +2 -0
idc/__init__.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""IDC — Effondrement Décisionnel Irréversible (Irreversible Decision Collapse).
|
|
2
|
+
|
|
3
|
+
Package portable (stdlib) pour envelopper un LLM avec ressource finie,
|
|
4
|
+
journal append-only et règle d'arrêt.
|
|
5
|
+
|
|
6
|
+
L'adaptateur LangGraph-shaped vit dans `idc.langgraph_adapter` (pas d'import
|
|
7
|
+
lourd ici) : `IdcGraphRunner` fonctionne sans langgraph ;
|
|
8
|
+
`compile_langgraph` requiert l'extra optionnel `[langgraph]`.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from .journal import AppendOnlyJournal, JournalEntry
|
|
12
|
+
from .agent import IdcAgent, IdcRunResult, Backend
|
|
13
|
+
from .mock_backend import MockBackend, MockScenario
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"AppendOnlyJournal",
|
|
17
|
+
"JournalEntry",
|
|
18
|
+
"IdcAgent",
|
|
19
|
+
"IdcRunResult",
|
|
20
|
+
"Backend",
|
|
21
|
+
"MockBackend",
|
|
22
|
+
"MockScenario",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
__version__ = "0.2.0"
|
idc/agent.py
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""Agent IDC : ressource finie, journal, règle d'arrêt."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import Callable, Protocol
|
|
7
|
+
|
|
8
|
+
from .journal import AppendOnlyJournal, JournalEntry
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Backend(Protocol):
|
|
12
|
+
def generate(self, prompt: str, max_tokens: int = 120) -> str: ...
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
GainFn = Callable[[int, str], float]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def default_gain(step: int, chunk: str) -> float:
|
|
19
|
+
del step
|
|
20
|
+
return 25.0 if len(chunk) > 20 else 8.0
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
IGNORANCE_MSG = "Je ne sais pas (ressource insuffisante ou incertitude trop élevée)."
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class IdcRunResult:
|
|
28
|
+
answer: str
|
|
29
|
+
steps: int
|
|
30
|
+
resource_final: float
|
|
31
|
+
tokens_approx_total: int
|
|
32
|
+
cost_total: float
|
|
33
|
+
stopped_early: bool
|
|
34
|
+
admitted_ignorance: bool
|
|
35
|
+
journal: AppendOnlyJournal
|
|
36
|
+
mode: str # "idc" | "baseline"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class IdcAgent:
|
|
41
|
+
backend: Backend
|
|
42
|
+
resource: float = 100.0
|
|
43
|
+
critical_threshold: float = 15.0
|
|
44
|
+
cost_per_token: float = 0.8
|
|
45
|
+
max_steps: int = 8
|
|
46
|
+
journal: AppendOnlyJournal = field(default_factory=AppendOnlyJournal)
|
|
47
|
+
gain_fn: GainFn = field(default=default_gain)
|
|
48
|
+
use_stop_rule: bool = True
|
|
49
|
+
# Sous IDC : si aucun chunk non vide n'a dépassé le seuil de gain « utile »,
|
|
50
|
+
# préférer l'aveu d'ignorance à une réponse creuse.
|
|
51
|
+
prefer_ignorance_if_no_useful: bool = True
|
|
52
|
+
useful_gain_floor: float = 40.0 # aligné sur cost_per_token*50 par défaut
|
|
53
|
+
|
|
54
|
+
def still_worth_it(self, estimated_gain: float) -> bool:
|
|
55
|
+
marginal_cost = self.cost_per_token * 50.0
|
|
56
|
+
risk = 2.0 if self.resource < 30.0 else 1.0
|
|
57
|
+
return estimated_gain > marginal_cost * risk
|
|
58
|
+
|
|
59
|
+
def run(self, task: str) -> IdcRunResult:
|
|
60
|
+
current = ""
|
|
61
|
+
step = 0
|
|
62
|
+
tokens_total = 0
|
|
63
|
+
cost_total = 0.0
|
|
64
|
+
stopped_early = False
|
|
65
|
+
max_gain_seen = 0.0
|
|
66
|
+
any_useful = False
|
|
67
|
+
|
|
68
|
+
while self.resource > self.critical_threshold and step < self.max_steps:
|
|
69
|
+
step += 1
|
|
70
|
+
context = (
|
|
71
|
+
f"Journal des engagements précédents :\n{self.journal.as_context()}\n\n"
|
|
72
|
+
f"Tâche : {task}\n\n"
|
|
73
|
+
f"Réponse partielle actuelle :\n{current}\n\n"
|
|
74
|
+
"Continue ou affine la réponse. Sois concis."
|
|
75
|
+
)
|
|
76
|
+
chunk = self.backend.generate(context, max_tokens=120)
|
|
77
|
+
tokens_approx = max(len(chunk.split()), 1) # tentative minimale
|
|
78
|
+
chunk_display = chunk.strip() if chunk.strip() else "(vide / incertitude)"
|
|
79
|
+
|
|
80
|
+
cost = tokens_approx * self.cost_per_token
|
|
81
|
+
self.resource = max(0.0, self.resource - cost)
|
|
82
|
+
tokens_total += tokens_approx
|
|
83
|
+
cost_total += cost
|
|
84
|
+
estimated_gain = self.gain_fn(step, chunk)
|
|
85
|
+
max_gain_seen = max(max_gain_seen, estimated_gain)
|
|
86
|
+
if chunk.strip() and estimated_gain >= self.useful_gain_floor:
|
|
87
|
+
any_useful = True
|
|
88
|
+
|
|
89
|
+
self.journal.append(
|
|
90
|
+
JournalEntry(
|
|
91
|
+
step=step,
|
|
92
|
+
text=chunk_display,
|
|
93
|
+
resource_after=self.resource,
|
|
94
|
+
tokens_approx=tokens_approx,
|
|
95
|
+
cost=cost,
|
|
96
|
+
estimated_gain=estimated_gain,
|
|
97
|
+
)
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
should_stop = False
|
|
101
|
+
if self.use_stop_rule:
|
|
102
|
+
if (not self.still_worth_it(estimated_gain)) or self.resource <= self.critical_threshold:
|
|
103
|
+
should_stop = True
|
|
104
|
+
else:
|
|
105
|
+
if self.resource <= self.critical_threshold:
|
|
106
|
+
should_stop = True
|
|
107
|
+
|
|
108
|
+
if chunk.strip():
|
|
109
|
+
current = f"{current}{chunk}\n".strip()
|
|
110
|
+
|
|
111
|
+
if should_stop:
|
|
112
|
+
break
|
|
113
|
+
|
|
114
|
+
if self.use_stop_rule and step < self.max_steps:
|
|
115
|
+
stopped_early = True
|
|
116
|
+
|
|
117
|
+
# Politique d'effondrement : ignorance si rien d'utile
|
|
118
|
+
if not current.strip():
|
|
119
|
+
admitted = True
|
|
120
|
+
answer = IGNORANCE_MSG
|
|
121
|
+
elif (
|
|
122
|
+
self.use_stop_rule
|
|
123
|
+
and self.prefer_ignorance_if_no_useful
|
|
124
|
+
and not any_useful
|
|
125
|
+
and max_gain_seen < self.useful_gain_floor
|
|
126
|
+
):
|
|
127
|
+
admitted = True
|
|
128
|
+
answer = IGNORANCE_MSG
|
|
129
|
+
else:
|
|
130
|
+
admitted = False
|
|
131
|
+
answer = current.strip()
|
|
132
|
+
|
|
133
|
+
return IdcRunResult(
|
|
134
|
+
answer=answer,
|
|
135
|
+
steps=step,
|
|
136
|
+
resource_final=self.resource,
|
|
137
|
+
tokens_approx_total=tokens_total,
|
|
138
|
+
cost_total=cost_total,
|
|
139
|
+
stopped_early=stopped_early,
|
|
140
|
+
admitted_ignorance=admitted,
|
|
141
|
+
journal=self.journal,
|
|
142
|
+
mode="idc" if self.use_stop_rule else "baseline",
|
|
143
|
+
)
|
idc/journal.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Journal append-only : engagements irréversibles."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(frozen=True)
|
|
9
|
+
class JournalEntry:
|
|
10
|
+
step: int
|
|
11
|
+
text: str
|
|
12
|
+
resource_after: float
|
|
13
|
+
tokens_approx: int = 0
|
|
14
|
+
cost: float = 0.0
|
|
15
|
+
estimated_gain: float = 0.0
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class AppendOnlyJournal:
|
|
20
|
+
"""Liste d'engagements ; on ne peut qu'ajouter (jamais réécrire)."""
|
|
21
|
+
|
|
22
|
+
entries: list[JournalEntry] = field(default_factory=list)
|
|
23
|
+
|
|
24
|
+
def append(self, entry: JournalEntry) -> None:
|
|
25
|
+
self.entries.append(entry)
|
|
26
|
+
|
|
27
|
+
def as_context(self) -> str:
|
|
28
|
+
if not self.entries:
|
|
29
|
+
return "(vide)"
|
|
30
|
+
return "\n---\n".join(
|
|
31
|
+
f"Étape {e.step} (R={e.resource_after:.1f}) : {e.text}" for e in self.entries
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
def to_dicts(self) -> list[dict]:
|
|
35
|
+
return [
|
|
36
|
+
{
|
|
37
|
+
"step": e.step,
|
|
38
|
+
"text": e.text,
|
|
39
|
+
"resource_after": e.resource_after,
|
|
40
|
+
"tokens_approx": e.tokens_approx,
|
|
41
|
+
"cost": e.cost,
|
|
42
|
+
"estimated_gain": e.estimated_gain,
|
|
43
|
+
}
|
|
44
|
+
for e in self.entries
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
def __len__(self) -> int:
|
|
48
|
+
return len(self.entries)
|
idc/langgraph_adapter.py
ADDED
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
"""Adaptateur LangGraph-shaped pour IDC (dépendance optionnelle).
|
|
2
|
+
|
|
3
|
+
Ce module **ne requiert pas** `langgraph` pour fonctionner : il expose une
|
|
4
|
+
boucle nœud/état compatible avec le modèle mental de LangGraph (state dict,
|
|
5
|
+
nœud de délibération, condition d'arrêt). Si `langgraph` est installé
|
|
6
|
+
(`pip install idc-wrapper[langgraph]`), `compile_langgraph()` construit un
|
|
7
|
+
vrai `StateGraph` ; sinon un ImportError clair est levé uniquement pour
|
|
8
|
+
cette voie.
|
|
9
|
+
|
|
10
|
+
Usage minimal (sans langgraph) ::
|
|
11
|
+
|
|
12
|
+
from idc.langgraph_adapter import IdcGraphRunner
|
|
13
|
+
from idc import MockBackend
|
|
14
|
+
from idc.mock_backend import VERBOSE
|
|
15
|
+
|
|
16
|
+
runner = IdcGraphRunner(backend=MockBackend(VERBOSE), use_stop_rule=True)
|
|
17
|
+
result = runner.run("tâche")
|
|
18
|
+
|
|
19
|
+
Usage LangGraph réel (si installé) ::
|
|
20
|
+
|
|
21
|
+
from idc.langgraph_adapter import compile_langgraph
|
|
22
|
+
graph = compile_langgraph(backend=..., use_stop_rule=True)
|
|
23
|
+
out = graph.invoke({"task": "...", "resource": 100.0, ...})
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
from dataclasses import dataclass, field
|
|
29
|
+
from typing import Any, Callable, Mapping, MutableMapping, Optional, Protocol
|
|
30
|
+
|
|
31
|
+
from .agent import Backend, GainFn, IGNORANCE_MSG, IdcRunResult, default_gain
|
|
32
|
+
from .journal import AppendOnlyJournal, JournalEntry
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
LANGGRAPH_INSTALL_HINT = (
|
|
36
|
+
"langgraph n'est pas installé. "
|
|
37
|
+
"Installez l'extra optionnel : pip install 'idc-wrapper[langgraph]' "
|
|
38
|
+
"ou pip install langgraph. "
|
|
39
|
+
"Sinon utilisez IdcGraphRunner (même interface nœud/état, sans dépendance)."
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class _LangGraphMissing(Protocol):
|
|
44
|
+
"""Marqueur de typage uniquement."""
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def require_langgraph() -> Any:
|
|
48
|
+
"""Importe langgraph ou lève ImportError avec message d'installation."""
|
|
49
|
+
try:
|
|
50
|
+
import langgraph # noqa: F401
|
|
51
|
+
from langgraph.graph import END, StateGraph # type: ignore
|
|
52
|
+
|
|
53
|
+
return StateGraph, END
|
|
54
|
+
except ImportError as exc: # pragma: no cover - chemin optionnel
|
|
55
|
+
raise ImportError(LANGGRAPH_INSTALL_HINT) from exc
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# ---------------------------------------------------------------------------
|
|
59
|
+
# État LangGraph-shaped (stdlib)
|
|
60
|
+
# ---------------------------------------------------------------------------
|
|
61
|
+
|
|
62
|
+
IdcState = dict[str, Any]
|
|
63
|
+
# Clés attendues :
|
|
64
|
+
# task: str
|
|
65
|
+
# answer: str
|
|
66
|
+
# partial: str
|
|
67
|
+
# step: int
|
|
68
|
+
# resource: float
|
|
69
|
+
# tokens_approx_total: int
|
|
70
|
+
# cost_total: float
|
|
71
|
+
# stopped: bool
|
|
72
|
+
# admitted_ignorance: bool
|
|
73
|
+
# journal_entries: list[dict]
|
|
74
|
+
# last_chunk: str
|
|
75
|
+
# last_gain: float
|
|
76
|
+
# max_gain_seen: float
|
|
77
|
+
# any_useful: bool
|
|
78
|
+
# use_stop_rule: bool
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def initial_state(
|
|
82
|
+
task: str,
|
|
83
|
+
*,
|
|
84
|
+
resource: float = 100.0,
|
|
85
|
+
use_stop_rule: bool = True,
|
|
86
|
+
) -> IdcState:
|
|
87
|
+
return {
|
|
88
|
+
"task": task,
|
|
89
|
+
"answer": "",
|
|
90
|
+
"partial": "",
|
|
91
|
+
"step": 0,
|
|
92
|
+
"resource": float(resource),
|
|
93
|
+
"tokens_approx_total": 0,
|
|
94
|
+
"cost_total": 0.0,
|
|
95
|
+
"stopped": False,
|
|
96
|
+
"admitted_ignorance": False,
|
|
97
|
+
"journal_entries": [],
|
|
98
|
+
"last_chunk": "",
|
|
99
|
+
"last_gain": 0.0,
|
|
100
|
+
"max_gain_seen": 0.0,
|
|
101
|
+
"any_useful": False,
|
|
102
|
+
"use_stop_rule": use_stop_rule,
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@dataclass
|
|
107
|
+
class IdcGraphConfig:
|
|
108
|
+
"""Paramètres partagés par le nœud de délibération et le runner."""
|
|
109
|
+
|
|
110
|
+
backend: Backend
|
|
111
|
+
resource_init: float = 100.0
|
|
112
|
+
critical_threshold: float = 15.0
|
|
113
|
+
cost_per_token: float = 0.8
|
|
114
|
+
max_steps: int = 8
|
|
115
|
+
gain_fn: GainFn = field(default=default_gain)
|
|
116
|
+
use_stop_rule: bool = True
|
|
117
|
+
prefer_ignorance_if_no_useful: bool = True
|
|
118
|
+
useful_gain_floor: float = 40.0
|
|
119
|
+
|
|
120
|
+
def still_worth_it(self, estimated_gain: float, resource: float) -> bool:
|
|
121
|
+
marginal_cost = self.cost_per_token * 50.0
|
|
122
|
+
risk = 2.0 if resource < 30.0 else 1.0
|
|
123
|
+
return estimated_gain > marginal_cost * risk
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def deliberation_node(state: Mapping[str, Any], cfg: IdcGraphConfig) -> IdcState:
|
|
127
|
+
"""Un tour de délibération IDC (nœud LangGraph-shaped).
|
|
128
|
+
|
|
129
|
+
Met à jour ressource, journal, gain ; marque `stopped` si la règle
|
|
130
|
+
d'arrêt (ou le seuil critique / max_steps) l'exige.
|
|
131
|
+
"""
|
|
132
|
+
s = dict(state)
|
|
133
|
+
if s.get("stopped"):
|
|
134
|
+
return s
|
|
135
|
+
|
|
136
|
+
step = int(s.get("step", 0)) + 1
|
|
137
|
+
resource = float(s.get("resource", cfg.resource_init))
|
|
138
|
+
partial = str(s.get("partial", ""))
|
|
139
|
+
task = str(s.get("task", ""))
|
|
140
|
+
journal_entries = list(s.get("journal_entries") or [])
|
|
141
|
+
|
|
142
|
+
context = (
|
|
143
|
+
f"Journal des engagements précédents :\n"
|
|
144
|
+
f"{_journal_as_context(journal_entries)}\n\n"
|
|
145
|
+
f"Tâche : {task}\n\n"
|
|
146
|
+
f"Réponse partielle actuelle :\n{partial}\n\n"
|
|
147
|
+
"Continue ou affine la réponse. Sois concis."
|
|
148
|
+
)
|
|
149
|
+
chunk = cfg.backend.generate(context, max_tokens=120)
|
|
150
|
+
tokens_approx = max(len(chunk.split()), 1)
|
|
151
|
+
chunk_display = chunk.strip() if chunk.strip() else "(vide / incertitude)"
|
|
152
|
+
cost = tokens_approx * cfg.cost_per_token
|
|
153
|
+
resource = max(0.0, resource - cost)
|
|
154
|
+
estimated_gain = cfg.gain_fn(step, chunk)
|
|
155
|
+
|
|
156
|
+
journal_entries.append(
|
|
157
|
+
{
|
|
158
|
+
"step": step,
|
|
159
|
+
"text": chunk_display,
|
|
160
|
+
"resource_after": resource,
|
|
161
|
+
"tokens_approx": tokens_approx,
|
|
162
|
+
"cost": cost,
|
|
163
|
+
"estimated_gain": estimated_gain,
|
|
164
|
+
}
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
max_gain_seen = max(float(s.get("max_gain_seen", 0.0)), estimated_gain)
|
|
168
|
+
any_useful = bool(s.get("any_useful", False))
|
|
169
|
+
if chunk.strip() and estimated_gain >= cfg.useful_gain_floor:
|
|
170
|
+
any_useful = True
|
|
171
|
+
|
|
172
|
+
use_stop = bool(s.get("use_stop_rule", cfg.use_stop_rule))
|
|
173
|
+
should_stop = False
|
|
174
|
+
if use_stop:
|
|
175
|
+
if (not cfg.still_worth_it(estimated_gain, resource)) or resource <= cfg.critical_threshold:
|
|
176
|
+
should_stop = True
|
|
177
|
+
else:
|
|
178
|
+
if resource <= cfg.critical_threshold:
|
|
179
|
+
should_stop = True
|
|
180
|
+
if step >= cfg.max_steps:
|
|
181
|
+
should_stop = True
|
|
182
|
+
|
|
183
|
+
if chunk.strip():
|
|
184
|
+
partial = f"{partial}{chunk}\n".strip()
|
|
185
|
+
|
|
186
|
+
s.update(
|
|
187
|
+
{
|
|
188
|
+
"step": step,
|
|
189
|
+
"resource": resource,
|
|
190
|
+
"tokens_approx_total": int(s.get("tokens_approx_total", 0)) + tokens_approx,
|
|
191
|
+
"cost_total": float(s.get("cost_total", 0.0)) + cost,
|
|
192
|
+
"journal_entries": journal_entries,
|
|
193
|
+
"last_chunk": chunk_display,
|
|
194
|
+
"last_gain": estimated_gain,
|
|
195
|
+
"max_gain_seen": max_gain_seen,
|
|
196
|
+
"any_useful": any_useful,
|
|
197
|
+
"partial": partial,
|
|
198
|
+
"stopped": should_stop,
|
|
199
|
+
"use_stop_rule": use_stop,
|
|
200
|
+
}
|
|
201
|
+
)
|
|
202
|
+
if should_stop:
|
|
203
|
+
s.update(_finalize(s, cfg))
|
|
204
|
+
return s
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def should_continue(state: Mapping[str, Any]) -> str:
|
|
208
|
+
"""Condition de routage : ``continue`` | ``end``."""
|
|
209
|
+
return "end" if state.get("stopped") else "continue"
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _finalize(state: Mapping[str, Any], cfg: IdcGraphConfig) -> dict[str, Any]:
|
|
213
|
+
partial = str(state.get("partial", "")).strip()
|
|
214
|
+
use_stop = bool(state.get("use_stop_rule", cfg.use_stop_rule))
|
|
215
|
+
any_useful = bool(state.get("any_useful", False))
|
|
216
|
+
max_gain_seen = float(state.get("max_gain_seen", 0.0))
|
|
217
|
+
|
|
218
|
+
if not partial:
|
|
219
|
+
return {"answer": IGNORANCE_MSG, "admitted_ignorance": True}
|
|
220
|
+
if (
|
|
221
|
+
use_stop
|
|
222
|
+
and cfg.prefer_ignorance_if_no_useful
|
|
223
|
+
and not any_useful
|
|
224
|
+
and max_gain_seen < cfg.useful_gain_floor
|
|
225
|
+
):
|
|
226
|
+
return {"answer": IGNORANCE_MSG, "admitted_ignorance": True}
|
|
227
|
+
return {"answer": partial, "admitted_ignorance": False}
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _journal_as_context(entries: list[dict]) -> str:
|
|
231
|
+
if not entries:
|
|
232
|
+
return "(vide)"
|
|
233
|
+
return "\n---\n".join(
|
|
234
|
+
f"Étape {e['step']} (R={e['resource_after']:.1f}) : {e['text']}" for e in entries
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def state_to_result(state: Mapping[str, Any], cfg: IdcGraphConfig) -> IdcRunResult:
|
|
239
|
+
"""Convertit l'état final en `IdcRunResult` (compat IdcAgent)."""
|
|
240
|
+
journal = AppendOnlyJournal()
|
|
241
|
+
for e in state.get("journal_entries") or []:
|
|
242
|
+
journal.append(
|
|
243
|
+
JournalEntry(
|
|
244
|
+
step=int(e["step"]),
|
|
245
|
+
text=str(e["text"]),
|
|
246
|
+
resource_after=float(e["resource_after"]),
|
|
247
|
+
tokens_approx=int(e.get("tokens_approx", 0)),
|
|
248
|
+
cost=float(e.get("cost", 0.0)),
|
|
249
|
+
estimated_gain=float(e.get("estimated_gain", 0.0)),
|
|
250
|
+
)
|
|
251
|
+
)
|
|
252
|
+
use_stop = bool(state.get("use_stop_rule", cfg.use_stop_rule))
|
|
253
|
+
step = int(state.get("step", 0))
|
|
254
|
+
stopped_early = bool(use_stop and step < cfg.max_steps and state.get("stopped"))
|
|
255
|
+
return IdcRunResult(
|
|
256
|
+
answer=str(state.get("answer", "")),
|
|
257
|
+
steps=step,
|
|
258
|
+
resource_final=float(state.get("resource", 0.0)),
|
|
259
|
+
tokens_approx_total=int(state.get("tokens_approx_total", 0)),
|
|
260
|
+
cost_total=float(state.get("cost_total", 0.0)),
|
|
261
|
+
stopped_early=stopped_early,
|
|
262
|
+
admitted_ignorance=bool(state.get("admitted_ignorance", False)),
|
|
263
|
+
journal=journal,
|
|
264
|
+
mode="idc" if use_stop else "baseline",
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
@dataclass
|
|
269
|
+
class IdcGraphRunner:
|
|
270
|
+
"""Boucle nœud/état IDC sans dépendance langgraph.
|
|
271
|
+
|
|
272
|
+
Miroir conceptuel d'un StateGraph à un nœud ``deliberate`` + arête
|
|
273
|
+
conditionnelle ``should_continue``.
|
|
274
|
+
"""
|
|
275
|
+
|
|
276
|
+
backend: Backend
|
|
277
|
+
resource: float = 100.0
|
|
278
|
+
critical_threshold: float = 15.0
|
|
279
|
+
cost_per_token: float = 0.8
|
|
280
|
+
max_steps: int = 8
|
|
281
|
+
gain_fn: GainFn = field(default=default_gain)
|
|
282
|
+
use_stop_rule: bool = True
|
|
283
|
+
prefer_ignorance_if_no_useful: bool = True
|
|
284
|
+
useful_gain_floor: float = 40.0
|
|
285
|
+
|
|
286
|
+
def config(self) -> IdcGraphConfig:
|
|
287
|
+
return IdcGraphConfig(
|
|
288
|
+
backend=self.backend,
|
|
289
|
+
resource_init=self.resource,
|
|
290
|
+
critical_threshold=self.critical_threshold,
|
|
291
|
+
cost_per_token=self.cost_per_token,
|
|
292
|
+
max_steps=self.max_steps,
|
|
293
|
+
gain_fn=self.gain_fn,
|
|
294
|
+
use_stop_rule=self.use_stop_rule,
|
|
295
|
+
prefer_ignorance_if_no_useful=self.prefer_ignorance_if_no_useful,
|
|
296
|
+
useful_gain_floor=self.useful_gain_floor,
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
def run(self, task: str) -> IdcRunResult:
|
|
300
|
+
cfg = self.config()
|
|
301
|
+
state = initial_state(task, resource=self.resource, use_stop_rule=self.use_stop_rule)
|
|
302
|
+
while not state.get("stopped"):
|
|
303
|
+
state = deliberation_node(state, cfg)
|
|
304
|
+
if int(state.get("step", 0)) >= cfg.max_steps:
|
|
305
|
+
state["stopped"] = True
|
|
306
|
+
state.update(_finalize(state, cfg))
|
|
307
|
+
break
|
|
308
|
+
if not state.get("answer"):
|
|
309
|
+
state.update(_finalize(state, cfg))
|
|
310
|
+
return state_to_result(state, cfg)
|
|
311
|
+
|
|
312
|
+
def invoke(self, state: MutableMapping[str, Any]) -> IdcState:
|
|
313
|
+
"""API style LangGraph ``graph.invoke(state)``."""
|
|
314
|
+
cfg = self.config()
|
|
315
|
+
s: IdcState = initial_state(
|
|
316
|
+
str(state.get("task", "")),
|
|
317
|
+
resource=float(state.get("resource", self.resource)),
|
|
318
|
+
use_stop_rule=bool(state.get("use_stop_rule", self.use_stop_rule)),
|
|
319
|
+
)
|
|
320
|
+
s.update({k: v for k, v in state.items() if k in s or k == "task"})
|
|
321
|
+
while not s.get("stopped"):
|
|
322
|
+
s = deliberation_node(s, cfg)
|
|
323
|
+
if int(s.get("step", 0)) >= cfg.max_steps:
|
|
324
|
+
s["stopped"] = True
|
|
325
|
+
s.update(_finalize(s, cfg))
|
|
326
|
+
break
|
|
327
|
+
if not s.get("answer"):
|
|
328
|
+
s.update(_finalize(s, cfg))
|
|
329
|
+
return s
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def compile_langgraph(
|
|
333
|
+
backend: Backend,
|
|
334
|
+
*,
|
|
335
|
+
use_stop_rule: bool = True,
|
|
336
|
+
resource: float = 100.0,
|
|
337
|
+
critical_threshold: float = 15.0,
|
|
338
|
+
cost_per_token: float = 0.8,
|
|
339
|
+
max_steps: int = 8,
|
|
340
|
+
gain_fn: Optional[GainFn] = None,
|
|
341
|
+
) -> Any:
|
|
342
|
+
"""Compile un vrai ``StateGraph`` LangGraph si le package est installé.
|
|
343
|
+
|
|
344
|
+
Raises
|
|
345
|
+
------
|
|
346
|
+
ImportError
|
|
347
|
+
Message clair avec hint ``pip install idc-wrapper[langgraph]``.
|
|
348
|
+
"""
|
|
349
|
+
StateGraph, END = require_langgraph()
|
|
350
|
+
cfg = IdcGraphConfig(
|
|
351
|
+
backend=backend,
|
|
352
|
+
resource_init=resource,
|
|
353
|
+
critical_threshold=critical_threshold,
|
|
354
|
+
cost_per_token=cost_per_token,
|
|
355
|
+
max_steps=max_steps,
|
|
356
|
+
gain_fn=gain_fn or default_gain,
|
|
357
|
+
use_stop_rule=use_stop_rule,
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
# État typé de façon souple (dict) pour rester compatible versions langgraph.
|
|
361
|
+
graph = StateGraph(dict)
|
|
362
|
+
|
|
363
|
+
def _node(state: dict) -> dict:
|
|
364
|
+
return deliberation_node(state, cfg)
|
|
365
|
+
|
|
366
|
+
def _route(state: dict) -> str:
|
|
367
|
+
return should_continue(state)
|
|
368
|
+
|
|
369
|
+
graph.add_node("deliberate", _node)
|
|
370
|
+
graph.set_entry_point("deliberate")
|
|
371
|
+
graph.add_conditional_edges(
|
|
372
|
+
"deliberate",
|
|
373
|
+
_route,
|
|
374
|
+
{"continue": "deliberate", "end": END},
|
|
375
|
+
)
|
|
376
|
+
return graph.compile()
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
__all__ = [
|
|
380
|
+
"LANGGRAPH_INSTALL_HINT",
|
|
381
|
+
"IdcState",
|
|
382
|
+
"IdcGraphConfig",
|
|
383
|
+
"IdcGraphRunner",
|
|
384
|
+
"initial_state",
|
|
385
|
+
"deliberation_node",
|
|
386
|
+
"should_continue",
|
|
387
|
+
"state_to_result",
|
|
388
|
+
"compile_langgraph",
|
|
389
|
+
"require_langgraph",
|
|
390
|
+
]
|
idc/mock_backend.py
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""Backend mock déterministe — aucun appel réseau, aucune clé API.
|
|
2
|
+
|
|
3
|
+
Produit des réponses scriptées pour exemples et micro-benchmarks.
|
|
4
|
+
Les nombres mesurés sur ce mock ne sont PAS des résultats de production LLM.
|
|
5
|
+
|
|
6
|
+
Note sur les gains : la heuristique VOI du dépôt compare
|
|
7
|
+
`estimated_gain` à `cost_per_token * 50` (≈ 40 avec le défaut 0.8).
|
|
8
|
+
Les gains des premières étapes utiles sont donc calibrés > 40 pour
|
|
9
|
+
permettre 2–3 tours productifs avant l'arrêt ; les étapes suivantes
|
|
10
|
+
passent sous le seuil (affinage / hallucination / boucle).
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from dataclasses import dataclass, field
|
|
16
|
+
from typing import Callable
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class MockScenario:
|
|
21
|
+
"""Scénario de réponses + gain estimé par étape.
|
|
22
|
+
|
|
23
|
+
`responses` : liste de chunks texte (une par étape).
|
|
24
|
+
`gains` : gain VOI estimé par étape (calibré vs coût marginal ≈ 40).
|
|
25
|
+
`uncertain` : scénario d'incertitude (favorise l'aveu d'ignorance).
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
name: str
|
|
29
|
+
responses: list[str]
|
|
30
|
+
gains: list[float] | None = None
|
|
31
|
+
uncertain: bool = False
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
SUPPORT_CLIENT = MockScenario(
|
|
35
|
+
name="support_client",
|
|
36
|
+
responses=[
|
|
37
|
+
"Diagnostic : délai de livraison signalé. Vérification du tracking.",
|
|
38
|
+
"Cause probable : transit international, pas de perte colis.",
|
|
39
|
+
"Action engagée : remboursement partiel ou relance transporteur.",
|
|
40
|
+
"Affinage inutile : le client a déjà les options claires.",
|
|
41
|
+
"Répétition : mêmes options, aucun fait nouveau.",
|
|
42
|
+
"Hallucination potentielle : inventer un numéro de suivi fantôme.",
|
|
43
|
+
"Encore une invention : fausse date de livraison garantie.",
|
|
44
|
+
"Boucle : reformuler sans valeur ajoutée.",
|
|
45
|
+
],
|
|
46
|
+
# Coût marginal ≈ 40 → étapes 1–3 utiles, puis stop
|
|
47
|
+
gains=[55.0, 48.0, 42.0, 12.0, 8.0, 5.0, 3.0, 2.0],
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
CODE_ASSISTANT = MockScenario(
|
|
51
|
+
name="code_assistant",
|
|
52
|
+
responses=[
|
|
53
|
+
"Bug localisé : index hors bornes sur liste vide.",
|
|
54
|
+
"Correctif : garde `if not items: return []` avant l'index.",
|
|
55
|
+
"Test unitaire suggéré pour le cas vide.",
|
|
56
|
+
"Refacto cosmétique sans impact fonctionnel.",
|
|
57
|
+
"Suggestion speculative hors scope.",
|
|
58
|
+
"Invention d'API inexistante.",
|
|
59
|
+
"Encore une API fantôme.",
|
|
60
|
+
"Boucle de reformulation.",
|
|
61
|
+
],
|
|
62
|
+
gains=[58.0, 50.0, 43.0, 14.0, 7.0, 4.0, 3.0, 2.0],
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
RESEARCH_SYNTH = MockScenario(
|
|
66
|
+
name="research_synth",
|
|
67
|
+
responses=[
|
|
68
|
+
"Sources : 3 papiers sur finitude et arrêt optimal.",
|
|
69
|
+
"Synthèse : budget de délibération + journal irréversible.",
|
|
70
|
+
"Limite : pas de preuve empirique LLM à grande échelle ici.",
|
|
71
|
+
"Digression méthodologique faible valeur.",
|
|
72
|
+
"Citation inventée (à éviter).",
|
|
73
|
+
"Seconde citation inventée.",
|
|
74
|
+
"Reformulation sans source.",
|
|
75
|
+
"Boucle narrative.",
|
|
76
|
+
],
|
|
77
|
+
gains=[52.0, 46.0, 41.0, 11.0, 6.0, 4.0, 3.0, 2.0],
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
UNCERTAIN = MockScenario(
|
|
81
|
+
name="uncertain",
|
|
82
|
+
responses=[
|
|
83
|
+
"", # pas de contenu fiable
|
|
84
|
+
"",
|
|
85
|
+
"",
|
|
86
|
+
"",
|
|
87
|
+
"",
|
|
88
|
+
"",
|
|
89
|
+
"",
|
|
90
|
+
"",
|
|
91
|
+
],
|
|
92
|
+
gains=[9.0, 6.0, 3.0, 2.0, 1.0, 1.0, 1.0, 1.0],
|
|
93
|
+
uncertain=True,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
VERBOSE = MockScenario(
|
|
97
|
+
name="verbose",
|
|
98
|
+
responses=[
|
|
99
|
+
"Point A utile : définition courte du problème.",
|
|
100
|
+
"Point B utile : contrainte principale identifiée.",
|
|
101
|
+
"Point C utile : option d'action recommandée.",
|
|
102
|
+
"Point D : digression longue sans fait nouveau, mots mots mots.",
|
|
103
|
+
"Point E : reformulation synonymique coûteuse en tokens.",
|
|
104
|
+
"Point F : invention plausible mais non fondée.",
|
|
105
|
+
"Point G : suite de l'invention.",
|
|
106
|
+
"Point H : boucle complète.",
|
|
107
|
+
],
|
|
108
|
+
gains=[50.0, 45.0, 41.0, 10.0, 6.0, 4.0, 2.0, 1.0],
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
SCENARIOS: dict[str, MockScenario] = {
|
|
112
|
+
s.name: s
|
|
113
|
+
for s in (SUPPORT_CLIENT, CODE_ASSISTANT, RESEARCH_SYNTH, UNCERTAIN, VERBOSE)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@dataclass
|
|
118
|
+
class MockBackend:
|
|
119
|
+
"""Backend LLM factice : consomme `responses` dans l'ordre."""
|
|
120
|
+
|
|
121
|
+
scenario: MockScenario
|
|
122
|
+
call_count: int = 0
|
|
123
|
+
_extra: list[str] = field(default_factory=list)
|
|
124
|
+
|
|
125
|
+
def __call__(self, prompt: str, max_tokens: int = 120) -> str:
|
|
126
|
+
return self.generate(prompt, max_tokens)
|
|
127
|
+
|
|
128
|
+
def generate(self, prompt: str, max_tokens: int = 120) -> str:
|
|
129
|
+
del prompt, max_tokens
|
|
130
|
+
idx = self.call_count
|
|
131
|
+
self.call_count += 1
|
|
132
|
+
if idx < len(self.scenario.responses):
|
|
133
|
+
return self.scenario.responses[idx]
|
|
134
|
+
if self._extra:
|
|
135
|
+
return self._extra[min(idx - len(self.scenario.responses), len(self._extra) - 1)]
|
|
136
|
+
return ""
|
|
137
|
+
|
|
138
|
+
def gain_for_step(self, step: int, chunk: str) -> float:
|
|
139
|
+
if self.scenario.gains and 1 <= step <= len(self.scenario.gains):
|
|
140
|
+
return self.scenario.gains[step - 1]
|
|
141
|
+
return 25.0 if len(chunk) > 20 else 8.0
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def make_gain_fn(backend: MockBackend) -> Callable[[int, str], float]:
|
|
145
|
+
return backend.gain_for_step
|
idc/test_agent.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Tests unitaires (stdlib unittest) — mock only, pas de clé API."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import unittest
|
|
6
|
+
|
|
7
|
+
from idc import IdcAgent, MockBackend
|
|
8
|
+
from idc.mock_backend import SUPPORT_CLIENT, UNCERTAIN, VERBOSE
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TestIdcVsBaseline(unittest.TestCase):
|
|
12
|
+
def test_idc_stops_earlier_than_baseline_on_verbose(self) -> None:
|
|
13
|
+
b_idc = MockBackend(VERBOSE)
|
|
14
|
+
b_base = MockBackend(VERBOSE)
|
|
15
|
+
idc = IdcAgent(backend=b_idc, use_stop_rule=True, gain_fn=b_idc.gain_for_step)
|
|
16
|
+
base = IdcAgent(backend=b_base, use_stop_rule=False, gain_fn=b_base.gain_for_step)
|
|
17
|
+
r_idc = idc.run("tâche verbose")
|
|
18
|
+
r_base = base.run("tâche verbose")
|
|
19
|
+
self.assertLess(r_idc.steps, r_base.steps)
|
|
20
|
+
self.assertGreaterEqual(r_idc.resource_final, r_base.resource_final)
|
|
21
|
+
self.assertGreaterEqual(r_idc.steps, 2)
|
|
22
|
+
self.assertLessEqual(r_idc.steps, 4)
|
|
23
|
+
|
|
24
|
+
def test_journal_grows_monotonically(self) -> None:
|
|
25
|
+
b = MockBackend(SUPPORT_CLIENT)
|
|
26
|
+
agent = IdcAgent(backend=b, use_stop_rule=True, gain_fn=b.gain_for_step)
|
|
27
|
+
r = agent.run("où est mon colis ?")
|
|
28
|
+
self.assertGreaterEqual(len(r.journal), 2)
|
|
29
|
+
steps = [e.step for e in r.journal.entries]
|
|
30
|
+
self.assertEqual(steps, list(range(1, len(steps) + 1)))
|
|
31
|
+
|
|
32
|
+
def test_uncertain_admits_ignorance_under_idc(self) -> None:
|
|
33
|
+
b = MockBackend(UNCERTAIN)
|
|
34
|
+
agent = IdcAgent(
|
|
35
|
+
backend=b,
|
|
36
|
+
resource=100.0,
|
|
37
|
+
critical_threshold=15.0,
|
|
38
|
+
cost_per_token=0.8,
|
|
39
|
+
max_steps=8,
|
|
40
|
+
use_stop_rule=True,
|
|
41
|
+
gain_fn=b.gain_for_step,
|
|
42
|
+
)
|
|
43
|
+
r = agent.run("question sans source")
|
|
44
|
+
self.assertTrue(r.admitted_ignorance)
|
|
45
|
+
self.assertLess(r.steps, 8)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
if __name__ == "__main__":
|
|
49
|
+
unittest.main()
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""Tests adaptateur LangGraph-shaped — mock only, langgraph optionnel."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import unittest
|
|
6
|
+
|
|
7
|
+
from idc import MockBackend
|
|
8
|
+
from idc.langgraph_adapter import (
|
|
9
|
+
LANGGRAPH_INSTALL_HINT,
|
|
10
|
+
IdcGraphRunner,
|
|
11
|
+
compile_langgraph,
|
|
12
|
+
deliberation_node,
|
|
13
|
+
initial_state,
|
|
14
|
+
require_langgraph,
|
|
15
|
+
should_continue,
|
|
16
|
+
)
|
|
17
|
+
from idc.mock_backend import UNCERTAIN, VERBOSE
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class TestIdcGraphRunner(unittest.TestCase):
|
|
21
|
+
def test_idc_stops_earlier_than_baseline(self) -> None:
|
|
22
|
+
b_idc = MockBackend(VERBOSE)
|
|
23
|
+
b_base = MockBackend(VERBOSE)
|
|
24
|
+
idc = IdcGraphRunner(backend=b_idc, use_stop_rule=True, gain_fn=b_idc.gain_for_step)
|
|
25
|
+
base = IdcGraphRunner(backend=b_base, use_stop_rule=False, gain_fn=b_base.gain_for_step)
|
|
26
|
+
r_idc = idc.run("tâche verbose")
|
|
27
|
+
r_base = base.run("tâche verbose")
|
|
28
|
+
self.assertLess(r_idc.steps, r_base.steps)
|
|
29
|
+
self.assertGreaterEqual(r_idc.resource_final, r_base.resource_final)
|
|
30
|
+
self.assertTrue(r_idc.stopped_early)
|
|
31
|
+
self.assertEqual(r_idc.mode, "idc")
|
|
32
|
+
self.assertEqual(r_base.mode, "baseline")
|
|
33
|
+
|
|
34
|
+
def test_node_loop_marks_stop(self) -> None:
|
|
35
|
+
b = MockBackend(VERBOSE)
|
|
36
|
+
cfg = IdcGraphRunner(backend=b, use_stop_rule=True, gain_fn=b.gain_for_step).config()
|
|
37
|
+
state = initial_state("x", use_stop_rule=True)
|
|
38
|
+
for _ in range(8):
|
|
39
|
+
state = deliberation_node(state, cfg)
|
|
40
|
+
if should_continue(state) == "end":
|
|
41
|
+
break
|
|
42
|
+
self.assertTrue(state["stopped"])
|
|
43
|
+
self.assertLess(state["step"], 8)
|
|
44
|
+
self.assertGreaterEqual(len(state["journal_entries"]), 2)
|
|
45
|
+
|
|
46
|
+
def test_uncertain_admits_ignorance(self) -> None:
|
|
47
|
+
b = MockBackend(UNCERTAIN)
|
|
48
|
+
r = IdcGraphRunner(
|
|
49
|
+
backend=b, use_stop_rule=True, gain_fn=b.gain_for_step
|
|
50
|
+
).run("question sans source")
|
|
51
|
+
self.assertTrue(r.admitted_ignorance)
|
|
52
|
+
|
|
53
|
+
def test_invoke_api(self) -> None:
|
|
54
|
+
b = MockBackend(VERBOSE)
|
|
55
|
+
out = IdcGraphRunner(backend=b, use_stop_rule=True, gain_fn=b.gain_for_step).invoke(
|
|
56
|
+
{"task": "demo", "resource": 100.0}
|
|
57
|
+
)
|
|
58
|
+
self.assertIn("answer", out)
|
|
59
|
+
self.assertTrue(out["stopped"])
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class TestLangGraphOptional(unittest.TestCase):
|
|
63
|
+
def test_require_langgraph_message_or_import(self) -> None:
|
|
64
|
+
try:
|
|
65
|
+
StateGraph, END = require_langgraph()
|
|
66
|
+
self.assertIsNotNone(StateGraph)
|
|
67
|
+
self.assertIsNotNone(END)
|
|
68
|
+
except ImportError as exc:
|
|
69
|
+
self.assertIn("idc-wrapper[langgraph]", str(exc))
|
|
70
|
+
self.assertIn("langgraph", str(exc).lower())
|
|
71
|
+
|
|
72
|
+
def test_compile_langgraph_or_skip(self) -> None:
|
|
73
|
+
b = MockBackend(VERBOSE)
|
|
74
|
+
try:
|
|
75
|
+
graph = compile_langgraph(
|
|
76
|
+
backend=b,
|
|
77
|
+
use_stop_rule=True,
|
|
78
|
+
gain_fn=b.gain_for_step,
|
|
79
|
+
)
|
|
80
|
+
except ImportError as exc:
|
|
81
|
+
self.assertIn(LANGGRAPH_INSTALL_HINT.split(".")[0], str(exc))
|
|
82
|
+
self.skipTest("langgraph non installé — OK pour CI")
|
|
83
|
+
return
|
|
84
|
+
# Si langgraph est présent : un invoke court doit s'arrêter tôt sous IDC.
|
|
85
|
+
out = graph.invoke(
|
|
86
|
+
{
|
|
87
|
+
"task": "demo langgraph",
|
|
88
|
+
"answer": "",
|
|
89
|
+
"partial": "",
|
|
90
|
+
"step": 0,
|
|
91
|
+
"resource": 100.0,
|
|
92
|
+
"tokens_approx_total": 0,
|
|
93
|
+
"cost_total": 0.0,
|
|
94
|
+
"stopped": False,
|
|
95
|
+
"admitted_ignorance": False,
|
|
96
|
+
"journal_entries": [],
|
|
97
|
+
"last_chunk": "",
|
|
98
|
+
"last_gain": 0.0,
|
|
99
|
+
"max_gain_seen": 0.0,
|
|
100
|
+
"any_useful": False,
|
|
101
|
+
"use_stop_rule": True,
|
|
102
|
+
}
|
|
103
|
+
)
|
|
104
|
+
self.assertTrue(out.get("stopped"))
|
|
105
|
+
self.assertLess(int(out.get("step", 99)), 8)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
if __name__ == "__main__":
|
|
109
|
+
unittest.main()
|
idc_agent.py
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""IDC agent CLI — entrée fine (OpenAI-compatible ou --mock).
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
export OPENAI_API_KEY=...
|
|
6
|
+
python python/idc_agent.py "Your task here"
|
|
7
|
+
|
|
8
|
+
# Sans clé API (démo déterministe) :
|
|
9
|
+
python python/idc_agent.py --mock support_client "Question client : où est mon colis ?"
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import argparse
|
|
15
|
+
import json
|
|
16
|
+
import os
|
|
17
|
+
import sys
|
|
18
|
+
import urllib.error
|
|
19
|
+
import urllib.request
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
# Permettre `python python/idc_agent.py` depuis la racine du dépôt
|
|
23
|
+
_ROOT = Path(__file__).resolve().parent
|
|
24
|
+
if str(_ROOT) not in sys.path:
|
|
25
|
+
sys.path.insert(0, str(_ROOT))
|
|
26
|
+
|
|
27
|
+
from idc import AppendOnlyJournal, IdcAgent, MockBackend, MockScenario # noqa: E402
|
|
28
|
+
from idc.mock_backend import SCENARIOS # noqa: E402
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class OpenAIBackend:
|
|
32
|
+
def __init__(self, api_url: str, api_key: str, model: str) -> None:
|
|
33
|
+
self.api_url = api_url
|
|
34
|
+
self.api_key = api_key
|
|
35
|
+
self.model = model
|
|
36
|
+
|
|
37
|
+
def generate(self, prompt: str, max_tokens: int = 120) -> str:
|
|
38
|
+
body = {
|
|
39
|
+
"model": self.model,
|
|
40
|
+
"messages": [{"role": "user", "content": prompt}],
|
|
41
|
+
"max_tokens": max_tokens,
|
|
42
|
+
"temperature": 0.7,
|
|
43
|
+
}
|
|
44
|
+
req = urllib.request.Request(
|
|
45
|
+
self.api_url,
|
|
46
|
+
data=json.dumps(body).encode("utf-8"),
|
|
47
|
+
headers={
|
|
48
|
+
"Authorization": f"Bearer {self.api_key}",
|
|
49
|
+
"Content-Type": "application/json",
|
|
50
|
+
},
|
|
51
|
+
method="POST",
|
|
52
|
+
)
|
|
53
|
+
with urllib.request.urlopen(req, timeout=60) as resp:
|
|
54
|
+
data = json.loads(resp.read().decode("utf-8"))
|
|
55
|
+
choices = data.get("choices") or []
|
|
56
|
+
if not choices:
|
|
57
|
+
return ""
|
|
58
|
+
return choices[0].get("message", {}).get("content") or ""
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def main() -> None:
|
|
62
|
+
parser = argparse.ArgumentParser(description="IDC agent (OpenAI-compatible or --mock)")
|
|
63
|
+
parser.add_argument("task", nargs="*", help="Tâche / question")
|
|
64
|
+
parser.add_argument(
|
|
65
|
+
"--mock",
|
|
66
|
+
metavar="SCENARIO",
|
|
67
|
+
choices=sorted(SCENARIOS.keys()),
|
|
68
|
+
help="Utiliser un backend mock (pas de clé API)",
|
|
69
|
+
)
|
|
70
|
+
parser.add_argument("--baseline", action="store_true", help="Désactiver la règle d'arrêt VOI")
|
|
71
|
+
parser.add_argument("--resource", type=float, default=100.0)
|
|
72
|
+
parser.add_argument("--threshold", type=float, default=15.0)
|
|
73
|
+
parser.add_argument("--max-steps", type=int, default=8)
|
|
74
|
+
parser.add_argument("--cost-per-token", type=float, default=0.8)
|
|
75
|
+
args = parser.parse_args()
|
|
76
|
+
|
|
77
|
+
task = " ".join(args.task).strip() or (
|
|
78
|
+
"Explique en 5 points pourquoi la finitude computationnelle peut réduire les hallucinations."
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
if args.mock:
|
|
82
|
+
scenario = SCENARIOS[args.mock]
|
|
83
|
+
backend: object = MockBackend(scenario=scenario)
|
|
84
|
+
gain_fn = backend.gain_for_step # type: ignore[attr-defined]
|
|
85
|
+
else:
|
|
86
|
+
base = os.environ.get("OPENAI_BASE_URL", "https://api.openai.com/v1").rstrip("/")
|
|
87
|
+
api_key = os.environ.get("OPENAI_API_KEY", "")
|
|
88
|
+
if not api_key:
|
|
89
|
+
raise SystemExit(
|
|
90
|
+
"Set OPENAI_API_KEY, or pass --mock <scenario> "
|
|
91
|
+
f"(scenarios: {', '.join(sorted(SCENARIOS))})."
|
|
92
|
+
)
|
|
93
|
+
backend = OpenAIBackend(
|
|
94
|
+
api_url=f"{base}/chat/completions",
|
|
95
|
+
api_key=api_key,
|
|
96
|
+
model=os.environ.get("OPENAI_MODEL", "gpt-4o-mini"),
|
|
97
|
+
)
|
|
98
|
+
gain_fn = None
|
|
99
|
+
|
|
100
|
+
agent_kwargs = dict(
|
|
101
|
+
backend=backend,
|
|
102
|
+
resource=args.resource,
|
|
103
|
+
critical_threshold=args.threshold,
|
|
104
|
+
cost_per_token=args.cost_per_token,
|
|
105
|
+
max_steps=args.max_steps,
|
|
106
|
+
use_stop_rule=not args.baseline,
|
|
107
|
+
)
|
|
108
|
+
if gain_fn is not None:
|
|
109
|
+
agent_kwargs["gain_fn"] = gain_fn
|
|
110
|
+
|
|
111
|
+
agent = IdcAgent(**agent_kwargs)
|
|
112
|
+
try:
|
|
113
|
+
result = agent.run(task)
|
|
114
|
+
except urllib.error.HTTPError as e:
|
|
115
|
+
raise SystemExit(f"HTTP {e.code}: {e.read().decode('utf-8', errors='replace')}") from e
|
|
116
|
+
|
|
117
|
+
mode = "BASELINE (pas de VOI)" if args.baseline else "IDC (règle d'arrêt)"
|
|
118
|
+
print(f"=== MODE : {mode} ===")
|
|
119
|
+
for e in result.journal.entries:
|
|
120
|
+
print(
|
|
121
|
+
f"[étape {e.step}] R={e.resource_after:.1f} | "
|
|
122
|
+
f"coût={e.cost:.1f} | gain≈{e.estimated_gain:.1f} | tokens≈{e.tokens_approx}"
|
|
123
|
+
)
|
|
124
|
+
if result.stopped_early:
|
|
125
|
+
print("→ Arrêt anticipé (VOI / seuil).")
|
|
126
|
+
print("\n=== RÉPONSE FINALE (après effondrement) ===\n" + result.answer)
|
|
127
|
+
print("\n=== JOURNAL APPEND-ONLY ===\n" + result.journal.as_context())
|
|
128
|
+
print(
|
|
129
|
+
f"\nRessource finale : {result.resource_final:.1f} | "
|
|
130
|
+
f"étapes : {result.steps} | tokens≈{result.tokens_approx_total} | "
|
|
131
|
+
f"ignorance : {result.admitted_ignorance}"
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
# Ré-exports pour import legacy : `from idc_agent import IdcAgent, AppendOnlyJournal`
|
|
136
|
+
__all__ = ["AppendOnlyJournal", "IdcAgent", "MockBackend", "MockScenario", "main"]
|
|
137
|
+
|
|
138
|
+
if __name__ == "__main__":
|
|
139
|
+
main()
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: idc-wrapper
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: IDC — Effondrement Décisionnel Irréversible : ressource finie, journal append-only, règle d'arrêt pour agents LLM.
|
|
5
|
+
Author: Micaël Lanoix
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/lanoixdesign/idc-wrapper
|
|
8
|
+
Project-URL: Repository, https://github.com/lanoixdesign/idc-wrapper
|
|
9
|
+
Project-URL: Issues, https://github.com/lanoixdesign/idc-wrapper/issues
|
|
10
|
+
Project-URL: Changelog, https://github.com/lanoixdesign/idc-wrapper/blob/main/CHANGELOG.md
|
|
11
|
+
Project-URL: Documentation, https://github.com/lanoixdesign/idc-wrapper#readme
|
|
12
|
+
Keywords: idc,agents,llm,finitude,stop-rule,agi,langgraph
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
21
|
+
Classifier: Intended Audience :: Developers
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Provides-Extra: langgraph
|
|
27
|
+
Requires-Dist: langgraph>=0.2; extra == "langgraph"
|
|
28
|
+
Dynamic: license-file
|
|
29
|
+
|
|
30
|
+
# IDC Wrapper
|
|
31
|
+
|
|
32
|
+
[](./LICENSE)
|
|
33
|
+
[](https://github.com/lanoixdesign/idc-wrapper/releases/tag/v0.2.0)
|
|
34
|
+
[](https://github.com/lanoixdesign/idc-wrapper/actions/workflows/ci.yml)
|
|
35
|
+
[](./python/)
|
|
36
|
+
[](./Cargo.toml)
|
|
37
|
+
|
|
38
|
+
**Couche d’enveloppe IDC — Effondrement Décisionnel Irréversible**
|
|
39
|
+
Open source, pensée pour **tous les runtimes d’agents** (Cursor, Claude Code, LangGraph, OpenAI Agents, boucles custom…).
|
|
40
|
+
|
|
41
|
+
> **English** — Finite deliberation budget `R`, append-only journal, optimal stop rule.
|
|
42
|
+
> Hallucination should cost more than admitting ignorance. Mock benches are real
|
|
43
|
+
> numbers on a fake LLM; theory remains hypothesis — see [PERFORMANCE.md](./PERFORMANCE.md).
|
|
44
|
+
|
|
45
|
+
## Pourquoi l’IDC (vision, sans claims inventés)
|
|
46
|
+
|
|
47
|
+
Une IA sans **finitude** peut réfléchir sans fin, hésiter, et inventer : inventer
|
|
48
|
+
ne coûte souvent pas plus cher qu’avouer « je ne sais pas ». L’IDC propose
|
|
49
|
+
d’envelopper le modèle avec :
|
|
50
|
+
|
|
51
|
+
1. une **ressource finie** `R` qui décroît avec le calcul,
|
|
52
|
+
2. un **journal append-only** (engagements irréversibles),
|
|
53
|
+
3. une **règle d’arrêt** (s’effondrer quand la valeur marginale de réfléchir
|
|
54
|
+
ne justifie plus le coût).
|
|
55
|
+
|
|
56
|
+
C’est un **modèle + protocole de test**, pas une preuve empirique déjà validée
|
|
57
|
+
sur les LLM de frontier. Les chiffres de ce dépôt viennent d’un **mock**
|
|
58
|
+
reproductible — voir [PERFORMANCE.md](./PERFORMANCE.md).
|
|
59
|
+
|
|
60
|
+
## Livre / concept
|
|
61
|
+
|
|
62
|
+
- Livre (Amazon FR) : [Effondrement Décisionnel Irréversible](https://www.amazon.fr/dp/B0HMX1CY6R)
|
|
63
|
+
- Citation machine-readable : [`CITATION.cff`](./CITATION.cff)
|
|
64
|
+
|
|
65
|
+
## Table des matières
|
|
66
|
+
|
|
67
|
+
- [Pour les agents](#pour-les-agents)
|
|
68
|
+
- [Quickstart Python](#quickstart-python)
|
|
69
|
+
- [Quickstart Rust](#quickstart-rust)
|
|
70
|
+
- [Exemples](#exemples)
|
|
71
|
+
- [Adaptateur LangGraph](#adaptateur-langgraph)
|
|
72
|
+
- [Packs agents](#packs-agents)
|
|
73
|
+
- [Performance (mock)](#performance-mock)
|
|
74
|
+
- [Structure](#structure)
|
|
75
|
+
- [Paramètres](#paramètres)
|
|
76
|
+
- [Roadmap](#roadmap)
|
|
77
|
+
- [Communauté](#communauté)
|
|
78
|
+
- [Licence & auteur](#licence--auteur)
|
|
79
|
+
|
|
80
|
+
## Pour les agents
|
|
81
|
+
|
|
82
|
+
Lis d’abord **[AGENTS.md](./AGENTS.md)**.
|
|
83
|
+
|
|
84
|
+
| Fichier | Rôle |
|
|
85
|
+
|---------|------|
|
|
86
|
+
| [`agents/skills/idc/SKILL.md`](./agents/skills/idc/SKILL.md) | Skill / recipe portable |
|
|
87
|
+
| [`schema/idc-protocol.json`](./schema/idc-protocol.json) | Contrat machine-readable |
|
|
88
|
+
| [`python/idc/`](./python/idc/) | Package référence (journal, agent, mock) |
|
|
89
|
+
| [`python/idc/langgraph_adapter.py`](./python/idc/langgraph_adapter.py) | Adaptateur LangGraph-shaped (optionnel) |
|
|
90
|
+
| [`python/idc_agent.py`](./python/idc_agent.py) | CLI (OpenAI-compatible ou `--mock`) |
|
|
91
|
+
| [`src/lib.rs`](./src/lib.rs) | Bibliothèque Rust (`IdcAgent`, journal) |
|
|
92
|
+
| [`src/main.rs`](./src/main.rs) | Binaire CLI mince |
|
|
93
|
+
| [`examples/`](./examples/) | Cas Sans IDC / Avec IDC |
|
|
94
|
+
| [`packs/`](./packs/) | Snippets Cursor / Claude / OpenAI Agents |
|
|
95
|
+
| [`PERFORMANCE.md`](./PERFORMANCE.md) | Mesures mock + hypothèses vs démontré |
|
|
96
|
+
|
|
97
|
+
## Quickstart Python
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
# Démo sans clé API (recommandé pour découvrir)
|
|
101
|
+
PYTHONPATH=python python3 python/idc_agent.py --mock support_client \
|
|
102
|
+
"Client : où est mon colis ?"
|
|
103
|
+
|
|
104
|
+
# Comparer baseline (pas de VOI) vs IDC
|
|
105
|
+
PYTHONPATH=python python3 python/idc_agent.py --mock verbose --baseline "long"
|
|
106
|
+
PYTHONPATH=python python3 python/idc_agent.py --mock verbose "long"
|
|
107
|
+
|
|
108
|
+
# Avec API compatible OpenAI
|
|
109
|
+
export OPENAI_API_KEY=sk-...
|
|
110
|
+
# optionnel : OPENAI_BASE_URL=http://localhost:11434/v1 OPENAI_MODEL=llama3.2
|
|
111
|
+
PYTHONPATH=python python3 python/idc_agent.py "Explique l’IDC en 5 points"
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Installation package (editable locale ; PyPI public prévu — voir [PUBLISHING.md](./PUBLISHING.md)) :
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
pip install -e .
|
|
118
|
+
# ou plus tard : pip install idc-wrapper
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Tests / bench (aucune clé) :
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
PYTHONPATH=python python3 -m unittest discover -s python/idc -v
|
|
125
|
+
PYTHONPATH=python python3 benchmarks/bench_mock.py
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Quickstart Rust
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
cargo build --release
|
|
132
|
+
export OPENAI_API_KEY=sk-...
|
|
133
|
+
./target/release/idc-wrapper
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
La logique vit dans la **crate lib** `idc_wrapper` (`src/lib.rs`) ; le binaire
|
|
137
|
+
`src/main.rs` l’appelle seulement. Par défaut : `https://api.openai.com/v1/chat/completions`,
|
|
138
|
+
modèle `gpt-4o-mini`.
|
|
139
|
+
|
|
140
|
+
> **Note toolchain** : `cargo check` / `cargo build` nécessitent un Rust stable
|
|
141
|
+
> récent (CI utilise `dtolnay/rust-toolchain@stable`). Si ton `rustc` local est
|
|
142
|
+
> trop ancien pour les dépendances (`reqwest` / `tokio`), la structure lib+bin
|
|
143
|
+
> reste valide — mets à jour le toolchain ou peaufine en CI.
|
|
144
|
+
|
|
145
|
+
## Exemples
|
|
146
|
+
|
|
147
|
+
Cas concrets avec narration **Sans IDC / Avec IDC / Bénéfice** et scripts mock :
|
|
148
|
+
|
|
149
|
+
| # | Dossier | Idée |
|
|
150
|
+
|---|---------|------|
|
|
151
|
+
| 01 | [`examples/01_support_client`](./examples/01_support_client/) | Support livraison |
|
|
152
|
+
| 02 | [`examples/02_code_assistant`](./examples/02_code_assistant/) | Fix bug + stop avant API fantôme |
|
|
153
|
+
| 03 | [`examples/03_research_synth`](./examples/03_research_synth/) | Synthèse bornée, pas de citation inventée |
|
|
154
|
+
| 04 | [`examples/04_langgraph`](./examples/04_langgraph/) | Boucle nœud/état style LangGraph + arrêt IDC |
|
|
155
|
+
|
|
156
|
+
Voir [`examples/README.md`](./examples/README.md). Flux ASCII/Mermaid : [`docs/demo.md`](./docs/demo.md).
|
|
157
|
+
|
|
158
|
+
## Adaptateur LangGraph
|
|
159
|
+
|
|
160
|
+
Boucle **nœud / état** compatible LangGraph, **sans dépendance obligatoire** :
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
# démo mock (stdlib)
|
|
164
|
+
PYTHONPATH=python python3 examples/04_langgraph/run_mock.py
|
|
165
|
+
|
|
166
|
+
# optionnel : vrai StateGraph
|
|
167
|
+
pip install 'idc-wrapper[langgraph]' # ou : pip install langgraph
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
```python
|
|
171
|
+
from idc.langgraph_adapter import IdcGraphRunner, compile_langgraph
|
|
172
|
+
# IdcGraphRunner : toujours disponible
|
|
173
|
+
# compile_langgraph(...) : ImportError clair si langgraph absent
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Détails : [`examples/04_langgraph/README.md`](./examples/04_langgraph/README.md).
|
|
177
|
+
|
|
178
|
+
## Packs agents
|
|
179
|
+
|
|
180
|
+
Snippets copy-paste prêts :
|
|
181
|
+
|
|
182
|
+
| Runtime | Pack |
|
|
183
|
+
|---------|------|
|
|
184
|
+
| Cursor | [`packs/cursor/RULE.md`](./packs/cursor/RULE.md) + [`packs/cursor/idc.mdc`](./packs/cursor/idc.mdc) |
|
|
185
|
+
| Claude Code | [`packs/claude/CLAUDE.md`](./packs/claude/CLAUDE.md) |
|
|
186
|
+
| OpenAI Agents SDK | [`packs/openai-agents/README.md`](./packs/openai-agents/README.md) (interface + stub) |
|
|
187
|
+
|
|
188
|
+
## Performance (mock)
|
|
189
|
+
|
|
190
|
+
Micro-benchmark **Baseline (N fixes) vs IDC (règle d’arrêt)** sur réponses mock.
|
|
191
|
+
Dernière agrégation typique (régénérer localement pour confirmer) :
|
|
192
|
+
|
|
193
|
+
| Agrégat | Baseline | IDC |
|
|
194
|
+
|---------|----------|-----|
|
|
195
|
+
| Étapes moyennes | 8.0 | ~3.4 |
|
|
196
|
+
| Tokens≈ moyens | ~44.8 | ~26.6 |
|
|
197
|
+
| R finale moyenne | ~64.2 | ~78.7 |
|
|
198
|
+
|
|
199
|
+
Détail, caveats, et distinction **hypothèse livre vs démontré par le code** :
|
|
200
|
+
**[PERFORMANCE.md](./PERFORMANCE.md)**. Aucun claim de perf LLM réel ici.
|
|
201
|
+
|
|
202
|
+
## Structure
|
|
203
|
+
|
|
204
|
+
```
|
|
205
|
+
idc-wrapper/
|
|
206
|
+
├── AGENTS.md # contrat pour runtimes agents
|
|
207
|
+
├── PERFORMANCE.md # mesures mock honnêtes
|
|
208
|
+
├── ROADMAP.md # v0.3 / v0.4
|
|
209
|
+
├── PUBLISHING.md # PyPI trusted publisher
|
|
210
|
+
├── python/idc/ # package (journal, agent, mock, langgraph_adapter)
|
|
211
|
+
├── python/idc_agent.py # CLI
|
|
212
|
+
├── src/lib.rs # crate Rust publique
|
|
213
|
+
├── src/main.rs # binaire mince
|
|
214
|
+
├── schema/idc-protocol.json
|
|
215
|
+
├── agents/skills/idc/SKILL.md
|
|
216
|
+
├── packs/ # Cursor / Claude / OpenAI Agents
|
|
217
|
+
├── examples/ # 4 cas + README (dont LangGraph)
|
|
218
|
+
├── docs/demo.md # flux IDC (ASCII / Mermaid)
|
|
219
|
+
├── benchmarks/ # bench_mock.py + results/
|
|
220
|
+
├── community/ # good-first-issues prêts à coller
|
|
221
|
+
└── .github/ # CI, publish-pypi, templates
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
## Paramètres
|
|
225
|
+
|
|
226
|
+
| Paramètre | Défaut | Rôle |
|
|
227
|
+
|-----------|--------|------|
|
|
228
|
+
| `resource` | 100.0 | Budget initial |
|
|
229
|
+
| `critical_threshold` | 15.0 | Force l’arrêt |
|
|
230
|
+
| `cost_per_token` | 0.8 | Coût approx. par token |
|
|
231
|
+
| `max_steps` | 8 | Garde-fou |
|
|
232
|
+
|
|
233
|
+
Heuristique VOI simple : `estimated_gain > cost_per_token * 50 * risk`.
|
|
234
|
+
|
|
235
|
+
## Roadmap
|
|
236
|
+
|
|
237
|
+
Priorités v0.3 / v0.4 (PyPI live, crates.io, bench LLM réel, plus d’adapters) :
|
|
238
|
+
**[ROADMAP.md](./ROADMAP.md)**.
|
|
239
|
+
|
|
240
|
+
## Communauté
|
|
241
|
+
|
|
242
|
+
- Contribuer : [CONTRIBUTING.md](./CONTRIBUTING.md) (FR + EN) — active aussi **Discussions** (Settings → Features)
|
|
243
|
+
- Conduite : [CODE_OF_CONDUCT.md](./CODE_OF_CONDUCT.md)
|
|
244
|
+
- Sécurité : [SECURITY.md](./SECURITY.md)
|
|
245
|
+
- Changelog : [CHANGELOG.md](./CHANGELOG.md)
|
|
246
|
+
- Good first issues : [community/GOOD_FIRST_ISSUES.md](./community/GOOD_FIRST_ISSUES.md)
|
|
247
|
+
- Issues / PR : templates dans `.github/`
|
|
248
|
+
- Publication PyPI : [PUBLISHING.md](./PUBLISHING.md)
|
|
249
|
+
|
|
250
|
+
Idées bienvenues : VOI plus fine, persistance disque du journal, publish crates.io,
|
|
251
|
+
adapter Agents SDK réel, calibrage dynamique du seuil — **avec mesures
|
|
252
|
+
reproductibles**, pas de chiffres inventés. (Adaptateur LangGraph-shaped : v0.2.0.)
|
|
253
|
+
|
|
254
|
+
## Licence & auteur
|
|
255
|
+
|
|
256
|
+
[MIT](./LICENSE) — Copyright (c) 2026 Micaël Lanoix
|
|
257
|
+
|
|
258
|
+
**Micaël Lanoix** — continuité des travaux sur l’ADN numérique des images
|
|
259
|
+
(PNGIA / Pixel 4D) et l’Effondrement Décisionnel Irréversible.
|
|
260
|
+
|
|
261
|
+
Citation : voir [`CITATION.cff`](./CITATION.cff). Livre : [Amazon FR](https://www.amazon.fr/dp/B0HMX1CY6R).
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
idc_agent.py,sha256=FKwAnRoUgvRpOZrZiUjosXu6XHNGJSadXROf06RGHyo,4981
|
|
2
|
+
idc/__init__.py,sha256=hkJicdNQvR5Zewd17UeN_e4dXUqmVtyJ41uI1szZriY,729
|
|
3
|
+
idc/agent.py,sha256=pHCKdR7DdJVL6fjJmapFXrlTh-MgvXYAW4zw2L1jHDc,4705
|
|
4
|
+
idc/journal.py,sha256=ajjwGkp9P8Eav7UGyq2VZ9xXNF8SeaKh5jS6WGj3ki4,1243
|
|
5
|
+
idc/langgraph_adapter.py,sha256=QNWmq31X2eO8qe-uVtvgyU5skIaK9wdZEGF7r0Ca1xs,12782
|
|
6
|
+
idc/mock_backend.py,sha256=FdxSWTjMVSxGE-JLGNM6WhYIAcYXsXKLcgeozFW86UA,4972
|
|
7
|
+
idc/test_agent.py,sha256=_eeOSaobZJ7FV3KMxMnPyMamivwBFtqOxqlR8wTrS0I,1823
|
|
8
|
+
idc/test_langgraph_adapter.py,sha256=_7zfpKLxvWfaSP-dghOCshcN11ymAk25B6HRhZBIflQ,3933
|
|
9
|
+
idc_wrapper-0.2.0.dist-info/licenses/LICENSE,sha256=e4aLNilgWIh9zBtkI1TgbpBUYCHFHhgWHNHliiC8_fA,1071
|
|
10
|
+
idc_wrapper-0.2.0.dist-info/METADATA,sha256=kqjcAL5mqgXk0gio3jq27gJlH7Z3KbRquFs_LY4AdJQ,10807
|
|
11
|
+
idc_wrapper-0.2.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
12
|
+
idc_wrapper-0.2.0.dist-info/entry_points.txt,sha256=CvyLvrPTa3-X3QnTQTIY1Q5oGTpvaEXCmabb0l9C_5k,45
|
|
13
|
+
idc_wrapper-0.2.0.dist-info/top_level.txt,sha256=M633gtg4v9SL3I17-BO_cZfAOXSgqtgsKA6V8O9tPLY,14
|
|
14
|
+
idc_wrapper-0.2.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Micaël Lanoix
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|