Spaces:
Running
Running
File size: 15,170 Bytes
d0808a4 7389066 b6b64c4 7389066 b6b64c4 7389066 b6b64c4 7389066 b6b64c4 7389066 b6b64c4 7389066 b6b64c4 7389066 b6b64c4 bace889 b6b64c4 bace889 b6b64c4 7389066 b6b64c4 7389066 b6b64c4 7389066 bace889 7389066 b6b64c4 7389066 b6b64c4 7389066 b6b64c4 7389066 b6b64c4 7389066 b6b64c4 7389066 b6b64c4 7389066 b6b64c4 7389066 b6b64c4 7389066 d0808a4 b6b64c4 d0808a4 b6b64c4 d0808a4 b6b64c4 d0808a4 b6b64c4 d0808a4 b6b64c4 d0808a4 b6b64c4 d0808a4 b6b64c4 d0808a4 b6b64c4 d0808a4 b6b64c4 d0808a4 b6b64c4 d0808a4 b6b64c4 d0808a4 b6b64c4 d0808a4 b6b64c4 d0808a4 b6b64c4 d0808a4 b6b64c4 d0808a4 b6b64c4 d0808a4 b6b64c4 d0808a4 b6b64c4 d0808a4 b6b64c4 d0808a4 b6b64c4 d0808a4 b6b64c4 d0808a4 b6b64c4 d0808a4 b6b64c4 d0808a4 b6b64c4 d0808a4 b6b64c4 d0808a4 b6b64c4 d0808a4 b6b64c4 d0808a4 b6b64c4 d0808a4 b6b64c4 d0808a4 b6b64c4 d0808a4 b6b64c4 d0808a4 b6b64c4 d0808a4 b6b64c4 d0808a4 b6b64c4 d0808a4 b6b64c4 d0808a4 b6b64c4 d0808a4 b6b64c4 d0808a4 b6b64c4 d0808a4 b6b64c4 d0808a4 b6b64c4 d0808a4 b6b64c4 d0808a4 7389066 d0808a4 7389066 d0808a4 b6b64c4 d0808a4 7389066 d0808a4 7389066 d0808a4 7389066 d0808a4 b6b64c4 d0808a4 b6b64c4 d0808a4 b6b64c4 d0808a4 b6b64c4 d0808a4 b6b64c4 d0808a4 b6b64c4 d0808a4 b6b64c4 d0808a4 b6b64c4 d0808a4 b6b64c4 d0808a4 b6b64c4 d0808a4 b6b64c4 d0808a4 b6b64c4 d0808a4 b6b64c4 5cb7289 305bf67 b6b64c4 305bf67 5cb7289 b6b64c4 5cb7289 b6b64c4 5cb7289 b6b64c4 d0808a4 b6b64c4 |
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 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 |
import gradio as gr
import subprocess
import json
import os
from typing import Dict, Any, List, Optional
import requests
from functools import lru_cache
import time
# Cache documentation for 1 hour to avoid excessive requests
@lru_cache(maxsize=10)
def fetch_liftoscript_docs_cached(url: str, cache_time: float) -> Optional[str]:
"""Fetch documentation from Liftosaur website with caching."""
try:
response = requests.get(url, timeout=10)
response.raise_for_status()
return response.text
except Exception as e:
print(f"Error fetching docs from {url}: {e}")
return None
def get_liftoscript_documentation(topic: str = "overview") -> Dict[str, Any]:
"""
Access official Liftoscript documentation from liftosaur.com.
This tool fetches the latest documentation directly from the official
Liftosaur website to ensure accuracy and up-to-date information.
Args:
topic: Documentation section to retrieve. Options:
- overview: General introduction to Liftoscript
- syntax: Exercise notation and formatting rules
- progressions: Built-in progression types
- all: Complete documentation
Returns:
Dictionary containing:
- topic: The requested topic name
- content: Documentation text from official source
- source: URL where documentation was fetched from
- cached: Whether this is cached data
- error: True if there was an error fetching docs
"""
# Use current time for cache invalidation (1 hour cache)
cache_time = time.time() // 3600
# Use Jina Reader for clean markdown conversion
base_url = "https://www.liftosaur.com/blog/docs/"
jina_url = f"https://r.jina.ai/{base_url}"
# Fetch documentation via Jina Reader
main_docs = fetch_liftoscript_docs_cached(jina_url, cache_time)
if not main_docs:
# Fallback to basic documentation if fetch fails
return {
"topic": topic,
"content": """# Liftoscript Quick Reference (Offline)
## Basic Format
Exercise Name / Sets x Reps / Weight / progress: type
## Progression Types
- lp(weight): Linear progression
- dp(weight, minReps, maxReps): Double progression
- sum(totalReps, weight): Sum of reps progression
- custom() { ... }: Custom JavaScript-like logic
## Weight Notation
- Absolute: 135lb, 60kg
- Percentage: 70%, 85%
- RPE: 3x5 @8 (3 sets of 5 at RPE 8)
Note: Full documentation available at https://www.liftosaur.com/docs""",
"source": "offline_fallback",
"cached": False,
"error": True,
}
# Parse specific sections based on topic
if topic == "all":
return {
"topic": "all",
"content": main_docs,
"source": base_url, # Show original URL, not Jina URL
"cached": True,
"error": False,
}
# Extract specific sections from the documentation
# This is a simplified extraction - in production you'd parse the HTML/markdown properly
sections = {
"overview": "Liftoscript is a programming language for defining workout programs",
"syntax": "Exercise format, sets/reps notation, weight formats",
"progressions": "Linear (lp), Double (dp), Sum, and Custom progressions",
}
if topic in sections:
# Try to extract the specific section from main_docs
# In a real implementation, you'd parse the HTML/markdown structure
content = f"# {topic.title()}\n\n{sections[topic]}\n\nFor complete documentation, see: {base_url}"
return {
"topic": topic,
"content": content,
"source": base_url,
"cached": True,
"error": False,
}
return {
"topic": topic,
"content": f"Unknown topic '{topic}'. Available topics: overview, syntax, progressions, all",
"source": base_url,
"cached": False,
"error": True,
}
def setup_parser():
"""Setup the minimal parser - just needs npm install."""
try:
# Check if Node.js is available
result = subprocess.run(["node", "--version"], capture_output=True, text=True)
if result.returncode != 0:
return False, "Node.js not found. Please ensure Node.js is installed."
# Check if dependencies are installed
if not os.path.exists("node_modules/@lezer/lr"):
print("Installing dependencies...")
install_result = subprocess.run(
["npm", "install"], capture_output=True, text=True, timeout=60
)
if install_result.returncode != 0:
return False, f"Failed to install dependencies: {install_result.stderr}"
# Check if parser files exist
required_files = [
"minimal-validator.js",
"liftoscript-parser.js",
"planner-parser.js",
]
missing_files = [f for f in required_files if not os.path.exists(f)]
if missing_files:
return False, f"Missing required files: {', '.join(missing_files)}"
# Test the validator
test_result = subprocess.run(
["node", "minimal-validator.js", "state.weight = 100lb", "--json"],
capture_output=True,
text=True,
timeout=5,
)
if test_result.returncode != 0:
return False, "Validator test failed"
return True, "Parser ready (minimal Lezer-based validator)"
except Exception as e:
return False, f"Setup error: {str(e)}"
def validate_liftoscript(script: str) -> Dict[str, Any]:
"""
Validate a Liftoscript program using the minimal Lezer-based parser.
This tool validates Liftoscript workout programs and returns detailed error information
if the syntax is invalid. It supports both planner format (Week/Day/Exercise) and
pure Liftoscript expressions.
Args:
script: The Liftoscript code to validate (either planner format or expression)
Returns:
Validation result dictionary with keys:
- valid (bool): Whether the script is syntactically valid
- error (str|None): Error message if invalid
- line (int|None): Line number of error
- column (int|None): Column number of error
- type (str|None): Detected script type ('planner' or 'liftoscript')
"""
if not script or not script.strip():
return {
"valid": False,
"error": "Empty script provided",
"line": None,
"column": None,
"type": None,
}
try:
# Use the minimal parser wrapper
wrapper = (
"parser-wrapper-minimal.js"
if os.path.exists("parser-wrapper-minimal.js")
else "minimal-validator.js"
)
# Call the validator
result = subprocess.run(
["node", wrapper, "-"]
if wrapper == "parser-wrapper-minimal.js"
else ["node", wrapper, "-", "--json"],
input=script,
capture_output=True,
text=True,
timeout=10,
)
if result.returncode != 0 and not result.stdout:
return {
"valid": False,
"error": f"Validator error: {result.stderr}",
"line": None,
"column": None,
"type": None,
}
# Parse JSON response
try:
validation_result = json.loads(result.stdout)
# Handle both formats (wrapper and direct)
if "errors" in validation_result:
# Wrapper format
if (
validation_result.get("errors")
and len(validation_result["errors"]) > 0
):
first_error = validation_result["errors"][0]
return {
"valid": False,
"error": first_error.get("message", "Unknown error"),
"line": first_error.get("line"),
"column": first_error.get("column"),
"type": validation_result.get("type"),
}
else:
return {
"valid": True,
"error": None,
"line": None,
"column": None,
"type": validation_result.get("type", "unknown"),
}
else:
# Direct format
if validation_result.get("valid"):
return {
"valid": True,
"error": None,
"line": None,
"column": None,
"type": validation_result.get("type", "unknown"),
}
else:
error = validation_result.get("error", {})
return {
"valid": False,
"error": error.get("message", "Unknown error"),
"line": error.get("line"),
"column": error.get("column"),
"type": error.get("type"),
}
except json.JSONDecodeError:
return {
"valid": False,
"error": "Invalid parser response",
"line": None,
"column": None,
"type": None,
}
except subprocess.TimeoutExpired:
return {
"valid": False,
"error": "Parser timeout - script too complex",
"line": None,
"column": None,
"type": None,
}
except Exception as e:
return {
"valid": False,
"error": f"Validation error: {str(e)}",
"line": None,
"column": None,
"type": None,
}
# Create Gradio interface
with gr.Blocks(title="Liftoscript MCP Server") as app:
gr.Markdown("""
# πͺ Liftoscript MCP Server
This MCP server provides tools for working with [Liftoscript](https://www.liftosaur.com/blog/docs/), the workout programming language used in Liftosaur.
## π οΈ Available MCP Tools
1. **validate_liftoscript** - Validate Liftoscript syntax and get detailed error information
2. **get_liftoscript_documentation** - Access official documentation for learning Liftoscript
## π¦ MCP Server Configuration
Add to your `claude_desktop_config.json`:
```json
{
"mcpServers": {
"liftoscript": {
"command": "npx",
"args": ["mcp-remote", "https://davanstrien-liftosaur-mcp.hf.space/gradio_api/mcp/sse"]
}
}
}
```
## π― Use Cases
- **For LLMs**: Access documentation and validate generated Liftoscript programs
- **For Developers**: Validate Liftoscript syntax in your applications
- **For Users**: Test and validate your Liftoscript programs
*Note: The documentation tool is available via MCP but not shown in this UI to keep it simple.*
""")
# Check parser setup
parser_ready, setup_message = setup_parser()
with gr.Row():
with gr.Column():
if parser_ready:
gr.Markdown(f"**β
Parser Status**: {setup_message}")
else:
gr.Markdown(f"**β οΈ Parser Status**: {setup_message}")
with gr.Row():
with gr.Column():
script_input = gr.Textbox(
label="Liftoscript Code",
placeholder="""Enter either Planner format:
# Week 1
## Day 1
Squat / 3x5 / progress: lp(5lb)
Bench Press / 3x8 @8
Or pure Liftoscript expressions:
if (completedReps >= reps) {
state.weight = state.weight + 5lb
}""",
lines=15,
)
validate_btn = gr.Button(
"Validate", variant="primary", interactive=parser_ready
)
with gr.Column():
validation_output = gr.JSON(label="Validation Result")
gr.Markdown("""
### π Example Scripts
Click any example below to test:
""")
gr.Examples(
examples=[
# Planner examples
[
"# Week 1\n## Day 1\nSquat / 3x5 / progress: lp(5lb)\nBench Press / 3x8"
],
["Deadlift / 1x5 / warmup: 1x5 135lb, 1x3 225lb, 1x1 315lb"],
["Bench Press / 3x8-12 @8 / progress: dp"],
# Liftoscript examples
["state.weight = 100lb"],
["if (completedReps >= reps) { state.weight += 5lb }"],
[
"for (r in completedReps) {\n if (r < reps[index]) {\n state.weight -= 5%\n }\n}"
],
# Invalid examples (for testing)
["// Missing closing brace\nif (true) { state.weight = 100lb"],
["// Invalid format\nSquat 3x5"],
],
inputs=script_input,
label="Example Scripts",
)
validate_btn.click(
fn=validate_liftoscript, inputs=script_input, outputs=validation_output
)
# Additional interface for documentation - keeps UI clean but exposes to MCP
gr.Markdown("---")
gr.Markdown("### Documentation Access (for MCP)")
with gr.Row():
doc_topic = gr.Dropdown(
choices=["overview", "syntax", "progressions", "all"],
label="Documentation Topic",
value="overview",
scale=1,
)
doc_btn = gr.Button("Get Documentation", scale=1)
doc_output = gr.JSON(label="Documentation")
doc_btn.click(
fn=get_liftoscript_documentation, inputs=doc_topic, outputs=doc_output
)
gr.Markdown("""
---
## π― Value Proposition
This minimal validator demonstrates that:
1. **Syntax validation** can be separated from full program evaluation
2. The Lezer parsers can be used **independently**
3. A standalone parser package would enable many tools and integrations
## π€ For Liftosaur Maintainers
This PoC shows demand for a standalone parser package. Benefits:
- Enable third-party tools (editors, linters, formatters)
- Support AI assistants like Claude via MCP
- Grow the Liftoscript ecosystem
- Minimal maintenance burden (just the parser, not the full runtime)
## π Resources
- [Liftoscript Documentation](https://www.liftosaur.com/docs/docs/liftoscript)
- [Liftosaur App](https://www.liftosaur.com/)
- [GitHub Repository](https://github.com/astashov/liftosaur)
- [Model Context Protocol](https://modelcontextprotocol.io/)
""")
# Launch with MCP server enabled
if __name__ == "__main__":
app.launch(mcp_server=True, server_name="0.0.0.0", share=False)
|