JayeshCC commited on
Commit
837bbe4
·
verified ·
1 Parent(s): 5ff47e7

Upload folder using huggingface_hub

Browse files
README.md CHANGED
@@ -80,22 +80,22 @@ class TokenOptimiserState(State):
80
 
81
  ## 📋 Tasks
82
 
83
- ### 🟢 Easy — Verbosity Reduction
84
- **Original:** `"Can you please explain in a very detailed manner what machine learning is and how it works step by step?"`
85
- **Goal:** Strip filler words, compress to core query
86
- **Expected optimized:** `"Explain machine learning briefly."`
87
- **Max output:** 50 tokens
88
-
89
- ### 🟡 Medium — Format Constraint Addition
90
- **Original:** `"I need a comprehensive analysis of the renewable energy market trends over the past decade, including solar, wind, and hydroelectric power growth rates..."`
91
- **Goal:** Compress input AND add explicit output format constraints
92
- **Expected optimized:** `"Summarize 2013-2023 renewable energy trends: solar, wind, hydro. In 5 bullet points."`
93
- **Max output:** 100 tokens
94
-
95
- ### 🔴 Hard — Multi-Intent Structured Output
96
- **Original:** 82-word complex data science analysis request with 5 sub-tasks
97
- **Goal:** Minimize total tokens (input + output) while producing structured JSON with all 5 required keys
98
- **Expected optimized:** Compressed prompt specifying `JSON with keys: top_categories, growth_regions, responsive_segments, budget_allocation, risks_watch`
99
  **Max output:** 200 tokens
100
 
101
  ---
 
80
 
81
  ## 📋 Tasks
82
 
83
+ ### 🟢 Easy — Redundancy Stripping
84
+ **Original:** `"Could you possibly help me understand, if it's not too much trouble, what the word 'photosynthesis' means? I would really appreciate it if you could explain it to me in simple terms that are easy to understand."`
85
+ **Goal:** Strip politeness filler and redundancy to a single direct question without formatting
86
+ **Expected optimized:** `"What does photosynthesis mean? Be brief."`
87
+ **Max output:** 30 tokens
88
+
89
+ ### 🟡 Medium — Constraint Injection
90
+ **Original:** `"I'm looking for information about the main differences between Python and JavaScript programming languages. Could you give me a thorough breakdown covering things like typing, use cases, performance, syntax style, and ecosystem so I can decide which one to learn first?"`
91
+ **Goal:** Compress input AND inject format + exactly 5 bullet point counts into prompt
92
+ **Expected optimized:** `"Compare Python and JavaScript (typing, use cases, performance, syntax, ecosystem) in exactly 5 bullet points."`
93
+ **Max output:** 120 tokens
94
+
95
+ ### 🔴 Hard — Multi-Key JSON Extraction
96
+ **Original:** `"We need you to analyze our e-commerce platform data and provide strategic insights. Specifically: first identify which product categories are performing best by revenue, second tell us which geographic regions show the most growth potential, third identify which customer segments respond best to promotions, fourth suggest how we should allocate our Q3 marketing budget across channels, and fifth flag any market risks we should be watching. Please be thorough in your analysis and provide detailed reasoning for each point."`
97
+ **Goal:** Compress 82-word multi-intent prompt and force structured JSON output with 5 exact required keys
98
+ **Expected optimized:** `"Analyze e-commerce data based on revenue, growth regions, responsive segments, Q3 budget, and market risks. Output strictly as JSON with keys: top_categories, growth_regions, responsive_segments, budget_allocation, risks_watch."`
99
  **Max output:** 200 tokens
100
 
101
  ---
inference.py CHANGED
@@ -15,6 +15,7 @@ Environment variables required:
15
  """
16
 
17
  import asyncio
 
18
  import os
19
  import textwrap
20
  from typing import List, Optional
@@ -27,6 +28,14 @@ except Exception: # pragma: no cover
27
 
28
  from token_optimiser import TokenOptimiserEnv, TokenOptimiserAction
29
 
 
 
 
 
 
 
 
 
30
  # ---------------------------------------------------------------------------
31
  # Configuration
32
  # ---------------------------------------------------------------------------
@@ -151,7 +160,7 @@ def get_optimized_prompt(
151
  result = (completion.choices[0].message.content or "").strip()
152
  return result if result else "Explain briefly."
153
  except Exception as exc:
154
- print(f"[DEBUG] LLM call failed: {exc}", flush=True)
155
  return _rule_based_compress(original_prompt, step)
156
 
157
 
@@ -210,11 +219,8 @@ async def run_episode(llm: OpenAI) -> None:
210
  env_state = await env.state()
211
  original_prompt: str = env_state.original_prompt or "Explain machine learning briefly."
212
 
213
- print(
214
- f"[DEBUG] Task difficulty={env_state.task_difficulty} | "
215
- f"Original prompt ({len(original_prompt.split())} words): {original_prompt[:80]}...",
216
- flush=True,
217
- )
218
 
219
  prev_reward = 0.0
220
  prev_response = ""
@@ -236,10 +242,7 @@ async def run_episode(llm: OpenAI) -> None:
236
  reward = result.reward # server puts reward at top-level, not inside obs
237
  done = result.done or (step >= MAX_STEPS)
238
  prev_response = obs.llm_response
239
- print(
240
- f"[DEBUG] tokens in={obs.input_tokens} out={obs.output_tokens}",
241
- flush=True,
242
- )
243
  except Exception as exc:
244
  error_msg = str(exc)
245
  done = True
@@ -263,7 +266,7 @@ async def run_episode(llm: OpenAI) -> None:
263
  success = score >= SUCCESS_THRESHOLD
264
 
265
  except Exception as exc:
266
- print(f"[DEBUG] Episode error: {exc}", flush=True)
267
  finally:
268
  try:
269
  await env.close()
@@ -274,7 +277,7 @@ async def run_episode(llm: OpenAI) -> None:
274
 
275
  async def main() -> None:
276
  if not HF_TOKEN:
277
- print("[ERROR] HF_TOKEN environment variable not set. Exiting.", flush=True)
278
  return
279
 
280
  llm = OpenAI(base_url=API_BASE_URL, api_key=HF_TOKEN)
 
15
  """
16
 
17
  import asyncio
18
+ import logging
19
  import os
20
  import textwrap
21
  from typing import List, Optional
 
28
 
29
  from token_optimiser import TokenOptimiserEnv, TokenOptimiserAction
30
 
31
+ logger = logging.getLogger("TokenOptimiserFrontend")
32
+ logger.setLevel(logging.INFO)
33
+ if not logger.handlers:
34
+ handler = logging.StreamHandler()
35
+ formatter = logging.Formatter('\033[96m%(asctime)s\033[0m | \033[93m%(levelname)-7s\033[0m | \033[1mCLIENT\033[0m | %(message)s', datefmt='%H:%M:%S')
36
+ handler.setFormatter(formatter)
37
+ logger.addHandler(handler)
38
+
39
  # ---------------------------------------------------------------------------
40
  # Configuration
41
  # ---------------------------------------------------------------------------
 
160
  result = (completion.choices[0].message.content or "").strip()
161
  return result if result else "Explain briefly."
162
  except Exception as exc:
163
+ logger.warning(f"LLM call failed, falling back to basic rule compression: {exc}")
164
  return _rule_based_compress(original_prompt, step)
165
 
166
 
 
219
  env_state = await env.state()
220
  original_prompt: str = env_state.original_prompt or "Explain machine learning briefly."
221
 
222
+ logger.info(f"Task connected. Difficulty: {env_state.task_difficulty.upper()}")
223
+ logger.info(f"Original prompt ({len(original_prompt.split())} words): {original_prompt[:80]}...")
 
 
 
224
 
225
  prev_reward = 0.0
226
  prev_response = ""
 
242
  reward = result.reward # server puts reward at top-level, not inside obs
243
  done = result.done or (step >= MAX_STEPS)
244
  prev_response = obs.llm_response
245
+ logger.info(f"Step {step} Tokens => Input: {obs.input_tokens}, Output: {obs.output_tokens}")
 
 
 
246
  except Exception as exc:
247
  error_msg = str(exc)
248
  done = True
 
266
  success = score >= SUCCESS_THRESHOLD
267
 
268
  except Exception as exc:
269
+ logger.error(f"Episode error aborted run: {exc}")
270
  finally:
271
  try:
272
  await env.close()
 
277
 
278
  async def main() -> None:
279
  if not HF_TOKEN:
280
+ logger.error("HF_TOKEN environment variable not set. Exiting.")
281
  return
282
 
283
  llm = OpenAI(base_url=API_BASE_URL, api_key=HF_TOKEN)
openenv.yaml CHANGED
@@ -4,4 +4,7 @@ type: space
4
  runtime: fastapi
5
  app: server.app:app
6
  port: 8000
7
-
 
 
 
 
4
  runtime: fastapi
5
  app: server.app:app
6
  port: 8000
7
+ tasks:
8
+ - redundancy_stripping (easy)
9
+ - constraint_injection (medium)
10
+ - multi_key_json_extraction (hard)
openenv_token_optimiser.egg-info/SOURCES.txt CHANGED
@@ -1,4 +1,8 @@
1
  README.md
 
 
 
 
2
  pyproject.toml
3
  ./__init__.py
4
  ./client.py
 
1
  README.md
2
+ __init__.py
3
+ client.py
4
+ inference.py
5
+ models.py
6
  pyproject.toml
7
  ./__init__.py
8
  ./client.py
server/app.py CHANGED
@@ -54,7 +54,7 @@ app = create_app(
54
  )
55
 
56
 
57
- def main() -> None:
58
  """
59
  Entry point for direct execution via uv run or python -m.
60
 
@@ -63,20 +63,23 @@ def main() -> None:
63
  uv run --project . server --port 8001
64
  python -m token_optimiser.server.app
65
 
 
 
 
 
66
  For production deployments, consider using uvicorn directly with
67
  multiple workers:
68
  uvicorn token_optimiser.server.app:app --workers 4
69
  """
70
- import argparse
71
  import uvicorn
72
 
73
- parser = argparse.ArgumentParser()
74
- parser.add_argument("--host", type=str, default="0.0.0.0")
75
- parser.add_argument("--port", type=int, default=8000)
76
- args = parser.parse_args()
77
-
78
- uvicorn.run(app, host=args.host, port=args.port)
79
 
80
 
81
  if __name__ == "__main__":
82
- main()
 
 
 
 
 
 
54
  )
55
 
56
 
57
+ def main(host: str = "0.0.0.0", port: int = 8000):
58
  """
59
  Entry point for direct execution via uv run or python -m.
60
 
 
63
  uv run --project . server --port 8001
64
  python -m token_optimiser.server.app
65
 
66
+ Args:
67
+ host: Host address to bind to (default: "0.0.0.0")
68
+ port: Port number to listen on (default: 8000)
69
+
70
  For production deployments, consider using uvicorn directly with
71
  multiple workers:
72
  uvicorn token_optimiser.server.app:app --workers 4
73
  """
 
74
  import uvicorn
75
 
76
+ uvicorn.run(app, host=host, port=port)
 
 
 
 
 
77
 
78
 
79
  if __name__ == "__main__":
80
+ import argparse
81
+
82
+ parser = argparse.ArgumentParser()
83
+ parser.add_argument("--port", type=int, default=8000)
84
+ args = parser.parse_args()
85
+ main(port=args.port)
server/token_optimiser_environment.py CHANGED
@@ -11,10 +11,22 @@ A sandboxed LLM interaction environment where an AI agent optimizes both input p
11
  and expected output responses to minimize total token usage while maintaining correctness.
12
  """
13
 
 
 
14
  import os
15
  import random
 
16
  from uuid import uuid4
17
 
 
 
 
 
 
 
 
 
 
18
  try:
19
  from openai import OpenAI
20
  except ImportError:
@@ -62,29 +74,30 @@ class TokenOptimiserEnvironment(Environment):
62
  # EASY TASK
63
  {
64
  "difficulty": "easy",
65
- "prompt": "Can you please explain in a very detailed manner what machine learning is and how it works step by step?",
66
- "expected_format": "brief explanation",
67
- "reference_response": "Machine learning is a subset of AI that enables computers to learn from data without explicit programming. It works by identifying patterns in training data to make predictions or decisions on new data.",
68
- "max_output_tokens": 50,
69
- "description": "Reduce verbosity while preserving core concept"
70
  },
71
  # MEDIUM TASK
72
  {
73
  "difficulty": "medium",
74
- "prompt": "I need a comprehensive analysis of the renewable energy market trends over the past decade, including solar, wind, and hydroelectric power growth rates, investment patterns, technological advancements, and policy impacts across different regions globally.",
75
- "expected_format": "5 bullet points summarizing key trends",
76
- "reference_response": "• Solar power capacity grew 22% annually avg. Wind energy investments reached $140B in 2020 Hydroelectric remains largest renewable source Battery storage tech advancing rapidly Policy incentives driving global adoption",
77
- "max_output_tokens": 100,
78
- "description": "Compress input + specify bullet point format + length limit"
79
  },
80
  # HARD TASK
81
  {
82
  "difficulty": "hard",
83
- "prompt": "As a senior data scientist, I need you to analyze our Q3 sales performance dataset and provide actionable insights. The dataset contains: customer demographics, purchase history, product categories, regional sales data, marketing campaign ROI, seasonal trends, and competitor analysis. Please identify: 1) Our top 3 performing product categories and why, 2) Geographic regions with highest growth potential, 3) Customer segments most responsive to our email campaigns, 4) Optimal marketing budget allocation for Q4, and 5) Risks to watch based on economic indicators.",
84
- "expected_format": "JSON with 5 keys: top_categories, growth_regions, responsive_segments, budget_allocation, risks_watch",
85
- "reference_response": '{"top_categories": ["electronics", "software", "home_goods"], "growth_regions": ["SE Asia", "Latin America", "Africa"], "responsive_segments": ["young_professionals", "tech_enthusiasts"], "budget_allocation": {"email": 0.3, "social": 0.25, "search": 0.2, "tv": 0.15, "other": 0.1}, "risks_watch": ["inflation", "supply_chain", "labor_shortage"]}',
 
86
  "max_output_tokens": 200,
87
- "description": "Multi-intent optimization: compress complex request + specify JSON format + accuracy + length constraints"
88
  }
89
  ]
90
 
@@ -106,6 +119,11 @@ class TokenOptimiserEnvironment(Environment):
106
  )
107
  self._reset_count += 1
108
 
 
 
 
 
 
109
  return TokenOptimiserObservation(
110
  llm_response="",
111
  input_tokens=0,
@@ -143,12 +161,24 @@ class TokenOptimiserEnvironment(Environment):
143
  # 4. Format compliance (0.0-0.2)
144
  expected_fmt = self._current_task["expected_format"]
145
  format_score = 0.0
146
- if "bullet" in expected_fmt and any(c in llm_response for c in ("", "*", "-", "\n")):
147
- format_score = 0.2
148
- elif "json" in expected_fmt.lower() and "{" in llm_response and "}" in llm_response:
149
- format_score = 0.2
150
- elif "brief" in expected_fmt and len(llm_response.split()) < 30:
151
- format_score = 0.2
 
 
 
 
 
 
 
 
 
 
 
 
152
 
153
  # 5. Length penalty if output way too long
154
  max_out = self._current_task["max_output_tokens"]
@@ -163,10 +193,11 @@ class TokenOptimiserEnvironment(Environment):
163
  )
164
  reward = max(0.0, min(1.0, reward))
165
 
166
- print(
167
- f"[ENV] tok_eff={token_efficiency:.2f} semantic={semantic_score:.2f} "
168
- f"fmt={format_score:.2f} => reward={reward:.2f}",
169
- flush=True,
 
170
  )
171
 
172
  return TokenOptimiserObservation(
@@ -196,13 +227,14 @@ class TokenOptimiserEnvironment(Environment):
196
  return text, in_tok, out_tok
197
  except Exception as e:
198
  if "429" in str(e) or "Too Many Requests" in str(e):
199
- print(f"[ENV] Rate limited, waiting 3s (attempt {attempt+1})...")
200
  time.sleep(3)
201
  else:
202
- print(f"[ENV] LLM call failed: {e}")
203
  break
204
 
205
  # Rule-based fallback
 
206
  return self._fallback_simulate(prompt)
207
 
208
  def _fallback_simulate(self, prompt: str) -> tuple[str, int, int]:
 
11
  and expected output responses to minimize total token usage while maintaining correctness.
12
  """
13
 
14
+ import json
15
+ import logging
16
  import os
17
  import random
18
+ import re
19
  from uuid import uuid4
20
 
21
+ logger = logging.getLogger("TokenOptimiserBackend")
22
+ logger.setLevel(logging.INFO)
23
+ if not logger.handlers:
24
+ handler = logging.StreamHandler()
25
+ # Add clear ANSI color prefixes for visibility in backend terminal
26
+ formatter = logging.Formatter('\033[94m%(asctime)s\033[0m | \033[92m%(levelname)-7s\033[0m | \033[1m%(message)s\033[0m', datefmt='%H:%M:%S')
27
+ handler.setFormatter(formatter)
28
+ logger.addHandler(handler)
29
+
30
  try:
31
  from openai import OpenAI
32
  except ImportError:
 
74
  # EASY TASK
75
  {
76
  "difficulty": "easy",
77
+ "prompt": "Could you possibly help me understand, if it's not too much trouble, what the word 'photosynthesis' means? I would really appreciate it if you could explain it to me in simple terms that are easy to understand.",
78
+ "expected_format": "plain_brief",
79
+ "reference_response": "Photosynthesis is how plants convert sunlight into food using CO2 and water.",
80
+ "max_output_tokens": 30,
81
+ "description": "Strip politeness filler and redundancy to a single direct question"
82
  },
83
  # MEDIUM TASK
84
  {
85
  "difficulty": "medium",
86
+ "prompt": "I'm looking for information about the main differences between Python and JavaScript programming languages. Could you give me a thorough breakdown covering things like typing, use cases, performance, syntax style, and ecosystem so I can decide which one to learn first?",
87
+ "expected_format": "bullet_5",
88
+ "reference_response": "• Python: dynamic typing, data/ML focus\nJS: dynamic typing, web/frontend focus\nPerformance: JS V8 faster for runtime\nSyntax: Python readable, JS C-like\nEcosystem: Python pip/sci libs, JS npm/frameworks",
89
+ "max_output_tokens": 120,
90
+ "description": "Compress input AND inject format + count constraint into prompt"
91
  },
92
  # HARD TASK
93
  {
94
  "difficulty": "hard",
95
+ "prompt": "We need you to analyze our e-commerce platform data and provide strategic insights. Specifically: first identify which product categories are performing best by revenue, second tell us which geographic regions show the most growth potential, third identify which customer segments respond best to promotions, fourth suggest how we should allocate our Q3 marketing budget across channels, and fifth flag any market risks we should be watching. Please be thorough in your analysis and provide detailed reasoning for each point.",
96
+ "expected_format": "json_5keys",
97
+ "reference_response": '{"top_categories":"...","growth_regions":"...","responsive_segments":"...","budget_allocation":"...","risks_watch":"..."}',
98
+ "required_json_keys": ["top_categories", "growth_regions", "responsive_segments", "budget_allocation", "risks_watch"],
99
  "max_output_tokens": 200,
100
+ "description": "Compress 82-word multi-intent prompt and force structured JSON output with 5 exact keys"
101
  }
102
  ]
103
 
 
119
  )
120
  self._reset_count += 1
121
 
122
+ logger.info(f"------ ENVIRONMENT RESET ------")
123
+ logger.info(f"Loaded Task: [{self._current_task['difficulty'].upper()}] Index: {self._state.task_index}")
124
+ logger.info(f"Requirements: Format='{self._current_task['expected_format']}', Max Tokens={self._current_task['max_output_tokens']}")
125
+ logger.info(f"-------------------------------")
126
+
127
  return TokenOptimiserObservation(
128
  llm_response="",
129
  input_tokens=0,
 
161
  # 4. Format compliance (0.0-0.2)
162
  expected_fmt = self._current_task["expected_format"]
163
  format_score = 0.0
164
+ if expected_fmt == "bullet_5":
165
+ bullet_count = llm_response.count('•')
166
+ if bullet_count >= 5:
167
+ format_score = 0.2
168
+ elif 3 <= bullet_count <= 4:
169
+ format_score = 0.1
170
+ elif expected_fmt == "json_5keys":
171
+ try:
172
+ parsed = json.loads(llm_response.strip())
173
+ keys_present = sum(1 for k in self._current_task["required_json_keys"] if k in parsed)
174
+ format_score = 0.04 * keys_present
175
+ except json.JSONDecodeError:
176
+ format_score = 0.0
177
+ elif expected_fmt == "plain_brief":
178
+ sentences = len([s for s in re.split(r'[.!?]+', llm_response) if s.strip()])
179
+ has_no_bullets = not any(c in llm_response for c in ("•", "-", "*"))
180
+ if sentences <= 2 and has_no_bullets:
181
+ format_score = 0.2
182
 
183
  # 5. Length penalty if output way too long
184
  max_out = self._current_task["max_output_tokens"]
 
193
  )
194
  reward = max(0.0, min(1.0, reward))
195
 
196
+ logger.info(f"[STEP {self._state.step_count}] Optimized Prompt Length: {len(optimized_prompt.split())} words")
197
+ logger.info(f" └─ Tokens => In: {int(input_tokens)}, Out: {int(output_tokens)}")
198
+ logger.info(
199
+ f" └─ Reward => Tok_Eff:{token_efficiency:.2f} | Semantic:{semantic_score*0.3:.2f} | "
200
+ f"Fmt:{format_score:.2f} | Penalty:{length_penalty:.2f} || TOTAL: {reward:.3f}"
201
  )
202
 
203
  return TokenOptimiserObservation(
 
227
  return text, in_tok, out_tok
228
  except Exception as e:
229
  if "429" in str(e) or "Too Many Requests" in str(e):
230
+ logger.warning(f"Rate limited, waiting 3s (attempt {attempt+1})...")
231
  time.sleep(3)
232
  else:
233
+ logger.error(f"LLM call failed: {e}")
234
  break
235
 
236
  # Rule-based fallback
237
+ logger.debug("Falling back to rule-based simulation.")
238
  return self._fallback_simulate(prompt)
239
 
240
  def _fallback_simulate(self, prompt: str) -> tuple[str, int, int]:
validator_script.sh ADDED
@@ -0,0 +1,185 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ #
3
+ # validate-submission.sh — OpenEnv Submission Validator
4
+ #
5
+ # Checks that your HF Space is live, Docker image builds, and openenv validate passes.
6
+ #
7
+ # Prerequisites:
8
+ # - Docker: https://docs.docker.com/get-docker/
9
+ # - openenv-core: pip install openenv-core
10
+ # - curl (usually pre-installed)
11
+ #
12
+ # Run:
13
+ # curl -fsSL https://raw.githubusercontent.com/<owner>/<repo>/main/scripts/validate-submission.sh | bash -s -- <ping_url> [repo_dir]
14
+ #
15
+ # Or download and run locally:
16
+ # chmod +x validate-submission.sh
17
+ # ./validate-submission.sh <ping_url> [repo_dir]
18
+ #
19
+ # Arguments:
20
+ # ping_url Your HuggingFace Space URL (e.g. https://your-space.hf.space)
21
+ # repo_dir Path to your repo (default: current directory)
22
+ #
23
+ # Examples:
24
+ # ./validate-submission.sh https://my-team.hf.space
25
+ # ./validate-submission.sh https://my-team.hf.space ./my-repo
26
+ #
27
+
28
+ set -uo pipefail
29
+
30
+ DOCKER_BUILD_TIMEOUT=600
31
+ if [ -t 1 ]; then
32
+ RED='\033[0;31m'
33
+ GREEN='\033[0;32m'
34
+ YELLOW='\033[1;33m'
35
+ BOLD='\033[1m'
36
+ NC='\033[0m'
37
+ else
38
+ RED='' GREEN='' YELLOW='' BOLD='' NC=''
39
+ fi
40
+
41
+ run_with_timeout() {
42
+ local secs="$1"; shift
43
+ if command -v timeout &>/dev/null; then
44
+ timeout "$secs" "$@"
45
+ elif command -v gtimeout &>/dev/null; then
46
+ gtimeout "$secs" "$@"
47
+ else
48
+ "$@" &
49
+ local pid=$!
50
+ ( sleep "$secs" && kill "$pid" 2>/dev/null ) &
51
+ local watcher=$!
52
+ wait "$pid" 2>/dev/null
53
+ local rc=$?
54
+ kill "$watcher" 2>/dev/null
55
+ wait "$watcher" 2>/dev/null
56
+ return $rc
57
+ fi
58
+ }
59
+
60
+ portable_mktemp() {
61
+ local prefix="${1:-validate}"
62
+ mktemp "${TMPDIR:-/tmp}/${prefix}-XXXXXX" 2>/dev/null || mktemp
63
+ }
64
+
65
+ CLEANUP_FILES=()
66
+ cleanup() { rm -f "${CLEANUP_FILES[@]+"${CLEANUP_FILES[@]}"}"; }
67
+ trap cleanup EXIT
68
+
69
+ PING_URL="${1:-}"
70
+ REPO_DIR="${2:-.}"
71
+
72
+ if [ -z "$PING_URL" ]; then
73
+ printf "Usage: %s <ping_url> [repo_dir]\n" "$0"
74
+ printf "\n"
75
+ printf " ping_url Your HuggingFace Space URL (e.g. https://your-space.hf.space)\n"
76
+ printf " repo_dir Path to your repo (default: current directory)\n"
77
+ exit 1
78
+ fi
79
+
80
+ if ! REPO_DIR="$(cd "$REPO_DIR" 2>/dev/null && pwd)"; then
81
+ printf "Error: directory '%s' not found\n" "${2:-.}"
82
+ exit 1
83
+ fi
84
+ PING_URL="${PING_URL%/}"
85
+ export PING_URL
86
+ PASS=0
87
+
88
+ log() { printf "[%s] %b\n" "$(date -u +%H:%M:%S)" "$*"; }
89
+ pass() { log "${GREEN}PASSED${NC} -- $1"; PASS=$((PASS + 1)); }
90
+ fail() { log "${RED}FAILED${NC} -- $1"; }
91
+ hint() { printf " ${YELLOW}Hint:${NC} %b\n" "$1"; }
92
+ stop_at() {
93
+ printf "\n"
94
+ printf "${RED}${BOLD}Validation stopped at %s.${NC} Fix the above before continuing.\n" "$1"
95
+ exit 1
96
+ }
97
+
98
+ printf "\n"
99
+ printf "${BOLD}========================================${NC}\n"
100
+ printf "${BOLD} OpenEnv Submission Validator${NC}\n"
101
+ printf "${BOLD}========================================${NC}\n"
102
+ log "Repo: $REPO_DIR"
103
+ log "Ping URL: $PING_URL"
104
+ printf "\n"
105
+
106
+ log "${BOLD}Step 1/3: Pinging HF Space${NC} ($PING_URL/reset) ..."
107
+
108
+ CURL_OUTPUT=$(portable_mktemp "validate-curl")
109
+ CLEANUP_FILES+=("$CURL_OUTPUT")
110
+ HTTP_CODE=$(curl -s -o "$CURL_OUTPUT" -w "%{http_code}" -X POST \
111
+ -H "Content-Type: application/json" -d '{}' \
112
+ "$PING_URL/reset" --max-time 30 2>"$CURL_OUTPUT" || printf "000")
113
+
114
+ if [ "$HTTP_CODE" = "200" ]; then
115
+ pass "HF Space is live and responds to /reset"
116
+ elif [ "$HTTP_CODE" = "000" ]; then
117
+ fail "HF Space not reachable (connection failed or timed out)"
118
+ hint "Check your network connection and that the Space is running."
119
+ hint "Try: curl -s -o /dev/null -w '%%{http_code}' -X POST $PING_URL/reset"
120
+ stop_at "Step 1"
121
+ else
122
+ fail "HF Space /reset returned HTTP $HTTP_CODE (expected 200)"
123
+ hint "Make sure your Space is running and the URL is correct."
124
+ hint "Try opening $PING_URL in your browser first."
125
+ stop_at "Step 1"
126
+ fi
127
+
128
+ log "${BOLD}Step 2/3: Running docker build${NC} ..."
129
+
130
+ if ! command -v docker &>/dev/null; then
131
+ fail "docker command not found"
132
+ hint "Install Docker: https://docs.docker.com/get-docker/"
133
+ stop_at "Step 2"
134
+ fi
135
+
136
+ if [ -f "$REPO_DIR/Dockerfile" ]; then
137
+ DOCKER_CONTEXT="$REPO_DIR"
138
+ elif [ -f "$REPO_DIR/server/Dockerfile" ]; then
139
+ DOCKER_CONTEXT="$REPO_DIR/server"
140
+ else
141
+ fail "No Dockerfile found in repo root or server/ directory"
142
+ stop_at "Step 2"
143
+ fi
144
+
145
+ log " Found Dockerfile in $DOCKER_CONTEXT"
146
+
147
+ BUILD_OK=false
148
+ BUILD_OUTPUT=$(run_with_timeout "$DOCKER_BUILD_TIMEOUT" docker build "$DOCKER_CONTEXT" 2>&1) && BUILD_OK=true
149
+
150
+ if [ "$BUILD_OK" = true ]; then
151
+ pass "Docker build succeeded"
152
+ else
153
+ fail "Docker build failed (timeout=${DOCKER_BUILD_TIMEOUT}s)"
154
+ printf "%s\n" "$BUILD_OUTPUT" | tail -20
155
+ stop_at "Step 2"
156
+ fi
157
+
158
+ log "${BOLD}Step 3/3: Running openenv validate${NC} ..."
159
+
160
+ if ! command -v openenv &>/dev/null; then
161
+ fail "openenv command not found"
162
+ hint "Install it: pip install openenv-core"
163
+ stop_at "Step 3"
164
+ fi
165
+
166
+ VALIDATE_OK=false
167
+ VALIDATE_OUTPUT=$(cd "$REPO_DIR" && openenv validate 2>&1) && VALIDATE_OK=true
168
+
169
+ if [ "$VALIDATE_OK" = true ]; then
170
+ pass "openenv validate passed"
171
+ [ -n "$VALIDATE_OUTPUT" ] && log " $VALIDATE_OUTPUT"
172
+ else
173
+ fail "openenv validate failed"
174
+ printf "%s\n" "$VALIDATE_OUTPUT"
175
+ stop_at "Step 3"
176
+ fi
177
+
178
+ printf "\n"
179
+ printf "${BOLD}========================================${NC}\n"
180
+ printf "${GREEN}${BOLD} All 3/3 checks passed!${NC}\n"
181
+ printf "${GREEN}${BOLD} Your submission is ready to submit.${NC}\n"
182
+ printf "${BOLD}========================================${NC}\n"
183
+ printf "\n"
184
+
185
+ exit 0