Melika Kheirieh commited on
Commit
260d5c1
·
1 Parent(s): e7d7c61

feat(llm): proxy-first fallback, env-only OpenAI client; docs: update .env.example

Browse files
.env.example CHANGED
@@ -1,11 +1,20 @@
1
- # ---- GAPGPT proxy config ----
2
- # If youre using a proxy (e.g., GapGPT, Helicone, LocalAI, etc.),
3
- # set these two values. Otherwise, leave them blank.
4
  PROXY_API_KEY="your-proxy-token-here"
5
- PROXY_BASE_URL="https://api.proxy.app/v1"
 
 
6
 
 
 
 
 
 
7
 
8
- # ---- optional direct OpenAI config (for fallback) ----
9
- # These will be used only if proxy variables are not set.
10
- #OPENAI_API_KEY="your-openai-key-here"
11
- #OPENAI_BASE_URL="https://api.openai.com/v1"
 
 
 
 
1
+ # ---- GAPGPT proxy (preferred if set) ----
2
+ # Set these if you're using GapGPT, Helicone, LocalAI, etc.
 
3
  PROXY_API_KEY="your-proxy-token-here"
4
+ PROXY_BASE_URL="https://api.gapgpt.app/v1"
5
+ # Optional:
6
+ # LLM_MODEL_ID="gpt-4o-mini"
7
 
8
+ # ---- Direct OpenAI fallback ----
9
+ # Only used if PROXY_* are not defined.
10
+ # OPENAI_API_KEY="your-openai-key-here"
11
+ # OPENAI_BASE_URL="https://api.openai.com/v1"
12
+ # OPENAI_MODEL_ID="gpt-4o-mini"
13
 
14
+ # ---- Database config ----
15
+ # DB_MODE can be "sqlite" (default) or "postgres"
16
+ DB_MODE=sqlite
17
+ # POSTGRES_DSN="postgresql+psycopg2://user:password@localhost:5432/demo"
18
+
19
+ # ---- App meta ----
20
+ APP_VERSION=0.1.0
.pre-commit-config.yaml CHANGED
@@ -32,4 +32,4 @@ repos:
32
  entry: make test
33
  language: system
34
  pass_filenames: false
35
- stages: [push]
 
32
  entry: make test
33
  language: system
34
  pass_filenames: false
35
+ stages: [pre-push]
README.md CHANGED
@@ -1,6 +1,6 @@
1
  # 🧩 NL2SQL Copilot
2
 
3
- A modular **Text-to-SQL Copilot** that converts natural language questions into safe and verified SQL queries.
4
  Built with **FastAPI**, **LangGraph**, and **SQLAlchemy**, designed for read-only databases and evaluation on Spider/Dr.Spider benchmarks.
5
 
6
  ---
@@ -96,4 +96,3 @@ mypy .
96
  ## 📄 License
97
 
98
  MIT © 2025 Melika Kheirieh
99
-
 
1
  # 🧩 NL2SQL Copilot
2
 
3
+ A modular **Text-to-SQL Copilot** that converts natural language questions into safe and verified SQL queries.
4
  Built with **FastAPI**, **LangGraph**, and **SQLAlchemy**, designed for read-only databases and evaluation on Spider/Dr.Spider benchmarks.
5
 
6
  ---
 
96
  ## 📄 License
97
 
98
  MIT © 2025 Melika Kheirieh
 
adapters/llm/openai_provider.py CHANGED
@@ -4,21 +4,55 @@ import json
4
  from adapters.llm.base import LLMProvider
5
  from openai import OpenAI
6
 
7
- # NOTE: Read keys/base URL from env. Do NOT pass base_url in constructors.
8
- # - OPENAI_API_KEY (required)
9
- # - OPENAI_BASE_URL (optional; defaults to OpenAI public)
10
- # - OPENAI_MODEL_ID (e.g., "gpt-4o-mini")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
 
12
 
13
  class OpenAIProvider(LLMProvider):
14
  provider_id = "openai"
15
 
16
  def __init__(self) -> None:
17
- self.client = OpenAI(
18
- api_key=os.environ["OPENAI_API_KEY"],
19
- base_url=os.getenv("OPENAI_BASE_URL", "https://api.openai.com/v1"),
20
- )
21
- self.model = os.getenv("OPENAI_MODEL_ID", "gpt-4o-mini")
 
 
22
 
23
  def plan(self, *, user_query, schema_preview):
24
  completion = self.client.chat.completions.create(
@@ -45,7 +79,7 @@ class OpenAIProvider(LLMProvider):
45
  self, *, user_query, schema_preview, plan_text, clarify_answers=None
46
  ):
47
  prompt = f"""
48
- You are a precise SQL generator.
49
  Return ONLY valid JSON with two keys: "sql" and "rationale".
50
  Do not include any markdown, backticks, or extra text.
51
 
@@ -72,12 +106,11 @@ class OpenAIProvider(LLMProvider):
72
  temperature=0,
73
  )
74
  content = completion.choices[0].message.content.strip()
75
- usage = completion.usage # ← لازم داریم
76
  t_in = usage.prompt_tokens if usage else None
77
  t_out = usage.completion_tokens if usage else None
78
  cost = self._estimate_cost(usage) if usage else None
79
 
80
- # Robust JSON parse (with fallback to substring)
81
  try:
82
  parsed = json.loads(content)
83
  except json.JSONDecodeError:
@@ -93,11 +126,9 @@ class OpenAIProvider(LLMProvider):
93
 
94
  sql = (parsed.get("sql") or "").strip()
95
  rationale = parsed.get("rationale") or ""
96
-
97
  if not sql:
98
  raise ValueError("LLM returned empty 'sql'")
99
 
100
- # IMPORTANT: return the expected 5-tuple
101
  return sql, rationale, t_in, t_out, cost
102
 
103
  def repair(self, *, sql, error_msg, schema_preview):
@@ -125,6 +156,5 @@ class OpenAIProvider(LLMProvider):
125
  )
126
 
127
  def _estimate_cost(self, usage):
128
- # Rough estimation example — can be refined with official token pricing
129
  total = usage.prompt_tokens + usage.completion_tokens
130
  return total * 0.000001
 
4
  from adapters.llm.base import LLMProvider
5
  from openai import OpenAI
6
 
7
+ # NOTE:
8
+ # - Prefer proxy if PROXY_API_KEY and PROXY_BASE_URL are set.
9
+ # - Otherwise, fallback to OPENAI_API_KEY (+ OPENAI_BASE_URL defaulting to https://api.openai.com/v1).
10
+ # - Do NOT pass base_url/api_key in the constructor; rely on env vars.
11
+
12
+
13
+ def _resolve_api_config() -> tuple[str, str, str]:
14
+ """
15
+ Returns (api_key, base_url, model_id) according to env.
16
+ Resolution order:
17
+ 1) Proxy: PROXY_API_KEY + PROXY_BASE_URL [+ PROXY_MODEL_ID]
18
+ 2) Direct: OPENAI_API_KEY [+ OPENAI_BASE_URL] [+ OPENAI_MODEL_ID]
19
+ Additionally, LLM_MODEL_ID (if set) overrides model choice.
20
+ """
21
+ # Optional global override for model id
22
+ override_model = os.getenv("LLM_MODEL_ID")
23
+
24
+ proxy_key = os.getenv("PROXY_API_KEY")
25
+ proxy_url = os.getenv("PROXY_BASE_URL")
26
+ if proxy_key and proxy_url:
27
+ model = (
28
+ override_model
29
+ or os.getenv("PROXY_MODEL_ID")
30
+ or os.getenv("OPENAI_MODEL_ID")
31
+ or "gpt-4o-mini"
32
+ )
33
+ return proxy_key, proxy_url, model
34
+
35
+ openai_key = os.getenv("OPENAI_API_KEY")
36
+ if not openai_key:
37
+ raise RuntimeError(
38
+ "No API credentials found. Set either PROXY_API_KEY/PROXY_BASE_URL or OPENAI_API_KEY."
39
+ )
40
+ openai_url = os.getenv("OPENAI_BASE_URL", "https://api.openai.com/v1")
41
+ model = override_model or os.getenv("OPENAI_MODEL_ID") or "gpt-4o-mini"
42
+ return openai_key, openai_url, model
43
 
44
 
45
  class OpenAIProvider(LLMProvider):
46
  provider_id = "openai"
47
 
48
  def __init__(self) -> None:
49
+ # Resolve and export to env so we don't pass into constructor.
50
+ api_key, base_url, model = _resolve_api_config()
51
+ os.environ["OPENAI_API_KEY"] = api_key
52
+ os.environ["OPENAI_BASE_URL"] = base_url
53
+ # Create client using env only
54
+ self.client = OpenAI()
55
+ self.model = model
56
 
57
  def plan(self, *, user_query, schema_preview):
58
  completion = self.client.chat.completions.create(
 
79
  self, *, user_query, schema_preview, plan_text, clarify_answers=None
80
  ):
81
  prompt = f"""
82
+ You are a precise SQL generator.
83
  Return ONLY valid JSON with two keys: "sql" and "rationale".
84
  Do not include any markdown, backticks, or extra text.
85
 
 
106
  temperature=0,
107
  )
108
  content = completion.choices[0].message.content.strip()
109
+ usage = completion.usage
110
  t_in = usage.prompt_tokens if usage else None
111
  t_out = usage.completion_tokens if usage else None
112
  cost = self._estimate_cost(usage) if usage else None
113
 
 
114
  try:
115
  parsed = json.loads(content)
116
  except json.JSONDecodeError:
 
126
 
127
  sql = (parsed.get("sql") or "").strip()
128
  rationale = parsed.get("rationale") or ""
 
129
  if not sql:
130
  raise ValueError("LLM returned empty 'sql'")
131
 
 
132
  return sql, rationale, t_in, t_out, cost
133
 
134
  def repair(self, *, sql, error_msg, schema_preview):
 
156
  )
157
 
158
  def _estimate_cost(self, usage):
 
159
  total = usage.prompt_tokens + usage.completion_tokens
160
  return total * 0.000001
huggingface.yml CHANGED
@@ -5,4 +5,4 @@ colorTo: purple
5
  sdk: gradio
6
  python_version: "3.11"
7
  app_file: app.py
8
- pinned: false
 
5
  sdk: gradio
6
  python_version: "3.11"
7
  app_file: app.py
8
+ pinned: false