thought-fork 0.6.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.
- thought_fork/__init__.py +165 -0
- thought_fork/cli.py +175 -0
- thought_fork/config.py +171 -0
- thought_fork/fork.py +61 -0
- thought_fork/manager.py +185 -0
- thought_fork/message.py +24 -0
- thought_fork/result.py +45 -0
- thought_fork/stance_selector.py +364 -0
- thought_fork/synthesis.py +211 -0
- thought_fork-0.6.0.dist-info/METADATA +213 -0
- thought_fork-0.6.0.dist-info/RECORD +15 -0
- thought_fork-0.6.0.dist-info/WHEEL +5 -0
- thought_fork-0.6.0.dist-info/entry_points.txt +2 -0
- thought_fork-0.6.0.dist-info/licenses/LICENSE +190 -0
- thought_fork-0.6.0.dist-info/top_level.txt +1 -0
thought_fork/__init__.py
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# Copyright 2026 Ameen Saeed
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""
|
|
16
|
+
Thought Fork — Branch your AI's reasoning like Git branches.
|
|
17
|
+
|
|
18
|
+
Thought Fork spawns N parallel reasoning paths ("forks") from a single prompt,
|
|
19
|
+
each biased by a different stance, then merges them into a synthesis with
|
|
20
|
+
explicit attribution.
|
|
21
|
+
|
|
22
|
+
Concept and vocabulary by Ameen Saeed, June 2026.
|
|
23
|
+
|
|
24
|
+
Simple usage::
|
|
25
|
+
|
|
26
|
+
from thought_fork import synthesize
|
|
27
|
+
|
|
28
|
+
result = await synthesize("Should I migrate to microservices?", fork_count=3)
|
|
29
|
+
print(result.synthesis) # Attributed final answer
|
|
30
|
+
print(result.forks["cautious"]) # Individual fork output
|
|
31
|
+
print(result.token_usage) # {"forks": ..., "synthesis": ..., "total": ...}
|
|
32
|
+
|
|
33
|
+
Advanced usage with custom stances::
|
|
34
|
+
|
|
35
|
+
from thought_fork import Fork, synthesize
|
|
36
|
+
|
|
37
|
+
result = await synthesize(
|
|
38
|
+
"Review this architecture",
|
|
39
|
+
forks=[
|
|
40
|
+
Fork(id="A", stance="security", system_prompt="Find vulnerabilities"),
|
|
41
|
+
Fork(id="B", stance="performance", system_prompt="Find bottlenecks"),
|
|
42
|
+
Fork(id="C", stance="maintainability", system_prompt="Find complexity risks"),
|
|
43
|
+
]
|
|
44
|
+
)
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
from __future__ import annotations
|
|
48
|
+
|
|
49
|
+
import time
|
|
50
|
+
|
|
51
|
+
__version__ = "0.6.0"
|
|
52
|
+
__author__ = "Ameen Saeed"
|
|
53
|
+
|
|
54
|
+
from thought_fork.config import BUILT_IN_STANCES, ForkConfig
|
|
55
|
+
from thought_fork.fork import Fork, get_stance_prompt
|
|
56
|
+
from thought_fork.manager import ForkManager
|
|
57
|
+
from thought_fork.message import Message
|
|
58
|
+
from thought_fork.result import ForkResult
|
|
59
|
+
from thought_fork.stance_selector import SelectedStance, StanceSelector
|
|
60
|
+
from thought_fork.synthesis import (
|
|
61
|
+
FORK_OUTPUT_TEMPLATE,
|
|
62
|
+
SYNTHESIS_SYSTEM_PROMPT,
|
|
63
|
+
SYNTHESIS_USER_TEMPLATE,
|
|
64
|
+
SynthesisEngine,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
async def synthesize(
|
|
69
|
+
prompt: str,
|
|
70
|
+
fork_count: int = 3,
|
|
71
|
+
stances: list[str] | None = None,
|
|
72
|
+
forks: list[Fork] | None = None,
|
|
73
|
+
history: list[Message] | None = None,
|
|
74
|
+
config: ForkConfig | None = None,
|
|
75
|
+
) -> ForkResult:
|
|
76
|
+
"""Fork a prompt into parallel reasoning paths and synthesize.
|
|
77
|
+
|
|
78
|
+
This is the main entry point for the Thought Fork library.
|
|
79
|
+
It wraps ForkManager + SynthesisEngine into a single async call.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
prompt: The question or problem to fork.
|
|
83
|
+
fork_count: Number of forks to spawn (ignored if ``forks`` is provided).
|
|
84
|
+
stances: List of stance names. Defaults to the first N built-in stances.
|
|
85
|
+
forks: Advanced — provide pre-built Fork objects with custom system prompts.
|
|
86
|
+
history: Optional list of previous Message objects for multi-turn context.
|
|
87
|
+
config: Optional ForkConfig to override model selection and limits.
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
A ForkResult containing the synthesis, individual fork outputs,
|
|
91
|
+
and token usage statistics.
|
|
92
|
+
"""
|
|
93
|
+
config = config or ForkConfig()
|
|
94
|
+
manager = ForkManager(config)
|
|
95
|
+
engine = SynthesisEngine(config)
|
|
96
|
+
|
|
97
|
+
overall_start = time.perf_counter()
|
|
98
|
+
|
|
99
|
+
# Create or use provided forks
|
|
100
|
+
if forks is not None:
|
|
101
|
+
# Advanced: caller provided pre-built Fork objects
|
|
102
|
+
fork_list = forks
|
|
103
|
+
elif stances is not None:
|
|
104
|
+
# Caller specified explicit stance names → use built-ins / custom prompts
|
|
105
|
+
fork_list = manager.create_forks(prompt, stances)
|
|
106
|
+
elif config.use_dynamic_stances:
|
|
107
|
+
# Default: let AI invent the best stances for this prompt
|
|
108
|
+
selector = StanceSelector(config)
|
|
109
|
+
selected = await selector.select(prompt, fork_count, history)
|
|
110
|
+
fork_list = selector.to_forks(selected)
|
|
111
|
+
else:
|
|
112
|
+
# Dynamic stances disabled → use configured default stances
|
|
113
|
+
resolved_stances = list(BUILT_IN_STANCES.keys())[:fork_count]
|
|
114
|
+
fork_list = manager.create_forks(prompt, resolved_stances)
|
|
115
|
+
|
|
116
|
+
# Run forks in parallel
|
|
117
|
+
fork_list = await manager.run_parallel(fork_list, prompt, history)
|
|
118
|
+
|
|
119
|
+
# Synthesize
|
|
120
|
+
synthesis_text, synthesis_tokens, synthesis_duration = (
|
|
121
|
+
await engine.synthesize(prompt, fork_list, history)
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
# Build result
|
|
125
|
+
total_fork_tokens = sum(f.token_count for f in fork_list)
|
|
126
|
+
total_tokens = total_fork_tokens + synthesis_tokens
|
|
127
|
+
overall_duration = int((time.perf_counter() - overall_start) * 1000)
|
|
128
|
+
|
|
129
|
+
return ForkResult(
|
|
130
|
+
synthesis=synthesis_text,
|
|
131
|
+
forks={f.stance: f.output for f in fork_list},
|
|
132
|
+
fork_details=[
|
|
133
|
+
{
|
|
134
|
+
"id": f.id,
|
|
135
|
+
"stance": f.stance,
|
|
136
|
+
"output": f.output,
|
|
137
|
+
"token_count": f.token_count,
|
|
138
|
+
"duration_ms": f.duration_ms,
|
|
139
|
+
}
|
|
140
|
+
for f in fork_list
|
|
141
|
+
],
|
|
142
|
+
token_usage={
|
|
143
|
+
"forks": total_fork_tokens,
|
|
144
|
+
"synthesis": synthesis_tokens,
|
|
145
|
+
"total": total_tokens,
|
|
146
|
+
},
|
|
147
|
+
duration_ms=overall_duration,
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
__all__ = [
|
|
152
|
+
"synthesize",
|
|
153
|
+
"Fork",
|
|
154
|
+
"ForkConfig",
|
|
155
|
+
"ForkManager",
|
|
156
|
+
"ForkResult",
|
|
157
|
+
"SelectedStance",
|
|
158
|
+
"StanceSelector",
|
|
159
|
+
"SynthesisEngine",
|
|
160
|
+
"BUILT_IN_STANCES",
|
|
161
|
+
"FORK_OUTPUT_TEMPLATE",
|
|
162
|
+
"SYNTHESIS_SYSTEM_PROMPT",
|
|
163
|
+
"SYNTHESIS_USER_TEMPLATE",
|
|
164
|
+
"get_stance_prompt",
|
|
165
|
+
]
|
thought_fork/cli.py
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# Copyright 2026 Ameen Saeed
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""
|
|
16
|
+
Thought Fork — Command-line interface.
|
|
17
|
+
|
|
18
|
+
Usage:
|
|
19
|
+
thought-fork "Should I use microservices?" --forks 3
|
|
20
|
+
thought-fork "Review this architecture" --forks 4 --static
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import argparse
|
|
26
|
+
import asyncio
|
|
27
|
+
import sys
|
|
28
|
+
import time
|
|
29
|
+
|
|
30
|
+
from dotenv import load_dotenv
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# Fork colors for terminal output (ANSI escape codes)
|
|
34
|
+
FORK_COLORS = [
|
|
35
|
+
"\033[94m", # Blue
|
|
36
|
+
"\033[93m", # Yellow/Amber
|
|
37
|
+
"\033[91m", # Red
|
|
38
|
+
"\033[92m", # Green
|
|
39
|
+
"\033[95m", # Magenta
|
|
40
|
+
"\033[96m", # Cyan
|
|
41
|
+
"\033[90m", # Gray
|
|
42
|
+
]
|
|
43
|
+
RESET = "\033[0m"
|
|
44
|
+
BOLD = "\033[1m"
|
|
45
|
+
DIM = "\033[2m"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _print_banner() -> None:
|
|
49
|
+
"""Print the Thought Fork banner."""
|
|
50
|
+
print()
|
|
51
|
+
print(f"{BOLD}🔀 Thought Fork — Branch your AI's reasoning{RESET}")
|
|
52
|
+
print()
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _print_fork_result(fork_detail: dict, color: str) -> None:
|
|
56
|
+
"""Print a single fork's output with styled header."""
|
|
57
|
+
print()
|
|
58
|
+
print(f"{color}{BOLD}{'═' * 60}{RESET}")
|
|
59
|
+
print(f"{color}{BOLD} Fork {fork_detail['id']} — {fork_detail['stance']}{RESET}")
|
|
60
|
+
print(f"{color}{BOLD}{'═' * 60}{RESET}")
|
|
61
|
+
print()
|
|
62
|
+
print(fork_detail["output"])
|
|
63
|
+
print()
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _print_synthesis(synthesis_text: str) -> None:
|
|
67
|
+
"""Print the synthesis output with styled header."""
|
|
68
|
+
print()
|
|
69
|
+
print(f"{BOLD}{'═' * 60}{RESET}")
|
|
70
|
+
print(f"{BOLD} ✦ Synthesis (Convergence){RESET}")
|
|
71
|
+
print(f"{BOLD}{'═' * 60}{RESET}")
|
|
72
|
+
print()
|
|
73
|
+
print(synthesis_text)
|
|
74
|
+
print()
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _print_summary(fork_details: list[dict], token_usage: dict, duration_ms: int) -> None:
|
|
78
|
+
"""Print token usage summary."""
|
|
79
|
+
print(f"{BOLD}📊 Token Usage Summary{RESET}")
|
|
80
|
+
print(f"┌──────┬──────────────────────────────┬────────┬──────────┐")
|
|
81
|
+
print(f"│ Fork │ Stance │ Tokens │ Duration │")
|
|
82
|
+
print(f"├──────┼──────────────────────────────┼────────┼──────────┤")
|
|
83
|
+
|
|
84
|
+
for detail in fork_details:
|
|
85
|
+
duration_s = detail["duration_ms"] / 1000
|
|
86
|
+
stance = detail["stance"][:28]
|
|
87
|
+
print(
|
|
88
|
+
f"│ {detail['id']:<4} │ {stance:<28} │ {detail['token_count']:>6} │ {duration_s:>6.1f}s │"
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
print(f"├──────┼──────────────────────────────┼────────┼──────────┤")
|
|
92
|
+
total_s = duration_ms / 1000
|
|
93
|
+
print(
|
|
94
|
+
f"│ {'ALL':<4} │ {'TOTAL':<28} │ {token_usage['total']:>6} │ {total_s:>6.1f}s │"
|
|
95
|
+
)
|
|
96
|
+
print(f"└──────┴──────────────────────────────┴────────┴──────────┘")
|
|
97
|
+
print()
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
async def _run(prompt: str, fork_count: int, use_dynamic: bool) -> None:
|
|
101
|
+
"""Execute a Thought Fork session."""
|
|
102
|
+
from thought_fork import ForkConfig, synthesize
|
|
103
|
+
|
|
104
|
+
config = ForkConfig(use_dynamic_stances=use_dynamic)
|
|
105
|
+
|
|
106
|
+
_print_banner()
|
|
107
|
+
print(f"{BOLD}Prompt:{RESET} {prompt}")
|
|
108
|
+
print(f"{DIM}Forks: {fork_count} | Dynamic stances: {'on' if use_dynamic else 'off'}{RESET}")
|
|
109
|
+
print()
|
|
110
|
+
print("⏳ Forking...")
|
|
111
|
+
|
|
112
|
+
result = await synthesize(prompt, fork_count=fork_count, config=config)
|
|
113
|
+
|
|
114
|
+
# Print each fork
|
|
115
|
+
for i, detail in enumerate(result.fork_details):
|
|
116
|
+
color = FORK_COLORS[i % len(FORK_COLORS)]
|
|
117
|
+
_print_fork_result(detail, color)
|
|
118
|
+
|
|
119
|
+
# Print synthesis
|
|
120
|
+
_print_synthesis(result.synthesis)
|
|
121
|
+
|
|
122
|
+
# Print summary
|
|
123
|
+
_print_summary(result.fork_details, result.token_usage, result.duration_ms)
|
|
124
|
+
|
|
125
|
+
print(
|
|
126
|
+
f"{DIM}Forks ran in parallel — wall-clock time is the slowest fork, "
|
|
127
|
+
f"not the sum.{RESET}"
|
|
128
|
+
)
|
|
129
|
+
print()
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def main() -> None:
|
|
133
|
+
"""CLI entry point for Thought Fork."""
|
|
134
|
+
# Ensure UTF-8 output on Windows terminals
|
|
135
|
+
if sys.platform == "win32":
|
|
136
|
+
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
|
137
|
+
sys.stderr.reconfigure(encoding="utf-8", errors="replace")
|
|
138
|
+
|
|
139
|
+
load_dotenv()
|
|
140
|
+
|
|
141
|
+
parser = argparse.ArgumentParser(
|
|
142
|
+
prog="thought-fork",
|
|
143
|
+
description="🔀 Thought Fork — Branch your AI's reasoning like Git branches.",
|
|
144
|
+
epilog="Example: thought-fork \"Should I use microservices?\" --forks 3",
|
|
145
|
+
)
|
|
146
|
+
parser.add_argument(
|
|
147
|
+
"prompt",
|
|
148
|
+
help="The question or problem to fork into parallel reasoning paths.",
|
|
149
|
+
)
|
|
150
|
+
parser.add_argument(
|
|
151
|
+
"--forks", "-n",
|
|
152
|
+
type=int,
|
|
153
|
+
default=3,
|
|
154
|
+
help="Number of parallel forks to spawn (default: 3).",
|
|
155
|
+
)
|
|
156
|
+
parser.add_argument(
|
|
157
|
+
"--static",
|
|
158
|
+
action="store_true",
|
|
159
|
+
help="Use static built-in stances instead of AI-selected dynamic stances.",
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
args = parser.parse_args()
|
|
163
|
+
|
|
164
|
+
try:
|
|
165
|
+
asyncio.run(_run(args.prompt, args.forks, use_dynamic=not args.static))
|
|
166
|
+
except KeyboardInterrupt:
|
|
167
|
+
print(f"\n{DIM}Cancelled.{RESET}")
|
|
168
|
+
sys.exit(0)
|
|
169
|
+
except ValueError as e:
|
|
170
|
+
print(f"\n❌ {e}")
|
|
171
|
+
sys.exit(1)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
if __name__ == "__main__":
|
|
175
|
+
main()
|
thought_fork/config.py
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# Copyright 2026 Ameen Saeed
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""
|
|
16
|
+
Thought Fork — Configuration and built-in stance definitions.
|
|
17
|
+
|
|
18
|
+
This module defines the ForkConfig dataclass for controlling fork/synthesis
|
|
19
|
+
behavior, and the BUILT_IN_STANCES dictionary mapping stance names to their
|
|
20
|
+
system prompts.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
import os
|
|
24
|
+
from dataclasses import dataclass, field
|
|
25
|
+
from typing import Any
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# ---------------------------------------------------------------------------
|
|
29
|
+
# Built-in Stances
|
|
30
|
+
# ---------------------------------------------------------------------------
|
|
31
|
+
# Each stance is a system prompt that biases a fork's reasoning toward a
|
|
32
|
+
# specific perspective. These are the stances defined as part of the
|
|
33
|
+
# Thought Fork framework (see CONCEPT.md).
|
|
34
|
+
|
|
35
|
+
BUILT_IN_STANCES: dict[str, str] = {
|
|
36
|
+
"cautious": (
|
|
37
|
+
"You are a seasoned risk analyst and cautious strategist. "
|
|
38
|
+
"Your methodology: identify every risk, edge case, failure mode, and second-order consequence BEFORE considering benefits. "
|
|
39
|
+
"You think in terms of 'what kills us?' not 'what helps us?' "
|
|
40
|
+
"Reason with precision. Use specific examples of real-world failures to ground your analysis. "
|
|
41
|
+
"Provide concrete safeguards and mitigation strategies, not vague warnings. "
|
|
42
|
+
"Your standard of evidence is high — demand data, precedent, or logical proof for every claim."
|
|
43
|
+
),
|
|
44
|
+
"creative": (
|
|
45
|
+
"You are a lateral thinker and innovation strategist with deep expertise in analogical reasoning. "
|
|
46
|
+
"Your methodology: actively reject the first 3 obvious answers and force yourself to explore non-obvious angles. "
|
|
47
|
+
"Draw analogies from completely different domains — biology, game theory, art, history, physics — to illuminate the problem from unexpected directions. "
|
|
48
|
+
"Challenge the framing of the question itself. Propose solutions that feel counterintuitive but are logically sound. "
|
|
49
|
+
"Your output should make the reader say 'I never would have thought of that.' "
|
|
50
|
+
"Be specific and substantive — creative does NOT mean vague."
|
|
51
|
+
),
|
|
52
|
+
"critical": (
|
|
53
|
+
"You are an adversarial critic and intellectual stress-tester. "
|
|
54
|
+
"Your methodology: assume the prevailing answer is wrong and rigorously argue why. "
|
|
55
|
+
"Challenge every premise, every assumption, every piece of 'conventional wisdom.' "
|
|
56
|
+
"Use formal logical analysis — identify specific fallacies, hidden assumptions, and gaps in evidence. "
|
|
57
|
+
"You are not cynical; you are surgically precise. Your goal is to make the argument stronger by finding its weakest links. "
|
|
58
|
+
"Quote specific claims and show exactly where they fail under scrutiny."
|
|
59
|
+
),
|
|
60
|
+
"pragmatic": (
|
|
61
|
+
"You are a battle-tested execution strategist and operational pragmatist. "
|
|
62
|
+
"Your methodology: ruthlessly filter for what actually works in practice given real-world constraints. "
|
|
63
|
+
"Ignore theoretical elegance — focus on implementation complexity, resource requirements, timeline, and ROI. "
|
|
64
|
+
"Provide concrete action plans with specific steps, not abstract frameworks. "
|
|
65
|
+
"Draw on real examples of successful execution in similar situations. "
|
|
66
|
+
"Your output should be immediately actionable by someone with a budget and a deadline."
|
|
67
|
+
),
|
|
68
|
+
"first-principles": (
|
|
69
|
+
"You are a first-principles thinker in the tradition of Feynman and Musk. "
|
|
70
|
+
"Your methodology: strip away every assumption, convention, and inherited belief until you reach bedrock truths. "
|
|
71
|
+
"Then rebuild the answer from those fundamentals using rigorous logical chains. "
|
|
72
|
+
"Explicitly name each assumption you're discarding and explain why it might be wrong. "
|
|
73
|
+
"Your reasoning should be transparent — show every step from axiom to conclusion. "
|
|
74
|
+
"If the conventional answer happens to be right, prove it from first principles rather than accepting it on authority."
|
|
75
|
+
),
|
|
76
|
+
"optimistic": (
|
|
77
|
+
"You are a strategic optimist and opportunity analyst. "
|
|
78
|
+
"Your methodology: map the full landscape of upside potential, best-case trajectories, and compounding advantages. "
|
|
79
|
+
"Identify catalysts and tailwinds that others miss because they're focused on risks. "
|
|
80
|
+
"Your optimism is NOT naive — it is grounded in specific evidence, precedent, and logical reasoning about why things could go right. "
|
|
81
|
+
"Provide a concrete roadmap for capturing the identified opportunities. "
|
|
82
|
+
"Show how small advantages compound into transformative outcomes."
|
|
83
|
+
),
|
|
84
|
+
"contrarian": (
|
|
85
|
+
"You are a professional contrarian and independent thinker. "
|
|
86
|
+
"Your methodology: identify the consensus view, then rigorously build the strongest possible case for the opposite position. "
|
|
87
|
+
"Find historical examples where the contrarian view was correct and the consensus was catastrophically wrong. "
|
|
88
|
+
"You are not being contrarian for its own sake — you genuinely believe the best insights hide in unpopular positions. "
|
|
89
|
+
"Steel-man the opposing view before attacking the consensus. "
|
|
90
|
+
"Your output should make the reader seriously reconsider what they thought they knew."
|
|
91
|
+
),
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
# ---------------------------------------------------------------------------
|
|
96
|
+
# ForkConfig
|
|
97
|
+
# ---------------------------------------------------------------------------
|
|
98
|
+
|
|
99
|
+
@dataclass
|
|
100
|
+
class ForkConfig:
|
|
101
|
+
"""Configuration for the Thought Fork engine.
|
|
102
|
+
|
|
103
|
+
Attributes:
|
|
104
|
+
fork_model: Model identifier for fork reasoning (fast/cheap).
|
|
105
|
+
synthesis_model: Model identifier for synthesis (quality).
|
|
106
|
+
stance_selector_model: Model used for dynamic stance selection (fast/cheap).
|
|
107
|
+
default_stances: List of stance names used when none are specified and
|
|
108
|
+
dynamic stances are disabled.
|
|
109
|
+
max_tokens: Maximum tokens per fork/synthesis response.
|
|
110
|
+
api_base_url: Base URL for the API (default: OpenRouter).
|
|
111
|
+
api_key: API key for the provider. If None, attempts to resolve from
|
|
112
|
+
OPENAI_API_KEY or OPENROUTER_API_KEY environment variables.
|
|
113
|
+
use_dynamic_stances: If True (default), uses AI to invent custom stances
|
|
114
|
+
for each prompt. If False, uses default_stances (static built-ins).
|
|
115
|
+
timeout_seconds: Maximum time in seconds to wait for a single API call.
|
|
116
|
+
max_concurrent_forks: Maximum number of forks to run simultaneously.
|
|
117
|
+
max_retries: Maximum number of times to retry a failed API call.
|
|
118
|
+
client: Optional pre-configured AsyncOpenAI client instance.
|
|
119
|
+
"""
|
|
120
|
+
|
|
121
|
+
fork_model: str = "anthropic/claude-haiku-4.5"
|
|
122
|
+
synthesis_model: str = "anthropic/claude-sonnet-4-6"
|
|
123
|
+
stance_selector_model: str = "anthropic/claude-haiku-4.5"
|
|
124
|
+
default_stances: list[str] = field(
|
|
125
|
+
default_factory=lambda: ["cautious", "creative", "critical"]
|
|
126
|
+
)
|
|
127
|
+
available_models: list[str] = field(default_factory=list)
|
|
128
|
+
max_tokens: int = 2048
|
|
129
|
+
synthesis_max_tokens: int = 6144
|
|
130
|
+
api_base_url: str = "https://openrouter.ai/api/v1"
|
|
131
|
+
api_key: str | None = None
|
|
132
|
+
use_dynamic_stances: bool = True
|
|
133
|
+
timeout_seconds: float = 120.0
|
|
134
|
+
max_concurrent_forks: int = 5
|
|
135
|
+
max_retries: int = 2
|
|
136
|
+
client: Any | None = None
|
|
137
|
+
|
|
138
|
+
def __post_init__(self):
|
|
139
|
+
"""Resolve API key and overrides from environment if not explicitly provided."""
|
|
140
|
+
if not self.api_key:
|
|
141
|
+
self.api_key = os.getenv("THOUGHT_FORK_API_KEY") or os.getenv("OPENROUTER_API_KEY") or os.getenv("OPENAI_API_KEY")
|
|
142
|
+
|
|
143
|
+
if os.getenv("THOUGHT_FORK_API_BASE"):
|
|
144
|
+
self.api_base_url = os.getenv("THOUGHT_FORK_API_BASE")
|
|
145
|
+
|
|
146
|
+
if os.getenv("THOUGHT_FORK_MODEL"):
|
|
147
|
+
self.fork_model = os.getenv("THOUGHT_FORK_MODEL")
|
|
148
|
+
|
|
149
|
+
if os.getenv("THOUGHT_FORK_SYNTHESIS_MODEL"):
|
|
150
|
+
self.synthesis_model = os.getenv("THOUGHT_FORK_SYNTHESIS_MODEL")
|
|
151
|
+
elif os.getenv("THOUGHT_FORK_MODEL"):
|
|
152
|
+
self.synthesis_model = os.getenv("THOUGHT_FORK_MODEL")
|
|
153
|
+
|
|
154
|
+
if os.getenv("THOUGHT_FORK_SELECTOR_MODEL"):
|
|
155
|
+
self.stance_selector_model = os.getenv("THOUGHT_FORK_SELECTOR_MODEL")
|
|
156
|
+
elif os.getenv("THOUGHT_FORK_MODEL"):
|
|
157
|
+
self.stance_selector_model = os.getenv("THOUGHT_FORK_MODEL")
|
|
158
|
+
|
|
159
|
+
if os.getenv("THOUGHT_FORK_AVAILABLE_MODELS"):
|
|
160
|
+
models = os.getenv("THOUGHT_FORK_AVAILABLE_MODELS").split(",")
|
|
161
|
+
self.available_models = [m.strip() for m in models if m.strip()]
|
|
162
|
+
|
|
163
|
+
# Check if URL is local (Ollama/vLLM)
|
|
164
|
+
is_local = any(host in self.api_base_url for host in ["localhost", "127.0.0.1", "0.0.0.0"])
|
|
165
|
+
|
|
166
|
+
if not self.api_key and not is_local:
|
|
167
|
+
raise ValueError(
|
|
168
|
+
"No API key found. Set OPENROUTER_API_KEY or OPENAI_API_KEY "
|
|
169
|
+
"in your environment, or pass api_key= to ForkConfig(). "
|
|
170
|
+
"Note: API key is not required for local URLs (localhost/127.0.0.1)."
|
|
171
|
+
)
|
thought_fork/fork.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Copyright 2026 Ameen Saeed
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""
|
|
16
|
+
Thought Fork — Fork dataclass and stance resolution.
|
|
17
|
+
|
|
18
|
+
A Fork represents a single parallel reasoning path. Each fork has a stance
|
|
19
|
+
(its reasoning perspective) and a system prompt that biases the AI toward
|
|
20
|
+
that stance.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from dataclasses import dataclass
|
|
24
|
+
|
|
25
|
+
from thought_fork.config import BUILT_IN_STANCES
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class Fork:
|
|
30
|
+
"""A single parallel reasoning path in the Thought Fork framework.
|
|
31
|
+
|
|
32
|
+
Attributes:
|
|
33
|
+
id: Letter identifier for this fork ("A", "B", "C", ...).
|
|
34
|
+
stance: The reasoning perspective (e.g., "cautious", "creative").
|
|
35
|
+
system_prompt: The full system prompt that biases reasoning.
|
|
36
|
+
output: The fork's generated reasoning output (populated after execution).
|
|
37
|
+
token_count: Number of tokens used in the response (populated after execution).
|
|
38
|
+
duration_ms: Execution time in milliseconds (populated after execution).
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
id: str
|
|
42
|
+
stance: str
|
|
43
|
+
system_prompt: str
|
|
44
|
+
output: str = ""
|
|
45
|
+
token_count: int = 0
|
|
46
|
+
duration_ms: int = 0
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def get_stance_prompt(stance: str) -> str:
|
|
50
|
+
"""Resolve a stance name to its system prompt.
|
|
51
|
+
|
|
52
|
+
If the stance name matches a built-in stance, returns that prompt.
|
|
53
|
+
Otherwise, treats the stance string itself as a custom system prompt.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
stance: A built-in stance name (e.g., "cautious") or a custom prompt string.
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
The resolved system prompt for the given stance.
|
|
60
|
+
"""
|
|
61
|
+
return BUILT_IN_STANCES.get(stance, stance)
|