char-fork/llama.cpp
git clone https://git.t4t.associates/char-fork/llama.cpp
67672dc5b
char/aliceai-t5-moe
1#!/usr/bin/env python3 2import sys 3import json 4import argparse 5import jinja2 .ext as jinja2_ext 6from PySide6 .QtWidgets import ( 7QApplication , 8QMainWindow , 9QWidget , 10QVBoxLayout , 11QHBoxLayout , 12QLabel , 13QPlainTextEdit , 14QTextEdit , 15QPushButton , 16QFileDialog , 17) 18from PySide6 .QtGui import QColor ,QColorConstants ,QTextCursor ,QTextFormat 19from PySide6 .QtCore import Qt ,QRect ,QSize 20from jinja2 import TemplateSyntaxError 21from jinja2 .sandbox import ImmutableSandboxedEnvironment 22from datetime import datetime 23 24 25def format_template_content (template_content ): 26"""Format the Jinja template content using Jinja2's lexer.""" 27if not template_content .strip (): 28return template_content 29 30env = ImmutableSandboxedEnvironment () 31tc_rstrip = template_content .rstrip () 32tokens = list (env .lex (tc_rstrip )) 33result = "" 34indent_level = 0 35i = 0 36 37while i < len (tokens ): 38token = tokens [i ] 39_ ,token_type ,token_value = token 40 41if token_type == "block_begin" : 42block_start = i 43# Collect all tokens for this block construct 44construct_content = token_value 45end_token_type = token_type .replace ("_begin" ,"_end" ) 46j = i + 1 47while j < len (tokens )and tokens [j ][1 ]!= end_token_type : 48construct_content += tokens [j ][2 ] 49j += 1 50 51if j < len (tokens ):# Found the end token 52construct_content += tokens [j ][2 ] 53i = j # Skip to the end token 54 55# Check for control structure keywords for indentation 56stripped_content = construct_content .strip () 57instr = block_start + 1 58while tokens [instr ][1 ]== "whitespace" : 59instr = instr + 1 60 61instruction_token = tokens [instr ][2 ] 62start_control_tokens = ["if" ,"for" ,"macro" ,"call" ,"block" ] 63end_control_tokens = ["end" + t for t in start_control_tokens ] 64is_control_start = any ( 65instruction_token .startswith (kw )for kw in start_control_tokens 66 ) 67is_control_end = any ( 68instruction_token .startswith (kw )for kw in end_control_tokens 69 ) 70 71# Adjust indentation for control structures 72# For control end blocks, decrease indent BEFORE adding the content 73if is_control_end : 74indent_level = max (0 ,indent_level - 1 ) 75 76# Remove all previous whitespace before this block 77result = result .rstrip () 78 79# Add proper indent, but only if this is not the first token 80added_newline = False 81if result :# Only add newline and indent if there's already content 82result += ( 83"\n" + " " * indent_level 84 )# Use 2 spaces per indent level 85added_newline = True 86else :# For the first token, don't add any indent 87result += "" 88 89# Add the block content 90result += stripped_content 91 92# Add '-' after '%' if it wasn't there and we added a newline or indent 93if ( 94added_newline 95and stripped_content .startswith ("{%" ) 96and not stripped_content .startswith ("{%-" ) 97 ): 98# Add '-' at the beginning 99result = ( 100result [:result .rfind ("{%" )] 101+ "{%-" 102+ result [result .rfind ("{%" )+ 2 :] 103 ) 104if stripped_content .endswith ("%}" )and not stripped_content .endswith ( 105"-%}" 106 ): 107# Only add '-' if this is not the last token or if there's content after 108if i + 1 < len (tokens )and tokens [i + 1 ][1 ]!= "eof" : 109result = result [:- 2 ]+ "-%}" 110 111# For control start blocks, increase indent AFTER adding the content 112if is_control_start : 113indent_level += 1 114else : 115# Malformed template, just add the token 116result += token_value 117elif token_type == "variable_begin" : 118# Collect all tokens for this variable construct 119construct_content = token_value 120end_token_type = token_type .replace ("_begin" ,"_end" ) 121j = i + 1 122while j < len (tokens )and tokens [j ][1 ]!= end_token_type : 123construct_content += tokens [j ][2 ] 124j += 1 125 126if j < len (tokens ):# Found the end token 127construct_content += tokens [j ][2 ] 128i = j # Skip to the end token 129 130# For variable constructs, leave them alone 131# Do not add indent or whitespace before or after them 132result += construct_content 133else : 134# Malformed template, just add the token 135result += token_value 136elif token_type == "data" : 137# Handle data (text between Jinja constructs) 138# For data content, preserve it as is 139result += token_value 140else : 141# Handle any other tokens 142result += token_value 143 144i += 1 145 146# Clean up trailing newlines and spaces 147result = result .rstrip () 148 149# Copy the newline / space count from the original 150if (trailing_length := len (template_content )- len (tc_rstrip )): 151result += template_content [- trailing_length :] 152 153return result 154 155 156# ------------------------ 157# Line Number Widget 158# ------------------------ 159class LineNumberArea (QWidget ): 160def __init__ (self ,editor ): 161super ().__init__ (editor ) 162self .code_editor = editor 163 164def sizeHint (self ): 165return QSize (self .code_editor .line_number_area_width (),0 ) 166 167def paintEvent (self ,event ): 168self .code_editor .line_number_area_paint_event (event ) 169 170 171class CodeEditor (QPlainTextEdit ): 172def __init__ (self ): 173super ().__init__ () 174self .line_number_area = LineNumberArea (self ) 175 176self .blockCountChanged .connect (self .update_line_number_area_width ) 177self .updateRequest .connect (self .update_line_number_area ) 178self .cursorPositionChanged .connect (self .highlight_current_line ) 179 180self .update_line_number_area_width (0 ) 181self .highlight_current_line () 182 183def line_number_area_width (self ): 184digits = len (str (self .blockCount ())) 185space = 3 + self .fontMetrics ().horizontalAdvance ("9" )* digits 186return space 187 188def update_line_number_area_width (self ,_ ): 189self .setViewportMargins (self .line_number_area_width (),0 ,0 ,0 ) 190 191def update_line_number_area (self ,rect ,dy ): 192if dy : 193self .line_number_area .scroll (0 ,dy ) 194else : 195self .line_number_area .update ( 1960 ,rect .y (),self .line_number_area .width (),rect .height () 197 ) 198 199if rect .contains (self .viewport ().rect ()): 200self .update_line_number_area_width (0 ) 201 202def resizeEvent (self ,event ): 203super ().resizeEvent (event ) 204cr = self .contentsRect () 205self .line_number_area .setGeometry ( 206QRect (cr .left (),cr .top (),self .line_number_area_width (),cr .height ()) 207 ) 208 209def line_number_area_paint_event (self ,event ): 210from PySide6 .QtGui import QPainter 211 212painter = QPainter (self .line_number_area ) 213painter .fillRect (event .rect (),QColorConstants .LightGray ) 214 215block = self .firstVisibleBlock () 216block_number = block .blockNumber () 217top = int ( 218self .blockBoundingGeometry (block ).translated (self .contentOffset ()).top () 219 ) 220bottom = top + int (self .blockBoundingRect (block ).height ()) 221 222while block .isValid ()and top <= event .rect ().bottom (): 223if block .isVisible ()and bottom >= event .rect ().top (): 224number = str (block_number + 1 ) 225painter .setPen (QColorConstants .Black ) 226painter .drawText ( 2270 , 228top , 229self .line_number_area .width ()- 2 , 230self .fontMetrics ().height (), 231Qt .AlignmentFlag .AlignRight , 232number , 233 ) 234block = block .next () 235top = bottom 236bottom = top + int (self .blockBoundingRect (block ).height ()) 237block_number += 1 238 239def highlight_current_line (self ): 240extra_selections = [] 241if not self .isReadOnly (): 242selection = QTextEdit .ExtraSelection () 243line_color = QColorConstants .Yellow .lighter (160 ) 244selection .format .setBackground (line_color )# pyright: ignore[reportAttributeAccessIssue] # ty: ignore[unresolved-attribute] 245selection .format .setProperty (QTextFormat .Property .FullWidthSelection ,True )# pyright: ignore[reportAttributeAccessIssue] # ty: ignore[unresolved-attribute] 246selection .cursor = self .textCursor ()# pyright: ignore[reportAttributeAccessIssue] # ty: ignore[unresolved-attribute] 247selection .cursor .clearSelection ()# pyright: ignore[reportAttributeAccessIssue] # ty: ignore[unresolved-attribute] 248extra_selections .append (selection ) 249self .setExtraSelections (extra_selections ) 250 251def highlight_position (self ,lineno :int ,col :int ,color :QColor ): 252block = self .document ().findBlockByLineNumber (lineno - 1 ) 253if block .isValid (): 254cursor = QTextCursor (block ) 255text = block .text () 256start = block .position ()+ max (0 ,col - 1 ) 257cursor .setPosition (start ) 258if col <= len (text ): 259cursor .movePosition ( 260QTextCursor .MoveOperation .NextCharacter , 261QTextCursor .MoveMode .KeepAnchor , 262 ) 263 264extra = QTextEdit .ExtraSelection () 265extra .format .setBackground (color .lighter (160 ))# pyright: ignore[reportAttributeAccessIssue] # ty: ignore[unresolved-attribute] 266extra .cursor = cursor # pyright: ignore[reportAttributeAccessIssue] # ty: ignore[unresolved-attribute] 267 268self .setExtraSelections (self .extraSelections ()+ [extra ]) 269 270def highlight_line (self ,lineno :int ,color :QColor ): 271block = self .document ().findBlockByLineNumber (lineno - 1 ) 272if block .isValid (): 273cursor = QTextCursor (block ) 274cursor .select (QTextCursor .SelectionType .LineUnderCursor ) 275 276extra = QTextEdit .ExtraSelection () 277extra .format .setBackground (color .lighter (160 ))# pyright: ignore[reportAttributeAccessIssue] # ty: ignore[unresolved-attribute] 278extra .cursor = cursor # pyright: ignore[reportAttributeAccessIssue] # ty: ignore[unresolved-attribute] 279 280self .setExtraSelections (self .extraSelections ()+ [extra ]) 281 282def clear_highlighting (self ): 283self .highlight_current_line () 284 285 286# ------------------------ 287# Main App 288# ------------------------ 289class JinjaTester (QMainWindow ): 290def __init__ (self ): 291super ().__init__ () 292self .setWindowTitle ("Jinja Template Tester" ) 293self .resize (1200 ,800 ) 294 295central = QWidget () 296main_layout = QVBoxLayout (central ) 297 298# -------- Top input area -------- 299input_layout = QHBoxLayout () 300 301# Template editor with label 302template_layout = QVBoxLayout () 303template_label = QLabel ("Jinja2 Template" ) 304template_layout .addWidget (template_label ) 305self .template_edit = CodeEditor () 306template_layout .addWidget (self .template_edit ) 307input_layout .addLayout (template_layout ) 308 309# JSON editor with label 310json_layout = QVBoxLayout () 311json_label = QLabel ("Context (JSON)" ) 312json_layout .addWidget (json_label ) 313self .json_edit = CodeEditor () 314self .json_edit .setPlainText (""" 315{ 316"add_generation_prompt": true, 317"bos_token": "", 318"eos_token": "", 319"messages": [ 320{ 321"role": "user", 322"content": "What is the capital of Poland?" 323} 324] 325} 326""" .strip ()) 327json_layout .addWidget (self .json_edit ) 328input_layout .addLayout (json_layout ) 329 330main_layout .addLayout (input_layout ) 331 332# -------- Rendered output area -------- 333output_label = QLabel ("Rendered Output" ) 334main_layout .addWidget (output_label ) 335self .output_edit = QPlainTextEdit () 336self .output_edit .setReadOnly (True ) 337main_layout .addWidget (self .output_edit ) 338 339# -------- Render button and status -------- 340btn_layout = QHBoxLayout () 341 342# Load template button 343self .load_btn = QPushButton ("Load Template" ) 344self .load_btn .clicked .connect (self .load_template ) 345btn_layout .addWidget (self .load_btn ) 346 347# Format template button 348self .format_btn = QPushButton ("Format" ) 349self .format_btn .clicked .connect (self .format_template ) 350btn_layout .addWidget (self .format_btn ) 351 352self .render_btn = QPushButton ("Render" ) 353self .render_btn .clicked .connect (self .render_template ) 354btn_layout .addWidget (self .render_btn ) 355main_layout .addLayout (btn_layout ) 356 357# Status label below buttons 358self .status_label = QLabel ("Ready" ) 359main_layout .addWidget (self .status_label ) 360 361self .setCentralWidget (central ) 362 363def render_template (self ): 364self .template_edit .clear_highlighting () 365self .output_edit .clear () 366 367template_str = self .template_edit .toPlainText () 368json_str = self .json_edit .toPlainText () 369 370# Parse JSON context 371try : 372context = json .loads (json_str )if json_str .strip ()else {} 373except Exception as e : 374self .status_label .setText (f"❌ JSON Error: { e } " ) 375return 376 377def raise_exception (text :str )-> str : 378raise RuntimeError (text ) 379 380env = ImmutableSandboxedEnvironment ( 381trim_blocks = True , 382lstrip_blocks = True , 383extensions = [jinja2_ext .loopcontrols ], 384 ) 385env .filters ["tojson" ]= ( 386lambda x , 387indent = None , 388separators = None , 389sort_keys = False , 390ensure_ascii = False :json .dumps ( 391x , 392indent = indent , 393separators = separators , 394sort_keys = sort_keys , 395ensure_ascii = ensure_ascii , 396 ) 397 ) 398env .globals ["strftime_now" ]= lambda format :datetime .now ().strftime (format )# ty: ignore[invalid-assignment, invalid-argument-type] 399env .globals ["raise_exception" ]= raise_exception # ty: ignore[invalid-assignment] 400try : 401template = env .from_string (template_str ) 402output = template .render (context ) 403self .output_edit .setPlainText (output ) 404self .status_label .setText ("✅ Render successful" ) 405except TemplateSyntaxError as e : 406self .status_label .setText (f"❌ Syntax Error (line { e . lineno } ): { e . message } " ) 407if e .lineno : 408self .template_edit .highlight_line (e .lineno ,QColor ("red" )) 409except Exception as e : 410# Catch all runtime errors 411# Try to extract template line number 412lineno = None 413tb = e .__traceback__ 414while tb : 415frame = tb .tb_frame 416if frame .f_code .co_filename == "<template>" : 417lineno = tb .tb_lineno 418break 419tb = tb .tb_next 420 421error_msg = f"Runtime Error: { type ( e ). __name__ } : { e } " 422if lineno : 423error_msg = f"Runtime Error at line { lineno } in template: { type ( e ). __name__ } : { e } " 424self .template_edit .highlight_line (lineno ,QColor ("orange" )) 425 426self .output_edit .setPlainText (error_msg ) 427self .status_label .setText (f"❌ { error_msg } " ) 428 429def load_template (self ): 430"""Load a Jinja template from a file using a file dialog.""" 431file_path ,_ = QFileDialog .getOpenFileName ( 432self , 433"Load Jinja Template" , 434"" , 435"Template Files (*.jinja *.j2 *.html *.txt);;All Files (*)" , 436 ) 437 438if file_path : 439try : 440with open (file_path ,"r" ,encoding = "utf-8" )as file : 441content = file .read () 442self .template_edit .setPlainText (content ) 443self .status_label .setText (f"✅ Loaded template from { file_path } " ) 444except Exception as e : 445self .status_label .setText (f"❌ Error loading file: { str ( e ) } " ) 446 447def format_template (self ): 448"""Format the Jinja template using Jinja2's lexer for proper parsing.""" 449try : 450template_content = self .template_edit .toPlainText () 451if not template_content .strip (): 452self .status_label .setText ("⚠️ Template is empty" ) 453return 454 455formatted_content = format_template_content (template_content ) 456self .template_edit .setPlainText (formatted_content ) 457self .status_label .setText ("✅ Template formatted" ) 458except Exception as e : 459self .status_label .setText (f"❌ Error formatting template: { str ( e ) } " ) 460 461 462if __name__ == "__main__" : 463if len (sys .argv )> 1 : 464# CLI mode 465parser = argparse .ArgumentParser (description = "Jinja Template Tester" ) 466parser .add_argument ( 467"--template" ,required = True ,help = "Path to Jinja template file" 468 ) 469parser .add_argument ("--context" ,required = True ,help = "JSON string for context" ) 470parser .add_argument ( 471"--action" , 472choices = ["format" ,"render" ], 473default = "render" , 474help = "Action to perform" , 475 ) 476args = parser .parse_args () 477 478# Load template 479with open (args .template ,"r" ,encoding = "utf-8" )as f : 480template_content = f .read () 481 482# Load JSON 483context = json .loads (args .context ) 484# Add missing variables 485context .setdefault ("bos_token" ,"" ) 486context .setdefault ("eos_token" ,"" ) 487context .setdefault ("add_generation_prompt" ,False ) 488 489env = ImmutableSandboxedEnvironment () 490 491if args .action == "format" : 492formatted = format_template_content (template_content ) 493formatted )# noqa: NP100 494elif args .action == "render" : 495template = env .from_string (template_content ) 496output = template .render (context ) 497output )# noqa: NP100 498 499else : 500# GUI mode 501app = QApplication (sys .argv ) 502window = JinjaTester () 503window .show () 504sys .exit (app .exec ())