p4r5kpftnp-cmd commited on
Commit
1145e92
·
1 Parent(s): df4e015

Add Claude API support (users supply their own key)

Browse files

Users can now pick a Claude model (Opus 4.7, Sonnet 4.6, Haiku 4.5) from
the model dropdown and paste their own Anthropic API key into a password
field above the editors. Groq remains the default (using the Space's
GROQ_API_KEY env var).

Implementation:
- `RAGProofChain` picks provider from model name prefix:
`claude-*` → ChatAnthropic, everything else → ChatGroq
- `api_key` parameter threaded through LangGraphAgent → RAGProofChain
so user-provided keys reach the LangChain client directly (never set
as an env var, so concurrent calls don't clobber each other)
- `solve_proof` validates: Claude models require the key field; Groq
models still rely on the Space's env var
- New deps: `langchain-anthropic`
- Test mocks accept **kwargs so the existing fuzz/concurrent suites still
pass with the new constructor signature

The Anthropic key is sent over HTTPS to the Space backend then to
Anthropic; it is not stored on disk and is not logged.

app.py CHANGED
@@ -81,6 +81,18 @@ GROQ_MODELS = [
81
  "llama-3.1-8b-instant",
82
  ]
83
 
 
 
 
 
 
 
 
 
 
 
 
 
84
  EXAMPLE_CODE = """\
85
  import Mathlib
86
 
@@ -336,12 +348,19 @@ input[type="range"] { accent-color: var(--accent) !important; }
336
  """
337
 
338
 
339
- def solve_proof(lean_code: str, model_name: str, max_retries: int):
340
  if not lean_code.strip():
341
  return _status_html("idle", "No input — paste a Lean 4 theorem on the left."), "", ""
342
 
343
- if not os.environ.get("GROQ_API_KEY"):
344
- return _status_html("err", "GROQ_API_KEY missing — add it as a Space secret."), "", ""
 
 
 
 
 
 
 
345
 
346
  tmp_path = None
347
  try:
@@ -354,7 +373,11 @@ def solve_proof(lean_code: str, model_name: str, max_retries: int):
354
 
355
  log_buf = io.StringIO()
356
  with _capture_stdout(log_buf):
357
- agent = LangGraphAgent(model_name=model_name, max_retries=int(max_retries))
 
 
 
 
358
  result = agent.solve_file_detailed(tmp_path)
359
 
360
  with open(tmp_path, encoding="utf-8") as f:
@@ -415,13 +438,21 @@ with gr.Blocks(
415
  # ─── Controls bar ───────────────────────────────────────────────
416
  with gr.Row(elem_id="controls"):
417
  model_dropdown = gr.Dropdown(
418
- choices=GROQ_MODELS, value=GROQ_MODELS[0],
419
  label="Model", show_label=True, container=False, scale=2,
420
  )
421
  retries_slider = gr.Slider(
422
  minimum=1, maximum=10, value=5, step=1,
423
  label="Max retries", show_label=True, container=False, scale=1,
424
  )
 
 
 
 
 
 
 
 
425
 
426
  # ─── Two editor panes ───────────────────────────────────────────
427
  with gr.Row(elem_id="editor-row", equal_height=True):
@@ -462,12 +493,12 @@ with gr.Blocks(
462
  # ─── Wiring ─────────────────────────────────────────────────────
463
  solve_btn.click(
464
  solve_proof,
465
- inputs=[lean_input, model_dropdown, retries_slider],
466
  outputs=[status_output, code_output, logs_output],
467
  )
468
  regen_btn.click(
469
  solve_proof,
470
- inputs=[lean_input, model_dropdown, retries_slider],
471
  outputs=[status_output, code_output, logs_output],
472
  )
473
  reset_btn.click(lambda: EXAMPLE_CODE, outputs=lean_input)
 
81
  "llama-3.1-8b-instant",
82
  ]
83
 
84
+ CLAUDE_MODELS = [
85
+ "claude-opus-4-7",
86
+ "claude-sonnet-4-6",
87
+ "claude-haiku-4-5-20251001",
88
+ ]
89
+
90
+ ALL_MODELS = GROQ_MODELS + CLAUDE_MODELS
91
+
92
+
93
+ def _is_claude(model_name: str) -> bool:
94
+ return model_name.startswith("claude-")
95
+
96
  EXAMPLE_CODE = """\
97
  import Mathlib
98
 
 
348
  """
349
 
350
 
351
+ def solve_proof(lean_code: str, model_name: str, max_retries: int, anthropic_api_key: str = ""):
352
  if not lean_code.strip():
353
  return _status_html("idle", "No input — paste a Lean 4 theorem on the left."), "", ""
354
 
355
+ claude = _is_claude(model_name)
356
+ if claude:
357
+ if not anthropic_api_key or not anthropic_api_key.strip():
358
+ return _status_html("err", "Anthropic API key required for Claude models — paste it above."), "", ""
359
+ api_key = anthropic_api_key.strip()
360
+ else:
361
+ if not os.environ.get("GROQ_API_KEY"):
362
+ return _status_html("err", "GROQ_API_KEY missing — add it as a Space secret."), "", ""
363
+ api_key = None # ChatGroq picks it up from env
364
 
365
  tmp_path = None
366
  try:
 
373
 
374
  log_buf = io.StringIO()
375
  with _capture_stdout(log_buf):
376
+ agent = LangGraphAgent(
377
+ model_name=model_name,
378
+ max_retries=int(max_retries),
379
+ api_key=api_key,
380
+ )
381
  result = agent.solve_file_detailed(tmp_path)
382
 
383
  with open(tmp_path, encoding="utf-8") as f:
 
438
  # ─── Controls bar ───────────────────────────────────────────────
439
  with gr.Row(elem_id="controls"):
440
  model_dropdown = gr.Dropdown(
441
+ choices=ALL_MODELS, value=GROQ_MODELS[0],
442
  label="Model", show_label=True, container=False, scale=2,
443
  )
444
  retries_slider = gr.Slider(
445
  minimum=1, maximum=10, value=5, step=1,
446
  label="Max retries", show_label=True, container=False, scale=1,
447
  )
448
+ anthropic_key_input = gr.Textbox(
449
+ label="Anthropic API key (only for Claude models)",
450
+ placeholder="sk-ant-…",
451
+ type="password",
452
+ show_label=True,
453
+ container=False,
454
+ scale=2,
455
+ )
456
 
457
  # ─── Two editor panes ───────────────────────────────────────────
458
  with gr.Row(elem_id="editor-row", equal_height=True):
 
493
  # ─── Wiring ─────────────────────────────────────────────────────
494
  solve_btn.click(
495
  solve_proof,
496
+ inputs=[lean_input, model_dropdown, retries_slider, anthropic_key_input],
497
  outputs=[status_output, code_output, logs_output],
498
  )
499
  regen_btn.click(
500
  solve_proof,
501
+ inputs=[lean_input, model_dropdown, retries_slider, anthropic_key_input],
502
  outputs=[status_output, code_output, logs_output],
503
  )
504
  reset_btn.click(lambda: EXAMPLE_CODE, outputs=lean_input)
requirements.txt CHANGED
@@ -3,6 +3,7 @@ groq
3
  langchain
4
  langchain-community
5
  langchain-groq
 
6
  langchain-huggingface
7
  langchain-classic
8
  langgraph
 
3
  langchain
4
  langchain-community
5
  langchain-groq
6
+ langchain-anthropic
7
  langchain-huggingface
8
  langchain-classic
9
  langgraph
src/langgraph_agent.py CHANGED
@@ -212,10 +212,11 @@ class LangGraphAgent:
212
  model_name: str = "llama-3.3-70b-versatile",
213
  max_retries: int = 5,
214
  index_dir: str | None = None,
 
215
  ):
216
  self._lean_env = LeanEnvironment(use_mathlib=True)
217
  self._retriever = MathLibRetriever(index_dir=index_dir)
218
- self._chain = RAGProofChain(model_name=model_name)
219
  self._graph = build_graph(self._lean_env, self._retriever, self._chain)
220
  self._max_retries = max_retries
221
 
 
212
  model_name: str = "llama-3.3-70b-versatile",
213
  max_retries: int = 5,
214
  index_dir: str | None = None,
215
+ api_key: str | None = None,
216
  ):
217
  self._lean_env = LeanEnvironment(use_mathlib=True)
218
  self._retriever = MathLibRetriever(index_dir=index_dir)
219
+ self._chain = RAGProofChain(model_name=model_name, api_key=api_key)
220
  self._graph = build_graph(self._lean_env, self._retriever, self._chain)
221
  self._max_retries = max_retries
222
 
src/rag_chain.py CHANGED
@@ -1,9 +1,10 @@
1
- from typing import List
2
 
3
  from langchain_core.documents import Document
4
  from langchain_core.output_parsers import StrOutputParser
5
  from langchain_core.prompts import ChatPromptTemplate
6
  from langchain_groq import ChatGroq
 
7
 
8
 
9
  _SYSTEM = """\
@@ -95,6 +96,23 @@ Provide the corrected Lean code that solves all goals and fixes all errors.
95
  """
96
 
97
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
98
  def _format_docs(docs: List[Document]) -> str:
99
  if not docs:
100
  return "(none retrieved)"
@@ -108,12 +126,12 @@ class RAGProofChain:
108
  LangChain LCEL chain: retrieved context + proof state → corrected Lean code.
109
  """
110
 
111
- def __init__(self, model_name: str = "llama-3.3-70b-versatile"):
112
  prompt = ChatPromptTemplate.from_messages([
113
  ("system", _SYSTEM),
114
  ("human", _HUMAN),
115
  ])
116
- llm = ChatGroq(model=model_name, max_tokens=1024)
117
  self._chain = prompt | llm | StrOutputParser()
118
 
119
  def generate(
 
1
+ from typing import List, Optional
2
 
3
  from langchain_core.documents import Document
4
  from langchain_core.output_parsers import StrOutputParser
5
  from langchain_core.prompts import ChatPromptTemplate
6
  from langchain_groq import ChatGroq
7
+ from langchain_anthropic import ChatAnthropic
8
 
9
 
10
  _SYSTEM = """\
 
96
  """
97
 
98
 
99
+ def _make_llm(model_name: str, api_key: Optional[str]):
100
+ """
101
+ Pick the chat LLM provider from the model name:
102
+ - `claude-*` → Anthropic (requires user-provided api_key)
103
+ - everything else → Groq (api_key optional; falls back to GROQ_API_KEY env)
104
+ """
105
+ if model_name.startswith("claude-"):
106
+ kwargs = {"model": model_name, "max_tokens": 1024}
107
+ if api_key:
108
+ kwargs["anthropic_api_key"] = api_key
109
+ return ChatAnthropic(**kwargs)
110
+ kwargs = {"model": model_name, "max_tokens": 1024}
111
+ if api_key:
112
+ kwargs["groq_api_key"] = api_key
113
+ return ChatGroq(**kwargs)
114
+
115
+
116
  def _format_docs(docs: List[Document]) -> str:
117
  if not docs:
118
  return "(none retrieved)"
 
126
  LangChain LCEL chain: retrieved context + proof state → corrected Lean code.
127
  """
128
 
129
+ def __init__(self, model_name: str = "llama-3.3-70b-versatile", api_key: Optional[str] = None):
130
  prompt = ChatPromptTemplate.from_messages([
131
  ("system", _SYSTEM),
132
  ("human", _HUMAN),
133
  ])
134
+ llm = _make_llm(model_name, api_key)
135
  self._chain = prompt | llm | StrOutputParser()
136
 
137
  def generate(
tests/test_app_fuzz.py CHANGED
@@ -48,7 +48,7 @@ class _DummyAgent:
48
  last_path: str | None = None
49
  last_bytes: bytes | None = None
50
 
51
- def __init__(self, model_name="x", max_retries=5):
52
  self.model_name = model_name
53
  self.max_retries = max_retries
54
  _DummyAgent.init_log.append((model_name, max_retries))
 
48
  last_path: str | None = None
49
  last_bytes: bytes | None = None
50
 
51
+ def __init__(self, model_name="x", max_retries=5, **kwargs):
52
  self.model_name = model_name
53
  self.max_retries = max_retries
54
  _DummyAgent.init_log.append((model_name, max_retries))
tests/test_concurrent_solve.py CHANGED
@@ -39,7 +39,7 @@ class _DummyAgent:
39
  # Optional: tag to print so we can detect cross-thread stdout bleed.
40
  print_tag: bool = False
41
 
42
- def __init__(self, model_name="x", max_retries=5):
43
  self.model_name = model_name
44
  self.max_retries = max_retries
45
 
 
39
  # Optional: tag to print so we can detect cross-thread stdout bleed.
40
  print_tag: bool = False
41
 
42
+ def __init__(self, model_name="x", max_retries=5, **kwargs):
43
  self.model_name = model_name
44
  self.max_retries = max_retries
45