ai-consultants 2.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/commands/ai-consultants:consult.md +94 -0
- package/.claude/commands/ai-consultants:debate.md +103 -0
- package/.claude/commands/ai-consultants:help.md +30 -0
- package/CLAUDE.md +839 -0
- package/LICENSE +21 -0
- package/README.md +758 -0
- package/SKILL.md +156 -0
- package/bin/ai-consultants +96 -0
- package/docs/COST_RATES.md +172 -0
- package/docs/JSON_SCHEMA.md +305 -0
- package/docs/SETUP.md +879 -0
- package/docs/SMART_ROUTING.md +219 -0
- package/docs/cost_rates.json +143 -0
- package/package.json +47 -0
- package/references/configuration.md +141 -0
- package/references/details.md +60 -0
- package/scripts/build_context.sh +562 -0
- package/scripts/classify_question.sh +148 -0
- package/scripts/config.sh +753 -0
- package/scripts/configure.sh +1093 -0
- package/scripts/consult_all.sh +934 -0
- package/scripts/debate_round.sh +421 -0
- package/scripts/doctor.sh +642 -0
- package/scripts/followup.sh +197 -0
- package/scripts/install.sh +319 -0
- package/scripts/lib/api.sh +640 -0
- package/scripts/lib/api_query.sh +228 -0
- package/scripts/lib/cache.sh +338 -0
- package/scripts/lib/chunking.sh +880 -0
- package/scripts/lib/code_optimizer.sh +671 -0
- package/scripts/lib/common.sh +1088 -0
- package/scripts/lib/costs.sh +674 -0
- package/scripts/lib/personas.sh +372 -0
- package/scripts/lib/progress.sh +251 -0
- package/scripts/lib/reflection.sh +447 -0
- package/scripts/lib/routing.sh +566 -0
- package/scripts/lib/schema.json +165 -0
- package/scripts/lib/session.sh +327 -0
- package/scripts/lib/symbol_map.sh +445 -0
- package/scripts/lib/voting.sh +495 -0
- package/scripts/peer_review.sh +460 -0
- package/scripts/preflight_check.sh +452 -0
- package/scripts/query_aider.sh +83 -0
- package/scripts/query_amp.sh +67 -0
- package/scripts/query_claude.sh +99 -0
- package/scripts/query_codex.sh +92 -0
- package/scripts/query_cursor.sh +72 -0
- package/scripts/query_deepseek.sh +17 -0
- package/scripts/query_gemini.sh +85 -0
- package/scripts/query_glm.sh +17 -0
- package/scripts/query_grok.sh +17 -0
- package/scripts/query_kilo.sh +140 -0
- package/scripts/query_kimi.sh +67 -0
- package/scripts/query_minimax.sh +17 -0
- package/scripts/query_mistral.sh +97 -0
- package/scripts/query_ollama.sh +328 -0
- package/scripts/query_qwen3.sh +93 -0
- package/scripts/setup_wizard.sh +367 -0
- package/scripts/skillport-install.sh +196 -0
- package/scripts/synthesize.sh +332 -0
- package/templates/consultation_report.md +323 -0
- package/templates/synthesis_prompt.md +120 -0
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# api_query.sh - API mode query execution for AI Consultants v2.6
|
|
3
|
+
#
|
|
4
|
+
# This module provides a unified API query function that handles the
|
|
5
|
+
# differences between API formats (OpenAI, Anthropic, Google AI).
|
|
6
|
+
#
|
|
7
|
+
# Used by query_*.sh scripts when they are in API mode instead of CLI mode.
|
|
8
|
+
|
|
9
|
+
# Guard against double-sourcing
|
|
10
|
+
if [[ -n "${_API_QUERY_SH_SOURCED:-}" ]]; then
|
|
11
|
+
return 0 2>/dev/null || exit 0
|
|
12
|
+
fi
|
|
13
|
+
_API_QUERY_SH_SOURCED=1
|
|
14
|
+
|
|
15
|
+
# Load dependencies
|
|
16
|
+
SCRIPT_DIR_API_QUERY="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
17
|
+
source "$SCRIPT_DIR_API_QUERY/common.sh"
|
|
18
|
+
source "$SCRIPT_DIR_API_QUERY/api.sh"
|
|
19
|
+
|
|
20
|
+
# =============================================================================
|
|
21
|
+
# API QUERY EXECUTION
|
|
22
|
+
# =============================================================================
|
|
23
|
+
|
|
24
|
+
# Execute an API query for a consultant in API mode
|
|
25
|
+
#
|
|
26
|
+
# Usage: run_api_mode_query <consultant_name> <model> <query> <output_file> <timeout_sec>
|
|
27
|
+
#
|
|
28
|
+
# This function:
|
|
29
|
+
# 1. Determines the correct API format based on consultant name
|
|
30
|
+
# 2. Builds the appropriate request body
|
|
31
|
+
# 3. Executes the API call with retry logic
|
|
32
|
+
# 4. Parses the response
|
|
33
|
+
# 5. Returns the raw text response
|
|
34
|
+
#
|
|
35
|
+
# Returns:
|
|
36
|
+
# 0 on success (response written to output_file)
|
|
37
|
+
# 1 on auth failure
|
|
38
|
+
# 2 on rate limit
|
|
39
|
+
# 3 on server error
|
|
40
|
+
# 124 on timeout
|
|
41
|
+
run_api_mode_query() {
|
|
42
|
+
local consultant_name="$1"
|
|
43
|
+
local model="$2"
|
|
44
|
+
local query="$3"
|
|
45
|
+
local output_file="$4"
|
|
46
|
+
local timeout_seconds="${5:-180}"
|
|
47
|
+
|
|
48
|
+
# Get API configuration
|
|
49
|
+
local api_format
|
|
50
|
+
api_format=$(get_api_format "$consultant_name")
|
|
51
|
+
local api_key_var
|
|
52
|
+
api_key_var=$(get_api_key_var "$consultant_name")
|
|
53
|
+
local api_url
|
|
54
|
+
api_url=$(get_api_url "$consultant_name")
|
|
55
|
+
|
|
56
|
+
# Validate API key (except for Google AI where key is in URL)
|
|
57
|
+
if [[ "$api_format" != "google_ai" && -z "${!api_key_var:-}" ]]; then
|
|
58
|
+
log_error "[$consultant_name] API key not set: $api_key_var"
|
|
59
|
+
return 1
|
|
60
|
+
fi
|
|
61
|
+
|
|
62
|
+
# Build request body based on format
|
|
63
|
+
local request_body
|
|
64
|
+
local final_api_url="$api_url"
|
|
65
|
+
local auth_style="bearer"
|
|
66
|
+
|
|
67
|
+
case "$api_format" in
|
|
68
|
+
google_ai)
|
|
69
|
+
request_body=$(build_google_ai_request "$query")
|
|
70
|
+
# Google AI appends model to URL; use x-goog-api-key header for security
|
|
71
|
+
final_api_url="${api_url}/${model}:generateContent"
|
|
72
|
+
auth_style="google_ai"
|
|
73
|
+
;;
|
|
74
|
+
anthropic)
|
|
75
|
+
request_body=$(build_anthropic_request "$query" "$model")
|
|
76
|
+
auth_style="anthropic"
|
|
77
|
+
;;
|
|
78
|
+
qwen)
|
|
79
|
+
request_body=$(build_qwen_request "$query" "$model")
|
|
80
|
+
;;
|
|
81
|
+
*) # openai format
|
|
82
|
+
request_body=$(build_openai_request "$query" "$model")
|
|
83
|
+
;;
|
|
84
|
+
esac
|
|
85
|
+
|
|
86
|
+
log_debug "[$consultant_name] API format: $api_format"
|
|
87
|
+
log_debug "[$consultant_name] API URL: $final_api_url"
|
|
88
|
+
|
|
89
|
+
# Create temp file for raw API response
|
|
90
|
+
local temp_response
|
|
91
|
+
temp_response=$(mktemp)
|
|
92
|
+
|
|
93
|
+
# Execute API call using unified run_api_query with auth_style
|
|
94
|
+
run_api_query "$consultant_name" "$temp_response" "$timeout_seconds" "$final_api_url" "$api_key_var" "$request_body" "$auth_style"
|
|
95
|
+
local exit_code=$?
|
|
96
|
+
|
|
97
|
+
if [[ $exit_code -ne 0 ]]; then
|
|
98
|
+
rm -f "$temp_response"
|
|
99
|
+
return $exit_code
|
|
100
|
+
fi
|
|
101
|
+
|
|
102
|
+
# Parse response based on format
|
|
103
|
+
local raw_response
|
|
104
|
+
raw_response=$(cat "$temp_response")
|
|
105
|
+
|
|
106
|
+
local parsed_content
|
|
107
|
+
case "$api_format" in
|
|
108
|
+
google_ai)
|
|
109
|
+
parsed_content=$(parse_google_ai_response "$raw_response")
|
|
110
|
+
;;
|
|
111
|
+
anthropic)
|
|
112
|
+
parsed_content=$(parse_anthropic_response "$raw_response")
|
|
113
|
+
;;
|
|
114
|
+
qwen)
|
|
115
|
+
parsed_content=$(parse_qwen_response "$raw_response")
|
|
116
|
+
;;
|
|
117
|
+
*) # openai format
|
|
118
|
+
parsed_content=$(parse_openai_response "$raw_response")
|
|
119
|
+
;;
|
|
120
|
+
esac
|
|
121
|
+
|
|
122
|
+
if [[ -z "$parsed_content" ]]; then
|
|
123
|
+
log_error "[$consultant_name] Failed to parse API response"
|
|
124
|
+
rm -f "$temp_response"
|
|
125
|
+
return 1
|
|
126
|
+
fi
|
|
127
|
+
|
|
128
|
+
# Write parsed content to output
|
|
129
|
+
echo "$parsed_content" > "$output_file"
|
|
130
|
+
|
|
131
|
+
# Extract and log token usage
|
|
132
|
+
local tokens
|
|
133
|
+
tokens=$(extract_token_usage "$raw_response" "$api_format")
|
|
134
|
+
log_debug "[$consultant_name] Tokens used: $tokens"
|
|
135
|
+
|
|
136
|
+
rm -f "$temp_response"
|
|
137
|
+
return 0
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
# =============================================================================
|
|
141
|
+
# GENERIC API CONSULTANT FUNCTION (v2.6)
|
|
142
|
+
# =============================================================================
|
|
143
|
+
|
|
144
|
+
# Run a consultation using API mode for any API-based consultant
|
|
145
|
+
# This function is called by consult_all.sh for custom API agents and
|
|
146
|
+
# as a fallback when no dedicated query script exists.
|
|
147
|
+
#
|
|
148
|
+
# Usage: run_api_consultant <consultant_name> <query> <context_file> <output_file>
|
|
149
|
+
#
|
|
150
|
+
# Parameters:
|
|
151
|
+
# consultant_name - Name of the consultant (e.g., "Qwen3", "DeepSeek")
|
|
152
|
+
# query - The query text (can be empty if context_file provides all content)
|
|
153
|
+
# context_file - Path to context file (can be empty)
|
|
154
|
+
# output_file - Path to write the JSON response
|
|
155
|
+
#
|
|
156
|
+
# Returns:
|
|
157
|
+
# 0 on success
|
|
158
|
+
# 1 on failure
|
|
159
|
+
run_api_consultant() {
|
|
160
|
+
local consultant_name="$1"
|
|
161
|
+
local query="$2"
|
|
162
|
+
local context_file="$3"
|
|
163
|
+
local output_file="$4"
|
|
164
|
+
|
|
165
|
+
local consultant_upper
|
|
166
|
+
consultant_upper=$(to_upper "$consultant_name")
|
|
167
|
+
|
|
168
|
+
# Get configuration for this consultant
|
|
169
|
+
local model_var="${consultant_upper}_MODEL"
|
|
170
|
+
local timeout_var="${consultant_upper}_TIMEOUT_SECONDS"
|
|
171
|
+
|
|
172
|
+
local model="${!model_var:-}"
|
|
173
|
+
local timeout_seconds="${!timeout_var:-180}"
|
|
174
|
+
|
|
175
|
+
# Build full query from query + context
|
|
176
|
+
local full_query
|
|
177
|
+
full_query=$(build_full_query "$query" "$context_file")
|
|
178
|
+
|
|
179
|
+
if [[ -z "$full_query" ]]; then
|
|
180
|
+
log_error "[$consultant_name] No query to send"
|
|
181
|
+
return 1
|
|
182
|
+
fi
|
|
183
|
+
|
|
184
|
+
local start_time
|
|
185
|
+
start_time=$(get_timestamp_ms)
|
|
186
|
+
|
|
187
|
+
# Create temp file for API response
|
|
188
|
+
local temp_response
|
|
189
|
+
temp_response=$(mktemp)
|
|
190
|
+
|
|
191
|
+
# Use run_api_mode_query which handles all the API format/auth logic
|
|
192
|
+
run_api_mode_query "$consultant_name" "$model" "$full_query" "$temp_response" "$timeout_seconds"
|
|
193
|
+
local exit_code=$?
|
|
194
|
+
|
|
195
|
+
local end_time
|
|
196
|
+
end_time=$(get_timestamp_ms)
|
|
197
|
+
local latency=$((end_time - start_time))
|
|
198
|
+
|
|
199
|
+
if [[ $exit_code -ne 0 ]]; then
|
|
200
|
+
build_error_response "$consultant_name" "${model:-unknown}" "API Consultant" \
|
|
201
|
+
"API query failed (exit code: $exit_code)" "$latency" > "$output_file"
|
|
202
|
+
rm -f "$temp_response"
|
|
203
|
+
return 1
|
|
204
|
+
fi
|
|
205
|
+
|
|
206
|
+
# Read parsed content from temp file (run_api_mode_query already parsed it)
|
|
207
|
+
local parsed_content
|
|
208
|
+
parsed_content=$(cat "$temp_response")
|
|
209
|
+
rm -f "$temp_response"
|
|
210
|
+
|
|
211
|
+
if [[ -z "$parsed_content" ]]; then
|
|
212
|
+
build_error_response "$consultant_name" "${model:-unknown}" "API Consultant" \
|
|
213
|
+
"Empty response from API" "$latency" > "$output_file"
|
|
214
|
+
return 1
|
|
215
|
+
fi
|
|
216
|
+
|
|
217
|
+
# Try to parse as structured JSON response
|
|
218
|
+
if echo "$parsed_content" | jq -e '.response' >/dev/null 2>&1; then
|
|
219
|
+
build_structured_response "$consultant_name" "${model:-unknown}" "API Consultant" \
|
|
220
|
+
"$parsed_content" "$latency" > "$output_file"
|
|
221
|
+
else
|
|
222
|
+
build_fallback_response "$consultant_name" "${model:-unknown}" "API Consultant" \
|
|
223
|
+
"$parsed_content" "$latency" > "$output_file"
|
|
224
|
+
fi
|
|
225
|
+
|
|
226
|
+
log_success "[$consultant_name] Response generated"
|
|
227
|
+
return 0
|
|
228
|
+
}
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# cache.sh - Semantic caching for AI Consultants
|
|
3
|
+
#
|
|
4
|
+
# Caches consultation responses based on semantic fingerprints
|
|
5
|
+
# to avoid redundant API calls for similar queries.
|
|
6
|
+
|
|
7
|
+
set -euo pipefail
|
|
8
|
+
|
|
9
|
+
# =============================================================================
|
|
10
|
+
# CONFIGURATION
|
|
11
|
+
# =============================================================================
|
|
12
|
+
|
|
13
|
+
CACHE_DIR="${CACHE_DIR:-/tmp/ai_consultants_cache}"
|
|
14
|
+
CACHE_TTL_HOURS="${CACHE_TTL_HOURS:-24}"
|
|
15
|
+
|
|
16
|
+
# =============================================================================
|
|
17
|
+
# CACHE INITIALIZATION
|
|
18
|
+
# =============================================================================
|
|
19
|
+
|
|
20
|
+
# Initialize cache directory
|
|
21
|
+
# Usage: init_cache
|
|
22
|
+
init_cache() {
|
|
23
|
+
if [[ ! -d "$CACHE_DIR" ]]; then
|
|
24
|
+
mkdir -p "$CACHE_DIR"
|
|
25
|
+
chmod 700 "$CACHE_DIR"
|
|
26
|
+
fi
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
# =============================================================================
|
|
30
|
+
# FINGERPRINT GENERATION
|
|
31
|
+
# =============================================================================
|
|
32
|
+
|
|
33
|
+
# Normalize query for consistent hashing
|
|
34
|
+
# Usage: _normalize_query <query>
|
|
35
|
+
_normalize_query() {
|
|
36
|
+
local query="$1"
|
|
37
|
+
# Lowercase, remove extra whitespace, trim
|
|
38
|
+
echo "$query" | tr '[:upper:]' '[:lower:]' | sed 's/[[:space:]]\+/ /g' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//'
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
# Calculate checksum for a file
|
|
42
|
+
# Usage: _file_checksum <file_path>
|
|
43
|
+
_file_checksum() {
|
|
44
|
+
local file="$1"
|
|
45
|
+
if [[ -f "$file" ]]; then
|
|
46
|
+
if command -v md5sum &>/dev/null; then
|
|
47
|
+
md5sum "$file" 2>/dev/null | cut -d' ' -f1
|
|
48
|
+
elif command -v md5 &>/dev/null; then
|
|
49
|
+
md5 -q "$file" 2>/dev/null
|
|
50
|
+
else
|
|
51
|
+
# Fallback: use file size and mtime
|
|
52
|
+
stat -f "%z%m" "$file" 2>/dev/null || stat -c "%s%Y" "$file" 2>/dev/null || echo "0"
|
|
53
|
+
fi
|
|
54
|
+
else
|
|
55
|
+
echo ""
|
|
56
|
+
fi
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
# Generate semantic fingerprint for a query + context
|
|
60
|
+
# Usage: generate_fingerprint <query> <category> [context_file]
|
|
61
|
+
generate_fingerprint() {
|
|
62
|
+
local query="$1"
|
|
63
|
+
local category="${2:-GENERAL}"
|
|
64
|
+
local context_file="${3:-}"
|
|
65
|
+
|
|
66
|
+
# Normalize query
|
|
67
|
+
local normalized_query
|
|
68
|
+
normalized_query=$(_normalize_query "$query")
|
|
69
|
+
|
|
70
|
+
# Build fingerprint components
|
|
71
|
+
local fingerprint_input="${normalized_query}|${category}"
|
|
72
|
+
|
|
73
|
+
# Add context checksum if provided
|
|
74
|
+
if [[ -n "$context_file" && -f "$context_file" ]]; then
|
|
75
|
+
local context_checksum
|
|
76
|
+
context_checksum=$(_file_checksum "$context_file")
|
|
77
|
+
fingerprint_input+="|${context_checksum}"
|
|
78
|
+
fi
|
|
79
|
+
|
|
80
|
+
# Generate hash
|
|
81
|
+
if command -v sha256sum &>/dev/null; then
|
|
82
|
+
echo -n "$fingerprint_input" | sha256sum | cut -d' ' -f1
|
|
83
|
+
elif command -v shasum &>/dev/null; then
|
|
84
|
+
echo -n "$fingerprint_input" | shasum -a 256 | cut -d' ' -f1
|
|
85
|
+
else
|
|
86
|
+
# Fallback to md5
|
|
87
|
+
if command -v md5sum &>/dev/null; then
|
|
88
|
+
echo -n "$fingerprint_input" | md5sum | cut -d' ' -f1
|
|
89
|
+
elif command -v md5 &>/dev/null; then
|
|
90
|
+
echo -n "$fingerprint_input" | md5
|
|
91
|
+
else
|
|
92
|
+
# Last resort: simple hash
|
|
93
|
+
echo -n "$fingerprint_input" | cksum | cut -d' ' -f1
|
|
94
|
+
fi
|
|
95
|
+
fi
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
# =============================================================================
|
|
99
|
+
# CACHE OPERATIONS
|
|
100
|
+
# =============================================================================
|
|
101
|
+
|
|
102
|
+
# Check if semantic caching is enabled
|
|
103
|
+
# Usage: is_cache_enabled
|
|
104
|
+
is_cache_enabled() {
|
|
105
|
+
[[ "${ENABLE_SEMANTIC_CACHE:-true}" == "true" ]]
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
# Get cache file path for a fingerprint
|
|
109
|
+
# Usage: _get_cache_path <fingerprint> <consultant>
|
|
110
|
+
_get_cache_path() {
|
|
111
|
+
local fingerprint="$1"
|
|
112
|
+
local consultant="${2:-all}"
|
|
113
|
+
echo "${CACHE_DIR}/${consultant}_${fingerprint}.json"
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
# Get cache age in hours
|
|
117
|
+
# Usage: _get_cache_age_hours <cache_file>
|
|
118
|
+
_get_cache_age_hours() {
|
|
119
|
+
local cache_file="$1"
|
|
120
|
+
|
|
121
|
+
if [[ ! -f "$cache_file" ]]; then
|
|
122
|
+
echo "999999" # Very old
|
|
123
|
+
return
|
|
124
|
+
fi
|
|
125
|
+
|
|
126
|
+
local now
|
|
127
|
+
local file_time
|
|
128
|
+
local age_seconds
|
|
129
|
+
|
|
130
|
+
now=$(date +%s)
|
|
131
|
+
|
|
132
|
+
# Get file modification time
|
|
133
|
+
if stat -f %m "$cache_file" &>/dev/null; then
|
|
134
|
+
# macOS
|
|
135
|
+
file_time=$(stat -f %m "$cache_file")
|
|
136
|
+
elif stat -c %Y "$cache_file" &>/dev/null; then
|
|
137
|
+
# Linux
|
|
138
|
+
file_time=$(stat -c %Y "$cache_file")
|
|
139
|
+
else
|
|
140
|
+
echo "999999"
|
|
141
|
+
return
|
|
142
|
+
fi
|
|
143
|
+
|
|
144
|
+
age_seconds=$((now - file_time))
|
|
145
|
+
echo $((age_seconds / 3600))
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
# Check cache for a response
|
|
149
|
+
# Usage: check_cache <query> <category> <consultant> [context_file]
|
|
150
|
+
# Returns: cached response JSON if found and valid, empty otherwise
|
|
151
|
+
check_cache() {
|
|
152
|
+
local query="$1"
|
|
153
|
+
local category="${2:-GENERAL}"
|
|
154
|
+
local consultant="${3:-all}"
|
|
155
|
+
local context_file="${4:-}"
|
|
156
|
+
|
|
157
|
+
# Check if caching is enabled
|
|
158
|
+
if ! is_cache_enabled; then
|
|
159
|
+
return 1
|
|
160
|
+
fi
|
|
161
|
+
|
|
162
|
+
init_cache
|
|
163
|
+
|
|
164
|
+
local fingerprint
|
|
165
|
+
fingerprint=$(generate_fingerprint "$query" "$category" "$context_file")
|
|
166
|
+
|
|
167
|
+
local cache_file
|
|
168
|
+
cache_file=$(_get_cache_path "$fingerprint" "$consultant")
|
|
169
|
+
|
|
170
|
+
# Check if cache file exists
|
|
171
|
+
if [[ ! -f "$cache_file" ]]; then
|
|
172
|
+
return 1
|
|
173
|
+
fi
|
|
174
|
+
|
|
175
|
+
# Check TTL
|
|
176
|
+
local age_hours
|
|
177
|
+
age_hours=$(_get_cache_age_hours "$cache_file")
|
|
178
|
+
local ttl="${CACHE_TTL_HOURS:-24}"
|
|
179
|
+
|
|
180
|
+
if [[ $age_hours -ge $ttl ]]; then
|
|
181
|
+
# Cache expired
|
|
182
|
+
rm -f "$cache_file" 2>/dev/null || true
|
|
183
|
+
return 1
|
|
184
|
+
fi
|
|
185
|
+
|
|
186
|
+
# Return cached content
|
|
187
|
+
cat "$cache_file"
|
|
188
|
+
return 0
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
# Store response in cache
|
|
192
|
+
# Usage: store_cache <query> <category> <consultant> <response_json> [context_file]
|
|
193
|
+
store_cache() {
|
|
194
|
+
local query="$1"
|
|
195
|
+
local category="${2:-GENERAL}"
|
|
196
|
+
local consultant="${3:-all}"
|
|
197
|
+
local response="$4"
|
|
198
|
+
local context_file="${5:-}"
|
|
199
|
+
|
|
200
|
+
# Check if caching is enabled
|
|
201
|
+
if ! is_cache_enabled; then
|
|
202
|
+
return 0
|
|
203
|
+
fi
|
|
204
|
+
|
|
205
|
+
init_cache
|
|
206
|
+
|
|
207
|
+
local fingerprint
|
|
208
|
+
fingerprint=$(generate_fingerprint "$query" "$category" "$context_file")
|
|
209
|
+
|
|
210
|
+
local cache_file
|
|
211
|
+
cache_file=$(_get_cache_path "$fingerprint" "$consultant")
|
|
212
|
+
|
|
213
|
+
# Store response with metadata
|
|
214
|
+
local timestamp
|
|
215
|
+
timestamp=$(date -Iseconds)
|
|
216
|
+
|
|
217
|
+
# Add cache metadata to response
|
|
218
|
+
local cached_response
|
|
219
|
+
cached_response=$(echo "$response" | jq --arg ts "$timestamp" --arg fp "$fingerprint" \
|
|
220
|
+
'. + {cache_metadata: {cached_at: $ts, fingerprint: $fp, from_cache: false}}' 2>/dev/null || echo "$response")
|
|
221
|
+
|
|
222
|
+
# Atomic write: write to temp file first, then move to final location
|
|
223
|
+
# This prevents race conditions with parallel consultants
|
|
224
|
+
local temp_file
|
|
225
|
+
temp_file=$(mktemp "${CACHE_DIR}/cache_tmp.XXXXXX")
|
|
226
|
+
echo "$cached_response" > "$temp_file"
|
|
227
|
+
chmod 600 "$temp_file"
|
|
228
|
+
mv "$temp_file" "$cache_file"
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
# Invalidate cache for a specific query
|
|
232
|
+
# Usage: invalidate_cache <query> <category> [consultant] [context_file]
|
|
233
|
+
invalidate_cache() {
|
|
234
|
+
local query="$1"
|
|
235
|
+
local category="${2:-GENERAL}"
|
|
236
|
+
local consultant="${3:-}"
|
|
237
|
+
local context_file="${4:-}"
|
|
238
|
+
|
|
239
|
+
init_cache
|
|
240
|
+
|
|
241
|
+
local fingerprint
|
|
242
|
+
fingerprint=$(generate_fingerprint "$query" "$category" "$context_file")
|
|
243
|
+
|
|
244
|
+
if [[ -n "$consultant" ]]; then
|
|
245
|
+
# Invalidate specific consultant cache
|
|
246
|
+
local cache_file
|
|
247
|
+
cache_file=$(_get_cache_path "$fingerprint" "$consultant")
|
|
248
|
+
rm -f "$cache_file" 2>/dev/null || true
|
|
249
|
+
else
|
|
250
|
+
# Invalidate all caches for this fingerprint
|
|
251
|
+
rm -f "${CACHE_DIR}"/*_"${fingerprint}".json 2>/dev/null || true
|
|
252
|
+
fi
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
# Clear all expired cache entries
|
|
256
|
+
# Usage: cleanup_cache
|
|
257
|
+
cleanup_cache() {
|
|
258
|
+
init_cache
|
|
259
|
+
|
|
260
|
+
local ttl="${CACHE_TTL_HOURS:-24}"
|
|
261
|
+
local count=0
|
|
262
|
+
|
|
263
|
+
for cache_file in "$CACHE_DIR"/*.json; do
|
|
264
|
+
if [[ -f "$cache_file" ]]; then
|
|
265
|
+
local age_hours
|
|
266
|
+
age_hours=$(_get_cache_age_hours "$cache_file")
|
|
267
|
+
|
|
268
|
+
if [[ $age_hours -ge $ttl ]]; then
|
|
269
|
+
rm -f "$cache_file" 2>/dev/null || true
|
|
270
|
+
((count++)) || true
|
|
271
|
+
fi
|
|
272
|
+
fi
|
|
273
|
+
done
|
|
274
|
+
|
|
275
|
+
echo "$count"
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
# Clear all cache entries
|
|
279
|
+
# Usage: clear_cache
|
|
280
|
+
clear_cache() {
|
|
281
|
+
init_cache
|
|
282
|
+
rm -f "$CACHE_DIR"/*.json 2>/dev/null || true
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
# =============================================================================
|
|
286
|
+
# CACHE STATISTICS
|
|
287
|
+
# =============================================================================
|
|
288
|
+
|
|
289
|
+
# Get cache statistics
|
|
290
|
+
# Usage: get_cache_stats
|
|
291
|
+
get_cache_stats() {
|
|
292
|
+
init_cache
|
|
293
|
+
|
|
294
|
+
local total_files=0
|
|
295
|
+
local total_size=0
|
|
296
|
+
local expired_files=0
|
|
297
|
+
local ttl="${CACHE_TTL_HOURS:-24}"
|
|
298
|
+
|
|
299
|
+
for cache_file in "$CACHE_DIR"/*.json; do
|
|
300
|
+
if [[ -f "$cache_file" ]]; then
|
|
301
|
+
((total_files++)) || true
|
|
302
|
+
|
|
303
|
+
local file_size
|
|
304
|
+
if stat -f %z "$cache_file" &>/dev/null; then
|
|
305
|
+
file_size=$(stat -f %z "$cache_file")
|
|
306
|
+
else
|
|
307
|
+
file_size=$(stat -c %s "$cache_file" 2>/dev/null || echo 0)
|
|
308
|
+
fi
|
|
309
|
+
total_size=$((total_size + file_size))
|
|
310
|
+
|
|
311
|
+
local age_hours
|
|
312
|
+
age_hours=$(_get_cache_age_hours "$cache_file")
|
|
313
|
+
if [[ $age_hours -ge $ttl ]]; then
|
|
314
|
+
((expired_files++)) || true
|
|
315
|
+
fi
|
|
316
|
+
fi
|
|
317
|
+
done
|
|
318
|
+
|
|
319
|
+
local size_kb=$((total_size / 1024))
|
|
320
|
+
|
|
321
|
+
cat << EOF
|
|
322
|
+
{
|
|
323
|
+
"total_entries": $total_files,
|
|
324
|
+
"expired_entries": $expired_files,
|
|
325
|
+
"total_size_kb": $size_kb,
|
|
326
|
+
"cache_dir": "$CACHE_DIR",
|
|
327
|
+
"ttl_hours": $ttl,
|
|
328
|
+
"enabled": $(is_cache_enabled && echo "true" || echo "false")
|
|
329
|
+
}
|
|
330
|
+
EOF
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
# Mark response as from cache (for logging/metrics)
|
|
334
|
+
# Usage: echo "$json" | mark_from_cache OR mark_from_cache "$json"
|
|
335
|
+
mark_from_cache() {
|
|
336
|
+
local response="${1:-$(cat)}"
|
|
337
|
+
echo "$response" | jq '.cache_metadata.from_cache = true' 2>/dev/null || echo "$response"
|
|
338
|
+
}
|