File size: 4,196 Bytes
1c02ba0 3ff18bb 2f9b16e b67bec7 34b75f2 3ff18bb 1c02ba0 b67bec7 1c02ba0 b67bec7 1c02ba0 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 | #!/usr/bin/env python3
"""FR-Start β Fahrenheit Research incorporation advisor CLI (fully local, no API).
Runs on Ollama. Usage:
python fr_start.py interactive chat
python fr_start.py "question" one-shot answer
"""
import sys
from pathlib import Path
import ollama
ROOT = Path(__file__).parent
# ponytail: gemma3:12b β strongest model already on this machine; drop to qwen2.5:7b-instruct if too slow
MODEL = "gemma3:12b"
NUM_CTX = 32768 # system prompt is ~13k tokens; default ctx would silently truncate the corpus
# ponytail: whole corpus in context (~30KB), add retrieval when corpus > ~200KB
def build_system() -> str:
prompt = (ROOT / "system_prompt.md").read_text()
corpus = "\n\n---\n\n".join(
f.read_text() for f in sorted((ROOT / "corpus").glob("*.md"))
)
# scope rule repeated after the corpus β small models weight the end of long prompts
tail = (
"# Final rule (absolute)\n"
"Decision procedure for every user message, in this order:\n"
"1. Does it involve companies, incorporation, entities, taxes or tax rates, "
"compliance, banking, payroll, founder visas, funding, grants, cross-border "
"structures, or exits β in or between the US, India, UAE, Singapore, or UK? "
"If YES: answer it from the corpus. This includes short factual questions like "
"'What is Singapore's corporate tax rate?' or 'Which Dubai zone for fintech?'. "
"NEVER give the refusal reply to these.\n"
"2. Only if the message is clearly unrelated (code, poems, trivia, math, health, "
"other countries, casual chat): give the standard FR-Start reply from the Scope "
"section, nothing else. The standard reply is always the ENTIRE response β "
"never append it before or after an answer, and never use it when you have "
"answered the question.\n"
"Formatting: simple markdown β bold key terms, '- ' lists, and small markdown "
"tables for comparisons and the Decision card."
)
return f"{prompt}\n\n# Reference corpus\n\n{corpus}\n\n{tail}"
MARKER = "This is FR-Start" # opening of the standard scope-refusal reply
def stream_reply(messages: list[dict]):
"""Stream the model's reply; cut off a scope-refusal wrongly appended after a real answer.
ponytail: 12B model sometimes tacks the refusal boilerplate onto valid answers β
enforcing in code beats another prompt nudge. Holds back a small tail so the
marker can't slip through split across chunks.
"""
pending = ""
hold = len(MARKER) + 8
for chunk in ollama.chat(model=MODEL, messages=messages, stream=True,
options={"num_ctx": NUM_CTX, "temperature": 0}):
pending += chunk["message"]["content"]
i = pending.find(MARKER)
if i > 0 and pending[:i].strip():
yield pending[:i].rstrip() # real answer followed by boilerplate β truncate
return
if i != 0 and len(pending) > hold:
yield pending[:-hold]
pending = pending[-hold:]
yield pending
def chat() -> None:
system = build_system()
messages: list[dict] = [{"role": "system", "content": system}]
one_shot = sys.argv[1] if len(sys.argv) > 1 else None
print(f"FR-Start (local: {MODEL}) β where should you incorporate? (US / India / UAE / Singapore / UK)")
print("Ctrl-C or 'quit' to exit. First answer is slow while the model loads.\n")
while True:
if one_shot:
user = one_shot
else:
try:
user = input("you> ").strip()
except (EOFError, KeyboardInterrupt):
print()
return
if not user or user.lower() in ("quit", "exit"):
return
messages.append({"role": "user", "content": user})
print()
reply = ""
for piece in stream_reply(messages):
reply += piece
print(piece, end="", flush=True)
print("\n")
messages.append({"role": "assistant", "content": reply})
if one_shot:
return
if __name__ == "__main__":
chat()
|