deepagent-bot commited on
Commit
ff25935
·
1 Parent(s): 0d7206c

ci: sync dashboard from 2bc80cd

Browse files

Source: github.com/CouLiBaLy-B/gh-deepagents@2bc80cd9efa80019caf02d83b448842dfd4f78b5

src/gh_deepagent/dashboard/pages/5_🚀_Trigger.py CHANGED
@@ -1,8 +1,9 @@
1
  """Trigger page — kick off a job locally (CLI shortcut).
2
 
3
- This page runs the agent **in-process** via the runner, NOT through the queue.
4
- Useful for one-off operator interventions; for production use, prefer GitHub
5
- labels / comments which go through the queue.
 
6
  """
7
  from __future__ import annotations
8
 
@@ -33,6 +34,39 @@ if not user.get("is_admin"):
33
  )
34
  st.stop()
35
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
  st.warning(
37
  "⚠️ This page runs the agent inside the Streamlit process. It will block "
38
  "the UI for the duration of the run. For long jobs, use the queue."
@@ -40,15 +74,21 @@ st.warning(
40
 
41
  tab_fix, tab_evolve, tab_review = st.tabs(["Fix issue", "Evolve repo", "Review PR"])
42
 
 
 
 
43
  with tab_fix:
44
- issue_url = st.text_input("Issue URL",
45
- placeholder="https://github.com/org/repo/issues/42")
 
 
 
46
  dry = st.checkbox("Dry-run (no PR)", key="dry-fix")
47
  if st.button("Run fix_issue", type="primary", disabled=not issue_url):
48
  with st.spinner("Agent running…"):
49
  try:
50
  from gh_deepagent.runner import fix_issue
51
- res = fix_issue(issue_url, dry_run=dry)
52
  if res.pr_url:
53
  st.success(f"✅ PR opened: {res.pr_url}")
54
  st.markdown(res.summary or "(no summary)")
@@ -59,9 +99,15 @@ with tab_fix:
59
  st.error(f"Failed: {e}")
60
 
61
  with tab_evolve:
62
- repo = st.text_input("Repo (owner/name)", value=os.getenv("DEEPAGENT_DEFAULT_REPO", ""),
63
- key="evolve-repo")
64
- instruction = st.text_area("Instruction", placeholder="What should the agent change?",
 
 
 
 
 
 
65
  height=120, key="evolve-instr")
66
  dry2 = st.checkbox("Dry-run (no PR)", key="dry-evolve")
67
  if st.button("Run evolve_code", type="primary",
@@ -80,8 +126,13 @@ with tab_evolve:
80
  st.error(f"Failed: {e}")
81
 
82
  with tab_review:
83
- repo3 = st.text_input("Repo (owner/name)", value=os.getenv("DEEPAGENT_DEFAULT_REPO", ""),
84
- key="review-repo")
 
 
 
 
 
85
  pr_n = st.number_input("PR number", min_value=1, step=1, key="review-pr")
86
  if st.button("Run review_pr", type="primary",
87
  disabled=not (repo3 and pr_n)):
 
1
  """Trigger page — kick off a job locally (CLI shortcut).
2
 
3
+ Runs the agent **in-process** via the runner, NOT through the queue. Useful
4
+ for one-off operator interventions. The LLM provider/model/token used here
5
+ comes from the env (or from the **⚙️ LLM Settings** page, which writes to
6
+ the same env vars for the current process).
7
  """
8
  from __future__ import annotations
9
 
 
34
  )
35
  st.stop()
36
 
37
+ # --- Current LLM banner -----------------------------------------------
38
+ _spec = st.session_state.get("llm.spec") or os.getenv(
39
+ "DEEPAGENT_MODEL", "anthropic:claude-sonnet-4-5"
40
+ )
41
+ _provider = _spec.split(":", 1)[0] if ":" in _spec else _spec
42
+ _key_env_map = {
43
+ "anthropic": "ANTHROPIC_API_KEY",
44
+ "openai": "OPENAI_API_KEY",
45
+ "google_genai": "GOOGLE_API_KEY",
46
+ "groq": "GROQ_API_KEY",
47
+ }
48
+ _key_env = _key_env_map.get(_provider)
49
+ _has_key = (not _key_env) or bool(os.getenv(_key_env))
50
+ cols = st.columns([3, 1])
51
+ with cols[0]:
52
+ st.info(
53
+ f"**Current LLM:** `{_spec}` "
54
+ + (f"· {_key_env} {'✅' if _has_key else '❌ missing'}" if _key_env else "")
55
+ + " — change it on the **⚙️ LLM Settings** page."
56
+ )
57
+ with cols[1]:
58
+ st.page_link(
59
+ "pages/8_⚙️_LLM_Settings.py",
60
+ label="⚙️ Configure LLM",
61
+ icon="🔧",
62
+ )
63
+
64
+ if not _has_key:
65
+ st.error(
66
+ f"⛔ The chosen provider needs `{_key_env}` to be set. "
67
+ "Set it on the LLM Settings page or in the Space *Settings → Variables and secrets*."
68
+ )
69
+
70
  st.warning(
71
  "⚠️ This page runs the agent inside the Streamlit process. It will block "
72
  "the UI for the duration of the run. For long jobs, use the queue."
 
74
 
75
  tab_fix, tab_evolve, tab_review = st.tabs(["Fix issue", "Evolve repo", "Review PR"])
76
 
77
+ # Helper to give consistent hints
78
+ _REPO_HELP = "Accepts `owner/repo`, a full GitHub URL, or an SSH URL — all normalised automatically."
79
+
80
  with tab_fix:
81
+ issue_url = st.text_input(
82
+ "Issue URL",
83
+ placeholder="https://github.com/org/repo/issues/42",
84
+ help="Must be a full GitHub issue URL.",
85
+ )
86
  dry = st.checkbox("Dry-run (no PR)", key="dry-fix")
87
  if st.button("Run fix_issue", type="primary", disabled=not issue_url):
88
  with st.spinner("Agent running…"):
89
  try:
90
  from gh_deepagent.runner import fix_issue
91
+ res = fix_issue(issue_url.strip(), dry_run=dry)
92
  if res.pr_url:
93
  st.success(f"✅ PR opened: {res.pr_url}")
94
  st.markdown(res.summary or "(no summary)")
 
99
  st.error(f"Failed: {e}")
100
 
101
  with tab_evolve:
102
+ repo = st.text_input(
103
+ "Repo",
104
+ value=os.getenv("DEEPAGENT_DEFAULT_REPO", ""),
105
+ key="evolve-repo",
106
+ help=_REPO_HELP,
107
+ placeholder="owner/repo or https://github.com/owner/repo",
108
+ )
109
+ instruction = st.text_area("Instruction",
110
+ placeholder="What should the agent change?",
111
  height=120, key="evolve-instr")
112
  dry2 = st.checkbox("Dry-run (no PR)", key="dry-evolve")
113
  if st.button("Run evolve_code", type="primary",
 
126
  st.error(f"Failed: {e}")
127
 
128
  with tab_review:
129
+ repo3 = st.text_input(
130
+ "Repo",
131
+ value=os.getenv("DEEPAGENT_DEFAULT_REPO", ""),
132
+ key="review-repo",
133
+ help=_REPO_HELP,
134
+ placeholder="owner/repo or https://github.com/owner/repo",
135
+ )
136
  pr_n = st.number_input("PR number", min_value=1, step=1, key="review-pr")
137
  if st.button("Run review_pr", type="primary",
138
  disabled=not (repo3 and pr_n)):
src/gh_deepagent/dashboard/pages/8_⚙️_LLM_Settings.py ADDED
@@ -0,0 +1,255 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """LLM provider / model / API-key configuration page.
2
+
3
+ For the **all-in-one demo Space**, this page lets any admin reconfigure the LLM
4
+ **without restarting the container**. The settings are written to
5
+ ``st.session_state`` and pushed into ``os.environ`` so that the in-process
6
+ ``runner.fix_issue / evolve_code / review_pr`` calls (page 🚀 Trigger) pick
7
+ them up immediately.
8
+
9
+ Limitations:
10
+ - Workers running in *other processes* keep the env vars they were started
11
+ with. For a multi-process deployment you still need to set DEEPAGENT_MODEL
12
+ + the right API key at container-start time.
13
+ - For the standalone (no-backend) dashboard this page is informative — you
14
+ can configure values for a future backend you'll deploy yourself.
15
+ """
16
+ from __future__ import annotations
17
+
18
+ import os
19
+
20
+ import streamlit as st
21
+
22
+ from gh_deepagent.dashboard.auth_ui import render_user_badge, require_login
23
+
24
+
25
+ st.set_page_config(page_title="LLM Settings · gh-deepagent",
26
+ page_icon="⚙️", layout="wide")
27
+ st.title("⚙️ LLM provider, model & token")
28
+ st.caption(
29
+ "Configure which LLM the agent should use. Changes apply **in this Space, "
30
+ "right now**, for jobs triggered via the dashboard. They do NOT propagate "
31
+ "to other Spaces or to workers in other processes."
32
+ )
33
+
34
+ _api, user = require_login()
35
+ render_user_badge()
36
+ if not user.get("is_admin"):
37
+ st.error("LLM settings are restricted to admins.")
38
+ st.stop()
39
+
40
+
41
+ # ----------------------------------------------------- known providers
42
+ PROVIDERS = {
43
+ "anthropic": {
44
+ "label": "Anthropic Claude",
45
+ "env_key": "ANTHROPIC_API_KEY",
46
+ "models": [
47
+ "claude-sonnet-4-5",
48
+ "claude-opus-4",
49
+ "claude-haiku-4",
50
+ "claude-sonnet-4-20250514",
51
+ ],
52
+ "spec_prefix": "anthropic:",
53
+ "doc": "https://docs.anthropic.com/en/api/getting-started",
54
+ },
55
+ "openai": {
56
+ "label": "OpenAI",
57
+ "env_key": "OPENAI_API_KEY",
58
+ "models": [
59
+ "gpt-4o-mini",
60
+ "gpt-4o",
61
+ "gpt-4.1",
62
+ "gpt-4.1-mini",
63
+ "gpt-5",
64
+ "o4-mini",
65
+ ],
66
+ "spec_prefix": "openai:",
67
+ "doc": "https://platform.openai.com/api-keys",
68
+ },
69
+ "google_genai": {
70
+ "label": "Google Gemini",
71
+ "env_key": "GOOGLE_API_KEY",
72
+ "models": [
73
+ "gemini-2.5-flash",
74
+ "gemini-2.5-pro",
75
+ ],
76
+ "spec_prefix": "google_genai:",
77
+ "doc": "https://aistudio.google.com/apikey",
78
+ },
79
+ "groq": {
80
+ "label": "Groq (Llama, Mixtral, …)",
81
+ "env_key": "GROQ_API_KEY",
82
+ "models": [
83
+ "llama-3.3-70b-versatile",
84
+ "llama-3.1-70b-versatile",
85
+ "mixtral-8x7b-32768",
86
+ ],
87
+ "spec_prefix": "groq:",
88
+ "doc": "https://console.groq.com/keys",
89
+ },
90
+ "ollama": {
91
+ "label": "Ollama (local, no API key)",
92
+ "env_key": "", # no key
93
+ "models": [
94
+ "qwen2.5-coder:14b",
95
+ "deepseek-coder-v2:16b",
96
+ "llama3.1:8b",
97
+ ],
98
+ "spec_prefix": "ollama:",
99
+ "doc": "https://ollama.com/library",
100
+ },
101
+ }
102
+
103
+
104
+ # ----------------------------------------------------- current state
105
+ def _current_spec() -> str:
106
+ return st.session_state.get("llm.spec") or os.getenv(
107
+ "DEEPAGENT_MODEL", "anthropic:claude-sonnet-4-5"
108
+ )
109
+
110
+
111
+ def _current_provider() -> str:
112
+ spec = _current_spec()
113
+ for pid, p in PROVIDERS.items():
114
+ if spec.startswith(p["spec_prefix"]):
115
+ return pid
116
+ return "anthropic"
117
+
118
+
119
+ # ----------------------------------------------------- UI
120
+ st.subheader("Current configuration")
121
+
122
+ cur_provider = _current_provider()
123
+ cur_spec = _current_spec()
124
+ cur_model = cur_spec.split(":", 1)[1] if ":" in cur_spec else cur_spec
125
+
126
+ cc = st.columns(3)
127
+ cc[0].metric("Provider", PROVIDERS[cur_provider]["label"])
128
+ cc[1].metric("Model", cur_model)
129
+ key_env = PROVIDERS[cur_provider]["env_key"]
130
+ if key_env:
131
+ key_set = bool(os.getenv(key_env))
132
+ cc[2].metric(
133
+ f"{key_env}",
134
+ "✅ set" if key_set else "❌ missing",
135
+ delta=None,
136
+ )
137
+ else:
138
+ cc[2].metric("API key", "— (none required)")
139
+
140
+ st.divider()
141
+
142
+ # ----------------------------------------------------- form
143
+ st.subheader("Change configuration")
144
+
145
+ with st.form("llm-settings-form"):
146
+ provider_id = st.selectbox(
147
+ "Provider",
148
+ list(PROVIDERS.keys()),
149
+ index=list(PROVIDERS.keys()).index(cur_provider),
150
+ format_func=lambda k: PROVIDERS[k]["label"],
151
+ )
152
+ p = PROVIDERS[provider_id]
153
+
154
+ # Model selector: known models + free-text override
155
+ model_options = p["models"] + ["(custom — type below)"]
156
+ default_idx = (
157
+ model_options.index(cur_model) if cur_model in model_options else 0
158
+ )
159
+ model_choice = st.selectbox("Model", model_options, index=default_idx)
160
+ custom_model = st.text_input(
161
+ "Custom model name",
162
+ value=cur_model if cur_model not in p["models"] else "",
163
+ disabled=model_choice != "(custom — type below)",
164
+ placeholder="e.g. my-org/my-finetune",
165
+ )
166
+ chosen_model = custom_model if model_choice == "(custom — type below)" else model_choice
167
+
168
+ api_key = ""
169
+ if p["env_key"]:
170
+ st.markdown(
171
+ f"**API key** → stored as env var `{p['env_key']}` "
172
+ f"([get one]({p['doc']}))"
173
+ )
174
+ api_key = st.text_input(
175
+ f"{p['env_key']}",
176
+ type="password",
177
+ value="", # never display the existing value
178
+ placeholder="Leave blank to keep the current one",
179
+ )
180
+ else:
181
+ st.caption(f"No API key needed for {p['label']}. "
182
+ f"Make sure an Ollama server is reachable.")
183
+
184
+ base_url = ""
185
+ if provider_id == "ollama":
186
+ base_url = st.text_input(
187
+ "OLLAMA_BASE_URL",
188
+ value=os.getenv("OLLAMA_BASE_URL", "http://localhost:11434"),
189
+ )
190
+
191
+ submitted = st.form_submit_button("Apply", type="primary")
192
+
193
+ if submitted:
194
+ if not chosen_model:
195
+ st.error("Pick or type a model name.")
196
+ st.stop()
197
+
198
+ spec = f"{p['spec_prefix']}{chosen_model}"
199
+ os.environ["DEEPAGENT_MODEL"] = spec
200
+ st.session_state["llm.spec"] = spec
201
+
202
+ if api_key and p["env_key"]:
203
+ os.environ[p["env_key"]] = api_key
204
+ st.session_state[f"llm.key.{p['env_key']}"] = "set" # marker only
205
+ if base_url and provider_id == "ollama":
206
+ os.environ["OLLAMA_BASE_URL"] = base_url
207
+
208
+ # The Settings cache is frozen — invalidate it so the next get_settings()
209
+ # picks up the new DEEPAGENT_MODEL.
210
+ try:
211
+ from gh_deepagent.config import get_settings
212
+ get_settings.cache_clear()
213
+ except Exception:
214
+ pass
215
+
216
+ st.success(f"✅ Applied: `{spec}`"
217
+ + (f" with **{p['env_key']}**" if api_key else ""))
218
+ st.info(
219
+ "These settings apply to **jobs you launch from the dashboard right now**. "
220
+ "They won't reach other workers / processes. For a permanent change, set "
221
+ "the corresponding env vars in the Space *Settings → Variables and secrets*."
222
+ )
223
+
224
+ st.divider()
225
+
226
+ # ----------------------------------------------------- diagnostics
227
+ with st.expander("🔎 What's in the environment right now?"):
228
+ rows = []
229
+ for pid, info in PROVIDERS.items():
230
+ env = info["env_key"]
231
+ if not env:
232
+ continue
233
+ rows.append({
234
+ "Provider": info["label"],
235
+ "Env var": env,
236
+ "Set": "✅" if os.getenv(env) else "—",
237
+ })
238
+ rows.append({
239
+ "Provider": "—",
240
+ "Env var": "DEEPAGENT_MODEL",
241
+ "Set": os.getenv("DEEPAGENT_MODEL", "(default)"),
242
+ })
243
+ rows.append({
244
+ "Provider": "—",
245
+ "Env var": "OLLAMA_BASE_URL",
246
+ "Set": os.getenv("OLLAMA_BASE_URL", "(default)"),
247
+ })
248
+ import pandas as pd
249
+ st.dataframe(pd.DataFrame(rows), use_container_width=True, hide_index=True)
250
+
251
+ st.caption(
252
+ "💡 For production: configure these as Space *Variables* (model) and "
253
+ "*Secrets* (API key). They'll then apply to every container restart, "
254
+ "not just your current session."
255
+ )
src/gh_deepagent/github_client.py CHANGED
@@ -153,3 +153,49 @@ def _parse_author(s: str) -> tuple[str, str]:
153
  if not m:
154
  return s.strip(), "bot@deepagent"
155
  return m.group(1).strip(), m.group(2).strip()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
153
  if not m:
154
  return s.strip(), "bot@deepagent"
155
  return m.group(1).strip(), m.group(2).strip()
156
+
157
+ # ---------------------------------------------------------------- normaliser
158
+
159
+ # Accepts: owner/repo
160
+ # https://github.com/owner/repo
161
+ # https://github.com/owner/repo.git
162
+ # git@github.com:owner/repo.git
163
+ # https://x-access-token:TOK@github.com/owner/repo[.git]
164
+ _REPO_RE = re.compile(
165
+ r"""^(?:
166
+ (?:https?://(?:[^@/]+@)?github\.com/)
167
+ |
168
+ (?:git@github\.com:)
169
+ )?
170
+ (?P<owner>[A-Za-z0-9][A-Za-z0-9-]*)
171
+ /
172
+ (?P<repo>[A-Za-z0-9._-]+?)
173
+ (?:\.git)?
174
+ /?$""",
175
+ re.VERBOSE,
176
+ )
177
+
178
+
179
+ def normalize_repo_full_name(value: str) -> str:
180
+ """Normalise any reasonable repo reference to ``owner/repo``.
181
+
182
+ Raises :class:`ValueError` if the value clearly isn't a GitHub repo ref.
183
+ """
184
+ if not value or not isinstance(value, str):
185
+ raise ValueError(f"empty repo reference: {value!r}")
186
+ s = value.strip()
187
+ # If it has multiple URL-like substrings (user pasted "https://...https://..."),
188
+ # keep only the trailing one.
189
+ if s.count("https://") > 1 or s.count("http://") > 1:
190
+ # Take the segment after the last protocol marker.
191
+ for marker in ("https://", "http://"):
192
+ if marker in s:
193
+ s = marker + s.rsplit(marker, 1)[1]
194
+ m = _REPO_RE.match(s)
195
+ if not m:
196
+ raise ValueError(
197
+ f"Not a valid GitHub repo reference: {value!r}. "
198
+ "Expected formats: 'owner/repo', a full https URL, or an ssh URL."
199
+ )
200
+ return f"{m['owner']}/{m['repo']}"
201
+
src/gh_deepagent/runner.py CHANGED
@@ -17,7 +17,7 @@ from rich.console import Console
17
 
18
  from .agent import build_agent
19
  from .config import get_settings
20
- from .github_client import GitHubOps, IssueRef
21
 
22
  console = Console()
23
 
@@ -149,6 +149,7 @@ def evolve_code(
149
  backend: Optional[str] = None,
150
  ) -> RunResult:
151
  """Apply a free-form evolution request to the repo."""
 
152
  settings = get_settings()
153
  settings.assert_ready()
154
 
@@ -191,6 +192,7 @@ def iterate_pr(
191
 
192
  Triggered by `/deepagent <instruction>` on a PR comment, or via CLI.
193
  """
 
194
  settings = get_settings()
195
  settings.assert_ready()
196
  gh = GitHubOps()
@@ -253,6 +255,7 @@ def iterate_pr(
253
  def review_pr(repo_full_name: str, pr_number: int, backend: Optional[str] = None) -> RunResult:
254
  """Post an automated code-review comment on a PR."""
255
  import urllib.request
 
256
  settings = get_settings()
257
  settings.assert_ready()
258
  gh = GitHubOps()
 
17
 
18
  from .agent import build_agent
19
  from .config import get_settings
20
+ from .github_client import GitHubOps, IssueRef, normalize_repo_full_name
21
 
22
  console = Console()
23
 
 
149
  backend: Optional[str] = None,
150
  ) -> RunResult:
151
  """Apply a free-form evolution request to the repo."""
152
+ repo_full_name = normalize_repo_full_name(repo_full_name)
153
  settings = get_settings()
154
  settings.assert_ready()
155
 
 
192
 
193
  Triggered by `/deepagent <instruction>` on a PR comment, or via CLI.
194
  """
195
+ repo_full_name = normalize_repo_full_name(repo_full_name)
196
  settings = get_settings()
197
  settings.assert_ready()
198
  gh = GitHubOps()
 
255
  def review_pr(repo_full_name: str, pr_number: int, backend: Optional[str] = None) -> RunResult:
256
  """Post an automated code-review comment on a PR."""
257
  import urllib.request
258
+ repo_full_name = normalize_repo_full_name(repo_full_name)
259
  settings = get_settings()
260
  settings.assert_ready()
261
  gh = GitHubOps()