localcode_query.sh
1 #!/usr/bin/env bash 2 # LocalCode Query Interface 3 # Sends queries to local LLM with full session context 4 # Usage: ./localcode_query.sh <session_id> "your question" 5 6 set -euo pipefail 7 8 # Configuration 9 SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 10 SESSIONS_DIR="${HOME}/.localcode/sessions" 11 OLLAMA_MODEL="${LLM_MODEL:-deepseek-coder:6.7b}" 12 OLLAMA_ENDPOINT="${OLLAMA_ENDPOINT:-http://localhost:11434}" 13 OLLAMA_TIMEOUT="${LLM_TIMEOUT:-180}" # Default 3 minutes (180s) for local LLMs with large context 14 CONTEXT_BUILDER="$SCRIPT_DIR/context_builder.sh" 15 16 # Color output 17 RED='\033[0;31m' 18 GREEN='\033[0;32m' 19 YELLOW='\033[1;33m' 20 BLUE='\033[0;34m' 21 CYAN='\033[0;36m' 22 MAGENTA='\033[0;35m' 23 NC='\033[0m' 24 25 info() { echo -e "${BLUE}ℹ${NC} $1" >&2; } 26 success() { echo -e "${GREEN}✓${NC} $1" >&2; } 27 error() { echo -e "${RED}✗${NC} $1" >&2; } 28 warn() { echo -e "${YELLOW}⚠${NC} $1" >&2; } 29 30 # Build complete query context 31 build_query_context() { 32 local session_dir="$1" 33 local question="$2" 34 35 local context_parts=() 36 37 # Part 1: Startup context (static, from session initialization) 38 if [[ -f "$session_dir/startup_context.txt" ]]; then 39 context_parts+=("$(cat "$session_dir/startup_context.txt")") 40 fi 41 42 # Part 2: Conversation history (last 5 turns for context) 43 local conv_history=$(jq -r ' 44 .[-5:] | 45 map("[\(.role | ascii_upcase)] \(.content)") | 46 join("\n\n") 47 ' "$session_dir/conversation.json" 2>/dev/null || echo "") 48 49 if [[ -n "$conv_history" ]]; then 50 context_parts+=("## CONVERSATION HISTORY (Last 5 turns)") 51 context_parts+=("$conv_history") 52 context_parts+=("") 53 fi 54 55 # Part 3: Recent tool results (last 3) 56 local tool_results=$(jq -r ' 57 .[-3:] | 58 map("[TOOL: \(.tool)] \(.result | .[0:500])") | 59 join("\n\n") 60 ' "$session_dir/tool_results.json" 2>/dev/null || echo "") 61 62 if [[ -n "$tool_results" ]]; then 63 context_parts+=("## RECENT TOOL EXECUTIONS") 64 context_parts+=("$tool_results") 65 context_parts+=("") 66 fi 67 68 # Part 4: Current question 69 context_parts+=("## CURRENT QUESTION") 70 context_parts+=("$question") 71 context_parts+=("") 72 context_parts+=("Provide a clear, technical answer. If you need to see file contents or run commands, say: 'TOOL_REQUEST: read_file(path)' or 'TOOL_REQUEST: grep_code(pattern)' or 'TOOL_REQUEST: run_bash(command)'") 73 74 # Combine all parts 75 printf "%s\n" "${context_parts[@]}" 76 } 77 78 # Query Ollama with context 79 query_llm() { 80 local full_context="$1" 81 local model="$2" 82 83 info "Querying $model (timeout: ${OLLAMA_TIMEOUT}s)..." 84 85 # Use configurable timeout 86 local response=$(curl -s -m "$OLLAMA_TIMEOUT" "$OLLAMA_ENDPOINT/api/generate" \ 87 -d "$(jq -n --arg model "$model" --arg prompt "$full_context" '{ 88 model: $model, 89 prompt: $prompt, 90 stream: false, 91 options: { 92 temperature: 0.7, 93 num_ctx: 8192 94 } 95 }')" 2>&1) 96 97 # Check for curl errors 98 if [[ $? -ne 0 ]]; then 99 error "Curl failed: $response" 100 return 1 101 fi 102 103 # Extract response from JSON 104 response=$(echo "$response" | jq -r '.response' 2>/dev/null) 105 106 if [[ -z "$response" ]]; then 107 error "Failed to get response from Ollama" 108 return 1 109 fi 110 111 echo "$response" 112 } 113 114 # Parse tool requests from LLM response 115 parse_tool_requests() { 116 local response="$1" 117 118 # Extract TOOL_REQUEST: patterns 119 echo "$response" | grep -o 'TOOL_REQUEST: [^)]*)' 120 } 121 122 # Execute tool 123 execute_tool() { 124 local tool_call="$1" 125 local session_dir="$2" 126 127 local project_path=$(jq -r '.project_path' "$session_dir/session.json") 128 129 info "Executing tool: $tool_call" 130 131 local result="" 132 133 case "$tool_call" in 134 read_file*) 135 local file_path=$(echo "$tool_call" | grep -oP 'read_file\(\K[^\)]+' | tr -d '"' | tr -d "'") 136 if [[ -f "$project_path/$file_path" ]]; then 137 result=$(head -100 "$project_path/$file_path") 138 success "Read file: $file_path (first 100 lines)" 139 else 140 result="ERROR: File not found: $file_path" 141 error "$result" 142 fi 143 ;; 144 145 grep_code*) 146 local pattern=$(echo "$tool_call" | grep -oP 'grep_code\(\K[^\)]+' | tr -d '"' | tr -d "'") 147 result=$(cd "$project_path" && grep -r "$pattern" . 2>/dev/null | head -20) 148 success "Searched for: $pattern" 149 ;; 150 151 glob_files*) 152 local glob_pattern=$(echo "$tool_call" | grep -oP 'glob_files\(\K[^\)]+' | tr -d '"' | tr -d "'") 153 result=$(cd "$project_path" && find . -name "$glob_pattern" 2>/dev/null | head -20) 154 success "Found files matching: $glob_pattern" 155 ;; 156 157 run_bash*) 158 local command=$(echo "$tool_call" | grep -oP 'run_bash\(\K[^\)]+' | tr -d '"' | tr -d "'") 159 result=$(cd "$project_path" && bash -c "$command" 2>&1 | head -50) 160 success "Executed: $command" 161 ;; 162 163 *) 164 result="ERROR: Unknown tool: $tool_call" 165 error "$result" 166 ;; 167 esac 168 169 # Store tool result 170 local tool_entry=$(jq -n \ 171 --arg tool "$tool_call" \ 172 --arg result "$result" \ 173 --arg timestamp "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \ 174 '{tool: $tool, result: $result, timestamp: $timestamp}') 175 176 jq ". += [$tool_entry]" "$session_dir/tool_results.json" > "$session_dir/tool_results.json.tmp" 177 mv "$session_dir/tool_results.json.tmp" "$session_dir/tool_results.json" 178 179 echo "$result" 180 } 181 182 # Save conversation turn 183 save_turn() { 184 local session_dir="$1" 185 local question="$2" 186 local response="$3" 187 188 # Add question 189 local user_entry=$(jq -n --arg content "$question" '{role: "user", content: $content}') 190 jq ". += [$user_entry]" "$session_dir/conversation.json" > "$session_dir/conversation.json.tmp" 191 mv "$session_dir/conversation.json.tmp" "$session_dir/conversation.json" 192 193 # Add response 194 local assistant_entry=$(jq -n --arg content "$response" '{role: "assistant", content: $content}') 195 jq ". += [$assistant_entry]" "$session_dir/conversation.json" > "$session_dir/conversation.json.tmp" 196 mv "$session_dir/conversation.json.tmp" "$session_dir/conversation.json" 197 198 # Increment turn counter 199 local turn=$(jq -r '.conversation_turn' "$session_dir/session.json") 200 jq ".conversation_turn = $((turn + 1))" "$session_dir/session.json" > "$session_dir/session.json.tmp" 201 mv "$session_dir/session.json.tmp" "$session_dir/session.json" 202 } 203 204 # Main query function 205 query() { 206 local session_id="$1" 207 local question="$2" 208 local session_dir="$SESSIONS_DIR/$session_id" 209 210 if [[ ! -d "$session_dir" ]]; then 211 error "Session not found: $session_id" 212 return 1 213 fi 214 215 local model=$(jq -r '.model' "$session_dir/session.json") 216 local project_path=$(jq -r '.project_path' "$session_dir/session.json") 217 218 echo -e "${CYAN}═══════════════════════════════════════════════════════════${NC}" >&2 219 echo -e "${CYAN}LocalCode Query${NC} (Session: $session_id)" >&2 220 echo -e "${CYAN}═══════════════════════════════════════════════════════════${NC}" >&2 221 echo "" >&2 222 223 # Build full context 224 info "Building query context..." 225 local full_context=$(build_query_context "$session_dir" "$question") 226 227 # Show context size and warn if too large 228 local context_size=$(echo "$full_context" | wc -c) 229 local estimated_tokens=$((context_size / 4)) 230 info "Context size: $context_size bytes (~$estimated_tokens tokens)" 231 232 # Warn if context is approaching or exceeding limits 233 if [[ $estimated_tokens -gt 6000 ]]; then 234 error "⚠️ Context TOO LARGE ($estimated_tokens tokens)! May fail with 8K window." 235 error " Reduce by: shortening question, clearing old tool results, or starting new session" 236 return 1 237 elif [[ $estimated_tokens -gt 4000 ]]; then 238 warn "⚠️ Context large ($estimated_tokens tokens). Approaching 8K limit (~6K safe max)" 239 warn " Consider starting fresh session if responses slow or fail" 240 elif [[ $estimated_tokens -gt 3000 ]]; then 241 warn "Context moderate ($estimated_tokens tokens). Still safe for 8K window" 242 fi 243 244 # Query LLM 245 local response=$(query_llm "$full_context" "$model") 246 247 if [[ -z "$response" ]]; then 248 error "No response from LLM" 249 return 1 250 fi 251 252 # Check for tool requests 253 local tool_requests=$(parse_tool_requests "$response" || echo "") 254 255 if [[ -n "$tool_requests" ]]; then 256 echo "" >&2 257 warn "LLM requested tools:" 258 echo "$tool_requests" >&2 259 echo "" >&2 260 261 # Execute tools and re-query 262 local tool_results_text="" 263 while IFS= read -r tool_call; do 264 if [[ -n "$tool_call" ]]; then 265 local result=$(execute_tool "$tool_call" "$session_dir") 266 tool_results_text+="\n[TOOL RESULT: $tool_call]\n$result\n" 267 fi 268 done <<< "$tool_requests" 269 270 # Re-query with tool results 271 local updated_context=$(printf "%s\n\n## TOOL RESULTS\n%s\n\nNow answer the original question with this additional information." "$full_context" "$tool_results_text") 272 response=$(query_llm "$updated_context" "$model") 273 fi 274 275 # Save turn 276 save_turn "$session_dir" "$question" "$response" 277 278 # Output response 279 echo "" >&2 280 echo -e "${GREEN}═══════════════════════════════════════════════════════════${NC}" >&2 281 echo -e "${GREEN}Response from $model:${NC}" >&2 282 echo -e "${GREEN}═══════════════════════════════════════════════════════════${NC}" >&2 283 echo "" 284 echo "$response" 285 echo "" 286 echo -e "${CYAN}═══════════════════════════════════════════════════════════${NC}" >&2 287 } 288 289 # Interactive mode 290 interactive() { 291 local session_id="$1" 292 293 echo -e "${CYAN}═══════════════════════════════════════════════════════════${NC}" 294 echo -e "${CYAN}LocalCode Interactive Mode${NC}" 295 echo -e "${CYAN}═══════════════════════════════════════════════════════════${NC}" 296 echo "" 297 echo "Session: $session_id" 298 echo "Type 'exit' to quit, 'help' for commands" 299 echo "" 300 301 while true; do 302 echo -ne "${MAGENTA}localcode>${NC} " 303 read -r input 304 305 case "$input" in 306 exit|quit) 307 echo "Goodbye!" 308 break 309 ;; 310 help) 311 echo "Commands:" 312 echo " exit/quit - Exit interactive mode" 313 echo " help - Show this help" 314 echo " Any other text - Send as query to LLM" 315 ;; 316 "") 317 continue 318 ;; 319 *) 320 query "$session_id" "$input" 321 ;; 322 esac 323 echo "" 324 done 325 } 326 327 # Main CLI 328 main() { 329 if [[ $# -lt 2 ]]; then 330 cat <<EOF >&2 331 Usage: $0 <session_id> "<question>" 332 or: $0 <session_id> --interactive 333 334 Examples: 335 # Single query 336 $0 session_20250111_123456 "What's the ECHO architecture?" 337 338 # Interactive mode 339 $0 session_20250111_123456 --interactive 340 EOF 341 exit 1 342 fi 343 344 local session_id="$1" 345 shift 346 347 if [[ "$1" == "--interactive" ]] || [[ "$1" == "-i" ]]; then 348 interactive "$session_id" 349 else 350 query "$session_id" "$*" 351 fi 352 } 353 354 # Run if called directly 355 if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then 356 main "$@" 357 fi