/ scripts / llm / localcode_query.sh
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