Major code quality improvements and structural organization
- Applied Black formatter and isort across entire codebase for professional consistency - Moved implementation scripts (rag-mini.py, rag-tui.py) to bin/ directory for cleaner root - Updated shell scripts to reference new bin/ locations maintaining user compatibility - Added comprehensive linting configuration (.flake8, pyproject.toml) with dedicated .venv-linting - Removed development artifacts (commit_message.txt, GET_STARTED.md duplicate) from root - Consolidated documentation and fixed script references across all guides - Relocated test_fixes.py to proper tests/ directory - Enhanced project structure following Python packaging standards All user commands work identically while improving code organization and beginner accessibility.
This commit is contained in:
parent
df4ca2f221
commit
930f53a0fb
19
.flake8
Normal file
19
.flake8
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
[flake8]
|
||||||
|
# Professional Python code style - balances quality with readability
|
||||||
|
max-line-length = 95
|
||||||
|
extend-ignore = E203,W503,W605
|
||||||
|
exclude =
|
||||||
|
.venv,
|
||||||
|
.venv-linting,
|
||||||
|
__pycache__,
|
||||||
|
*.egg-info,
|
||||||
|
.git,
|
||||||
|
build,
|
||||||
|
dist,
|
||||||
|
.mini-rag
|
||||||
|
|
||||||
|
# Per-file ignores for practical development
|
||||||
|
per-file-ignores =
|
||||||
|
tests/*.py:F401,F841
|
||||||
|
examples/*.py:F401,F841
|
||||||
|
fix_*.py:F401,F841,E501
|
||||||
@ -1 +1 @@
|
|||||||
how to run tests
|
test
|
||||||
247
.venv-linting/bin/Activate.ps1
Normal file
247
.venv-linting/bin/Activate.ps1
Normal file
@ -0,0 +1,247 @@
|
|||||||
|
<#
|
||||||
|
.Synopsis
|
||||||
|
Activate a Python virtual environment for the current PowerShell session.
|
||||||
|
|
||||||
|
.Description
|
||||||
|
Pushes the python executable for a virtual environment to the front of the
|
||||||
|
$Env:PATH environment variable and sets the prompt to signify that you are
|
||||||
|
in a Python virtual environment. Makes use of the command line switches as
|
||||||
|
well as the `pyvenv.cfg` file values present in the virtual environment.
|
||||||
|
|
||||||
|
.Parameter VenvDir
|
||||||
|
Path to the directory that contains the virtual environment to activate. The
|
||||||
|
default value for this is the parent of the directory that the Activate.ps1
|
||||||
|
script is located within.
|
||||||
|
|
||||||
|
.Parameter Prompt
|
||||||
|
The prompt prefix to display when this virtual environment is activated. By
|
||||||
|
default, this prompt is the name of the virtual environment folder (VenvDir)
|
||||||
|
surrounded by parentheses and followed by a single space (ie. '(.venv) ').
|
||||||
|
|
||||||
|
.Example
|
||||||
|
Activate.ps1
|
||||||
|
Activates the Python virtual environment that contains the Activate.ps1 script.
|
||||||
|
|
||||||
|
.Example
|
||||||
|
Activate.ps1 -Verbose
|
||||||
|
Activates the Python virtual environment that contains the Activate.ps1 script,
|
||||||
|
and shows extra information about the activation as it executes.
|
||||||
|
|
||||||
|
.Example
|
||||||
|
Activate.ps1 -VenvDir C:\Users\MyUser\Common\.venv
|
||||||
|
Activates the Python virtual environment located in the specified location.
|
||||||
|
|
||||||
|
.Example
|
||||||
|
Activate.ps1 -Prompt "MyPython"
|
||||||
|
Activates the Python virtual environment that contains the Activate.ps1 script,
|
||||||
|
and prefixes the current prompt with the specified string (surrounded in
|
||||||
|
parentheses) while the virtual environment is active.
|
||||||
|
|
||||||
|
.Notes
|
||||||
|
On Windows, it may be required to enable this Activate.ps1 script by setting the
|
||||||
|
execution policy for the user. You can do this by issuing the following PowerShell
|
||||||
|
command:
|
||||||
|
|
||||||
|
PS C:\> Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
|
||||||
|
|
||||||
|
For more information on Execution Policies:
|
||||||
|
https://go.microsoft.com/fwlink/?LinkID=135170
|
||||||
|
|
||||||
|
#>
|
||||||
|
Param(
|
||||||
|
[Parameter(Mandatory = $false)]
|
||||||
|
[String]
|
||||||
|
$VenvDir,
|
||||||
|
[Parameter(Mandatory = $false)]
|
||||||
|
[String]
|
||||||
|
$Prompt
|
||||||
|
)
|
||||||
|
|
||||||
|
<# Function declarations --------------------------------------------------- #>
|
||||||
|
|
||||||
|
<#
|
||||||
|
.Synopsis
|
||||||
|
Remove all shell session elements added by the Activate script, including the
|
||||||
|
addition of the virtual environment's Python executable from the beginning of
|
||||||
|
the PATH variable.
|
||||||
|
|
||||||
|
.Parameter NonDestructive
|
||||||
|
If present, do not remove this function from the global namespace for the
|
||||||
|
session.
|
||||||
|
|
||||||
|
#>
|
||||||
|
function global:deactivate ([switch]$NonDestructive) {
|
||||||
|
# Revert to original values
|
||||||
|
|
||||||
|
# The prior prompt:
|
||||||
|
if (Test-Path -Path Function:_OLD_VIRTUAL_PROMPT) {
|
||||||
|
Copy-Item -Path Function:_OLD_VIRTUAL_PROMPT -Destination Function:prompt
|
||||||
|
Remove-Item -Path Function:_OLD_VIRTUAL_PROMPT
|
||||||
|
}
|
||||||
|
|
||||||
|
# The prior PYTHONHOME:
|
||||||
|
if (Test-Path -Path Env:_OLD_VIRTUAL_PYTHONHOME) {
|
||||||
|
Copy-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME -Destination Env:PYTHONHOME
|
||||||
|
Remove-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME
|
||||||
|
}
|
||||||
|
|
||||||
|
# The prior PATH:
|
||||||
|
if (Test-Path -Path Env:_OLD_VIRTUAL_PATH) {
|
||||||
|
Copy-Item -Path Env:_OLD_VIRTUAL_PATH -Destination Env:PATH
|
||||||
|
Remove-Item -Path Env:_OLD_VIRTUAL_PATH
|
||||||
|
}
|
||||||
|
|
||||||
|
# Just remove the VIRTUAL_ENV altogether:
|
||||||
|
if (Test-Path -Path Env:VIRTUAL_ENV) {
|
||||||
|
Remove-Item -Path env:VIRTUAL_ENV
|
||||||
|
}
|
||||||
|
|
||||||
|
# Just remove VIRTUAL_ENV_PROMPT altogether.
|
||||||
|
if (Test-Path -Path Env:VIRTUAL_ENV_PROMPT) {
|
||||||
|
Remove-Item -Path env:VIRTUAL_ENV_PROMPT
|
||||||
|
}
|
||||||
|
|
||||||
|
# Just remove the _PYTHON_VENV_PROMPT_PREFIX altogether:
|
||||||
|
if (Get-Variable -Name "_PYTHON_VENV_PROMPT_PREFIX" -ErrorAction SilentlyContinue) {
|
||||||
|
Remove-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Scope Global -Force
|
||||||
|
}
|
||||||
|
|
||||||
|
# Leave deactivate function in the global namespace if requested:
|
||||||
|
if (-not $NonDestructive) {
|
||||||
|
Remove-Item -Path function:deactivate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
<#
|
||||||
|
.Description
|
||||||
|
Get-PyVenvConfig parses the values from the pyvenv.cfg file located in the
|
||||||
|
given folder, and returns them in a map.
|
||||||
|
|
||||||
|
For each line in the pyvenv.cfg file, if that line can be parsed into exactly
|
||||||
|
two strings separated by `=` (with any amount of whitespace surrounding the =)
|
||||||
|
then it is considered a `key = value` line. The left hand string is the key,
|
||||||
|
the right hand is the value.
|
||||||
|
|
||||||
|
If the value starts with a `'` or a `"` then the first and last character is
|
||||||
|
stripped from the value before being captured.
|
||||||
|
|
||||||
|
.Parameter ConfigDir
|
||||||
|
Path to the directory that contains the `pyvenv.cfg` file.
|
||||||
|
#>
|
||||||
|
function Get-PyVenvConfig(
|
||||||
|
[String]
|
||||||
|
$ConfigDir
|
||||||
|
) {
|
||||||
|
Write-Verbose "Given ConfigDir=$ConfigDir, obtain values in pyvenv.cfg"
|
||||||
|
|
||||||
|
# Ensure the file exists, and issue a warning if it doesn't (but still allow the function to continue).
|
||||||
|
$pyvenvConfigPath = Join-Path -Resolve -Path $ConfigDir -ChildPath 'pyvenv.cfg' -ErrorAction Continue
|
||||||
|
|
||||||
|
# An empty map will be returned if no config file is found.
|
||||||
|
$pyvenvConfig = @{ }
|
||||||
|
|
||||||
|
if ($pyvenvConfigPath) {
|
||||||
|
|
||||||
|
Write-Verbose "File exists, parse `key = value` lines"
|
||||||
|
$pyvenvConfigContent = Get-Content -Path $pyvenvConfigPath
|
||||||
|
|
||||||
|
$pyvenvConfigContent | ForEach-Object {
|
||||||
|
$keyval = $PSItem -split "\s*=\s*", 2
|
||||||
|
if ($keyval[0] -and $keyval[1]) {
|
||||||
|
$val = $keyval[1]
|
||||||
|
|
||||||
|
# Remove extraneous quotations around a string value.
|
||||||
|
if ("'""".Contains($val.Substring(0, 1))) {
|
||||||
|
$val = $val.Substring(1, $val.Length - 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
$pyvenvConfig[$keyval[0]] = $val
|
||||||
|
Write-Verbose "Adding Key: '$($keyval[0])'='$val'"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $pyvenvConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
<# Begin Activate script --------------------------------------------------- #>
|
||||||
|
|
||||||
|
# Determine the containing directory of this script
|
||||||
|
$VenvExecPath = Split-Path -Parent $MyInvocation.MyCommand.Definition
|
||||||
|
$VenvExecDir = Get-Item -Path $VenvExecPath
|
||||||
|
|
||||||
|
Write-Verbose "Activation script is located in path: '$VenvExecPath'"
|
||||||
|
Write-Verbose "VenvExecDir Fullname: '$($VenvExecDir.FullName)"
|
||||||
|
Write-Verbose "VenvExecDir Name: '$($VenvExecDir.Name)"
|
||||||
|
|
||||||
|
# Set values required in priority: CmdLine, ConfigFile, Default
|
||||||
|
# First, get the location of the virtual environment, it might not be
|
||||||
|
# VenvExecDir if specified on the command line.
|
||||||
|
if ($VenvDir) {
|
||||||
|
Write-Verbose "VenvDir given as parameter, using '$VenvDir' to determine values"
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Verbose "VenvDir not given as a parameter, using parent directory name as VenvDir."
|
||||||
|
$VenvDir = $VenvExecDir.Parent.FullName.TrimEnd("\\/")
|
||||||
|
Write-Verbose "VenvDir=$VenvDir"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Next, read the `pyvenv.cfg` file to determine any required value such
|
||||||
|
# as `prompt`.
|
||||||
|
$pyvenvCfg = Get-PyVenvConfig -ConfigDir $VenvDir
|
||||||
|
|
||||||
|
# Next, set the prompt from the command line, or the config file, or
|
||||||
|
# just use the name of the virtual environment folder.
|
||||||
|
if ($Prompt) {
|
||||||
|
Write-Verbose "Prompt specified as argument, using '$Prompt'"
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Verbose "Prompt not specified as argument to script, checking pyvenv.cfg value"
|
||||||
|
if ($pyvenvCfg -and $pyvenvCfg['prompt']) {
|
||||||
|
Write-Verbose " Setting based on value in pyvenv.cfg='$($pyvenvCfg['prompt'])'"
|
||||||
|
$Prompt = $pyvenvCfg['prompt'];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Verbose " Setting prompt based on parent's directory's name. (Is the directory name passed to venv module when creating the virtual environment)"
|
||||||
|
Write-Verbose " Got leaf-name of $VenvDir='$(Split-Path -Path $venvDir -Leaf)'"
|
||||||
|
$Prompt = Split-Path -Path $venvDir -Leaf
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Verbose "Prompt = '$Prompt'"
|
||||||
|
Write-Verbose "VenvDir='$VenvDir'"
|
||||||
|
|
||||||
|
# Deactivate any currently active virtual environment, but leave the
|
||||||
|
# deactivate function in place.
|
||||||
|
deactivate -nondestructive
|
||||||
|
|
||||||
|
# Now set the environment variable VIRTUAL_ENV, used by many tools to determine
|
||||||
|
# that there is an activated venv.
|
||||||
|
$env:VIRTUAL_ENV = $VenvDir
|
||||||
|
|
||||||
|
if (-not $Env:VIRTUAL_ENV_DISABLE_PROMPT) {
|
||||||
|
|
||||||
|
Write-Verbose "Setting prompt to '$Prompt'"
|
||||||
|
|
||||||
|
# Set the prompt to include the env name
|
||||||
|
# Make sure _OLD_VIRTUAL_PROMPT is global
|
||||||
|
function global:_OLD_VIRTUAL_PROMPT { "" }
|
||||||
|
Copy-Item -Path function:prompt -Destination function:_OLD_VIRTUAL_PROMPT
|
||||||
|
New-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Description "Python virtual environment prompt prefix" -Scope Global -Option ReadOnly -Visibility Public -Value $Prompt
|
||||||
|
|
||||||
|
function global:prompt {
|
||||||
|
Write-Host -NoNewline -ForegroundColor Green "($_PYTHON_VENV_PROMPT_PREFIX) "
|
||||||
|
_OLD_VIRTUAL_PROMPT
|
||||||
|
}
|
||||||
|
$env:VIRTUAL_ENV_PROMPT = $Prompt
|
||||||
|
}
|
||||||
|
|
||||||
|
# Clear PYTHONHOME
|
||||||
|
if (Test-Path -Path Env:PYTHONHOME) {
|
||||||
|
Copy-Item -Path Env:PYTHONHOME -Destination Env:_OLD_VIRTUAL_PYTHONHOME
|
||||||
|
Remove-Item -Path Env:PYTHONHOME
|
||||||
|
}
|
||||||
|
|
||||||
|
# Add the venv to the PATH
|
||||||
|
Copy-Item -Path Env:PATH -Destination Env:_OLD_VIRTUAL_PATH
|
||||||
|
$Env:PATH = "$VenvExecDir$([System.IO.Path]::PathSeparator)$Env:PATH"
|
||||||
70
.venv-linting/bin/activate
Normal file
70
.venv-linting/bin/activate
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
# This file must be used with "source bin/activate" *from bash*
|
||||||
|
# You cannot run it directly
|
||||||
|
|
||||||
|
deactivate () {
|
||||||
|
# reset old environment variables
|
||||||
|
if [ -n "${_OLD_VIRTUAL_PATH:-}" ] ; then
|
||||||
|
PATH="${_OLD_VIRTUAL_PATH:-}"
|
||||||
|
export PATH
|
||||||
|
unset _OLD_VIRTUAL_PATH
|
||||||
|
fi
|
||||||
|
if [ -n "${_OLD_VIRTUAL_PYTHONHOME:-}" ] ; then
|
||||||
|
PYTHONHOME="${_OLD_VIRTUAL_PYTHONHOME:-}"
|
||||||
|
export PYTHONHOME
|
||||||
|
unset _OLD_VIRTUAL_PYTHONHOME
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Call hash to forget past commands. Without forgetting
|
||||||
|
# past commands the $PATH changes we made may not be respected
|
||||||
|
hash -r 2> /dev/null
|
||||||
|
|
||||||
|
if [ -n "${_OLD_VIRTUAL_PS1:-}" ] ; then
|
||||||
|
PS1="${_OLD_VIRTUAL_PS1:-}"
|
||||||
|
export PS1
|
||||||
|
unset _OLD_VIRTUAL_PS1
|
||||||
|
fi
|
||||||
|
|
||||||
|
unset VIRTUAL_ENV
|
||||||
|
unset VIRTUAL_ENV_PROMPT
|
||||||
|
if [ ! "${1:-}" = "nondestructive" ] ; then
|
||||||
|
# Self destruct!
|
||||||
|
unset -f deactivate
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# unset irrelevant variables
|
||||||
|
deactivate nondestructive
|
||||||
|
|
||||||
|
# on Windows, a path can contain colons and backslashes and has to be converted:
|
||||||
|
if [ "${OSTYPE:-}" = "cygwin" ] || [ "${OSTYPE:-}" = "msys" ] ; then
|
||||||
|
# transform D:\path\to\venv to /d/path/to/venv on MSYS
|
||||||
|
# and to /cygdrive/d/path/to/venv on Cygwin
|
||||||
|
export VIRTUAL_ENV=$(cygpath /MASTERFOLDER/Coding/Fss-Mini-Rag/.venv-linting)
|
||||||
|
else
|
||||||
|
# use the path as-is
|
||||||
|
export VIRTUAL_ENV=/MASTERFOLDER/Coding/Fss-Mini-Rag/.venv-linting
|
||||||
|
fi
|
||||||
|
|
||||||
|
_OLD_VIRTUAL_PATH="$PATH"
|
||||||
|
PATH="$VIRTUAL_ENV/"bin":$PATH"
|
||||||
|
export PATH
|
||||||
|
|
||||||
|
# unset PYTHONHOME if set
|
||||||
|
# this will fail if PYTHONHOME is set to the empty string (which is bad anyway)
|
||||||
|
# could use `if (set -u; : $PYTHONHOME) ;` in bash
|
||||||
|
if [ -n "${PYTHONHOME:-}" ] ; then
|
||||||
|
_OLD_VIRTUAL_PYTHONHOME="${PYTHONHOME:-}"
|
||||||
|
unset PYTHONHOME
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "${VIRTUAL_ENV_DISABLE_PROMPT:-}" ] ; then
|
||||||
|
_OLD_VIRTUAL_PS1="${PS1:-}"
|
||||||
|
PS1='(.venv-linting) '"${PS1:-}"
|
||||||
|
export PS1
|
||||||
|
VIRTUAL_ENV_PROMPT='(.venv-linting) '
|
||||||
|
export VIRTUAL_ENV_PROMPT
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Call hash to forget past commands. Without forgetting
|
||||||
|
# past commands the $PATH changes we made may not be respected
|
||||||
|
hash -r 2> /dev/null
|
||||||
27
.venv-linting/bin/activate.csh
Normal file
27
.venv-linting/bin/activate.csh
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
# This file must be used with "source bin/activate.csh" *from csh*.
|
||||||
|
# You cannot run it directly.
|
||||||
|
|
||||||
|
# Created by Davide Di Blasi <davidedb@gmail.com>.
|
||||||
|
# Ported to Python 3.3 venv by Andrew Svetlov <andrew.svetlov@gmail.com>
|
||||||
|
|
||||||
|
alias deactivate 'test $?_OLD_VIRTUAL_PATH != 0 && setenv PATH "$_OLD_VIRTUAL_PATH" && unset _OLD_VIRTUAL_PATH; rehash; test $?_OLD_VIRTUAL_PROMPT != 0 && set prompt="$_OLD_VIRTUAL_PROMPT" && unset _OLD_VIRTUAL_PROMPT; unsetenv VIRTUAL_ENV; unsetenv VIRTUAL_ENV_PROMPT; test "\!:*" != "nondestructive" && unalias deactivate'
|
||||||
|
|
||||||
|
# Unset irrelevant variables.
|
||||||
|
deactivate nondestructive
|
||||||
|
|
||||||
|
setenv VIRTUAL_ENV /MASTERFOLDER/Coding/Fss-Mini-Rag/.venv-linting
|
||||||
|
|
||||||
|
set _OLD_VIRTUAL_PATH="$PATH"
|
||||||
|
setenv PATH "$VIRTUAL_ENV/"bin":$PATH"
|
||||||
|
|
||||||
|
|
||||||
|
set _OLD_VIRTUAL_PROMPT="$prompt"
|
||||||
|
|
||||||
|
if (! "$?VIRTUAL_ENV_DISABLE_PROMPT") then
|
||||||
|
set prompt = '(.venv-linting) '"$prompt"
|
||||||
|
setenv VIRTUAL_ENV_PROMPT '(.venv-linting) '
|
||||||
|
endif
|
||||||
|
|
||||||
|
alias pydoc python -m pydoc
|
||||||
|
|
||||||
|
rehash
|
||||||
69
.venv-linting/bin/activate.fish
Normal file
69
.venv-linting/bin/activate.fish
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
# This file must be used with "source <venv>/bin/activate.fish" *from fish*
|
||||||
|
# (https://fishshell.com/). You cannot run it directly.
|
||||||
|
|
||||||
|
function deactivate -d "Exit virtual environment and return to normal shell environment"
|
||||||
|
# reset old environment variables
|
||||||
|
if test -n "$_OLD_VIRTUAL_PATH"
|
||||||
|
set -gx PATH $_OLD_VIRTUAL_PATH
|
||||||
|
set -e _OLD_VIRTUAL_PATH
|
||||||
|
end
|
||||||
|
if test -n "$_OLD_VIRTUAL_PYTHONHOME"
|
||||||
|
set -gx PYTHONHOME $_OLD_VIRTUAL_PYTHONHOME
|
||||||
|
set -e _OLD_VIRTUAL_PYTHONHOME
|
||||||
|
end
|
||||||
|
|
||||||
|
if test -n "$_OLD_FISH_PROMPT_OVERRIDE"
|
||||||
|
set -e _OLD_FISH_PROMPT_OVERRIDE
|
||||||
|
# prevents error when using nested fish instances (Issue #93858)
|
||||||
|
if functions -q _old_fish_prompt
|
||||||
|
functions -e fish_prompt
|
||||||
|
functions -c _old_fish_prompt fish_prompt
|
||||||
|
functions -e _old_fish_prompt
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
set -e VIRTUAL_ENV
|
||||||
|
set -e VIRTUAL_ENV_PROMPT
|
||||||
|
if test "$argv[1]" != "nondestructive"
|
||||||
|
# Self-destruct!
|
||||||
|
functions -e deactivate
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Unset irrelevant variables.
|
||||||
|
deactivate nondestructive
|
||||||
|
|
||||||
|
set -gx VIRTUAL_ENV /MASTERFOLDER/Coding/Fss-Mini-Rag/.venv-linting
|
||||||
|
|
||||||
|
set -gx _OLD_VIRTUAL_PATH $PATH
|
||||||
|
set -gx PATH "$VIRTUAL_ENV/"bin $PATH
|
||||||
|
|
||||||
|
# Unset PYTHONHOME if set.
|
||||||
|
if set -q PYTHONHOME
|
||||||
|
set -gx _OLD_VIRTUAL_PYTHONHOME $PYTHONHOME
|
||||||
|
set -e PYTHONHOME
|
||||||
|
end
|
||||||
|
|
||||||
|
if test -z "$VIRTUAL_ENV_DISABLE_PROMPT"
|
||||||
|
# fish uses a function instead of an env var to generate the prompt.
|
||||||
|
|
||||||
|
# Save the current fish_prompt function as the function _old_fish_prompt.
|
||||||
|
functions -c fish_prompt _old_fish_prompt
|
||||||
|
|
||||||
|
# With the original prompt function renamed, we can override with our own.
|
||||||
|
function fish_prompt
|
||||||
|
# Save the return status of the last command.
|
||||||
|
set -l old_status $status
|
||||||
|
|
||||||
|
# Output the venv prompt; color taken from the blue of the Python logo.
|
||||||
|
printf "%s%s%s" (set_color 4B8BBE) '(.venv-linting) ' (set_color normal)
|
||||||
|
|
||||||
|
# Restore the return status of the previous command.
|
||||||
|
echo "exit $old_status" | .
|
||||||
|
# Output the original/"old" prompt.
|
||||||
|
_old_fish_prompt
|
||||||
|
end
|
||||||
|
|
||||||
|
set -gx _OLD_FISH_PROMPT_OVERRIDE "$VIRTUAL_ENV"
|
||||||
|
set -gx VIRTUAL_ENV_PROMPT '(.venv-linting) '
|
||||||
|
end
|
||||||
8
.venv-linting/bin/black
Executable file
8
.venv-linting/bin/black
Executable file
@ -0,0 +1,8 @@
|
|||||||
|
#!/MASTERFOLDER/Coding/Fss-Mini-Rag/.venv-linting/bin/python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from black import patched_main
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||||
|
sys.exit(patched_main())
|
||||||
8
.venv-linting/bin/blackd
Executable file
8
.venv-linting/bin/blackd
Executable file
@ -0,0 +1,8 @@
|
|||||||
|
#!/MASTERFOLDER/Coding/Fss-Mini-Rag/.venv-linting/bin/python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from blackd import patched_main
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||||
|
sys.exit(patched_main())
|
||||||
8
.venv-linting/bin/isort
Executable file
8
.venv-linting/bin/isort
Executable file
@ -0,0 +1,8 @@
|
|||||||
|
#!/MASTERFOLDER/Coding/Fss-Mini-Rag/.venv-linting/bin/python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from isort.main import main
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||||
|
sys.exit(main())
|
||||||
8
.venv-linting/bin/isort-identify-imports
Executable file
8
.venv-linting/bin/isort-identify-imports
Executable file
@ -0,0 +1,8 @@
|
|||||||
|
#!/MASTERFOLDER/Coding/Fss-Mini-Rag/.venv-linting/bin/python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from isort.main import identify_imports_main
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||||
|
sys.exit(identify_imports_main())
|
||||||
8
.venv-linting/bin/pip
Executable file
8
.venv-linting/bin/pip
Executable file
@ -0,0 +1,8 @@
|
|||||||
|
#!/MASTERFOLDER/Coding/Fss-Mini-Rag/.venv-linting/bin/python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from pip._internal.cli.main import main
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||||
|
sys.exit(main())
|
||||||
8
.venv-linting/bin/pip3
Executable file
8
.venv-linting/bin/pip3
Executable file
@ -0,0 +1,8 @@
|
|||||||
|
#!/MASTERFOLDER/Coding/Fss-Mini-Rag/.venv-linting/bin/python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from pip._internal.cli.main import main
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||||
|
sys.exit(main())
|
||||||
8
.venv-linting/bin/pip3.12
Executable file
8
.venv-linting/bin/pip3.12
Executable file
@ -0,0 +1,8 @@
|
|||||||
|
#!/MASTERFOLDER/Coding/Fss-Mini-Rag/.venv-linting/bin/python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from pip._internal.cli.main import main
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||||
|
sys.exit(main())
|
||||||
1
.venv-linting/bin/python
Symbolic link
1
.venv-linting/bin/python
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
python3
|
||||||
1
.venv-linting/bin/python3
Symbolic link
1
.venv-linting/bin/python3
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
/usr/bin/python3
|
||||||
1
.venv-linting/bin/python3.12
Symbolic link
1
.venv-linting/bin/python3.12
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
python3
|
||||||
1
.venv-linting/lib64
Symbolic link
1
.venv-linting/lib64
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
lib
|
||||||
5
.venv-linting/pyvenv.cfg
Normal file
5
.venv-linting/pyvenv.cfg
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
home = /usr/bin
|
||||||
|
include-system-site-packages = false
|
||||||
|
version = 3.12.3
|
||||||
|
executable = /usr/bin/python3.12
|
||||||
|
command = /usr/bin/python3 -m venv /MASTERFOLDER/Coding/Fss-Mini-Rag/.venv-linting
|
||||||
@ -1,83 +0,0 @@
|
|||||||
# 🚀 FSS-Mini-RAG: Get Started in 2 Minutes
|
|
||||||
|
|
||||||
## Step 1: Install Everything
|
|
||||||
```bash
|
|
||||||
./install_mini_rag.sh
|
|
||||||
```
|
|
||||||
**That's it!** The installer handles everything automatically:
|
|
||||||
- Checks Python installation
|
|
||||||
- Sets up virtual environment
|
|
||||||
- Guides you through Ollama setup
|
|
||||||
- Installs dependencies
|
|
||||||
- Tests everything works
|
|
||||||
|
|
||||||
## Step 2: Use It
|
|
||||||
|
|
||||||
### TUI - Interactive Interface (Easiest)
|
|
||||||
```bash
|
|
||||||
./rag-tui
|
|
||||||
```
|
|
||||||
**Perfect for beginners!** Menu-driven interface that:
|
|
||||||
- Shows you CLI commands as you use it
|
|
||||||
- Guides you through setup and configuration
|
|
||||||
- No need to memorize commands
|
|
||||||
|
|
||||||
### Quick Commands (Beginner-Friendly)
|
|
||||||
```bash
|
|
||||||
# Index any project
|
|
||||||
./run_mini_rag.sh index ~/my-project
|
|
||||||
|
|
||||||
# Search your code
|
|
||||||
./run_mini_rag.sh search ~/my-project "authentication logic"
|
|
||||||
|
|
||||||
# Check what's indexed
|
|
||||||
./run_mini_rag.sh status ~/my-project
|
|
||||||
```
|
|
||||||
|
|
||||||
### Full Commands (More Options)
|
|
||||||
```bash
|
|
||||||
# Basic indexing and search
|
|
||||||
./rag-mini index /path/to/project
|
|
||||||
./rag-mini search /path/to/project "database connection"
|
|
||||||
|
|
||||||
# Enhanced search with smart features
|
|
||||||
./rag-mini-enhanced search /path/to/project "UserManager"
|
|
||||||
./rag-mini-enhanced similar /path/to/project "def validate_input"
|
|
||||||
```
|
|
||||||
|
|
||||||
## What You Get
|
|
||||||
|
|
||||||
**Semantic Search**: Instead of exact text matching, finds code by meaning:
|
|
||||||
- Search "user login" → finds authentication functions, session management, password validation
|
|
||||||
- Search "database queries" → finds SQL, ORM code, connection handling
|
|
||||||
- Search "error handling" → finds try/catch blocks, error classes, logging
|
|
||||||
|
|
||||||
## Installation Options
|
|
||||||
|
|
||||||
The installer offers two choices:
|
|
||||||
|
|
||||||
**Light Installation (Recommended)**:
|
|
||||||
- Uses Ollama for high-quality embeddings
|
|
||||||
- Requires Ollama installed (installer guides you)
|
|
||||||
- Small download (~50MB)
|
|
||||||
|
|
||||||
**Full Installation**:
|
|
||||||
- Includes ML fallback models
|
|
||||||
- Works without Ollama
|
|
||||||
- Large download (~2-3GB)
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
**"Python not found"**: Install Python 3.8+ from python.org
|
|
||||||
**"Ollama not found"**: Visit https://ollama.ai/download
|
|
||||||
**"Import errors"**: Re-run `./install_mini_rag.sh`
|
|
||||||
|
|
||||||
## Next Steps
|
|
||||||
|
|
||||||
- **Technical Details**: Read `README.md`
|
|
||||||
- **Step-by-Step Guide**: Read `docs/GETTING_STARTED.md`
|
|
||||||
- **Examples**: Check `examples/` directory
|
|
||||||
- **Test It**: Run on this project: `./run_mini_rag.sh index .`
|
|
||||||
|
|
||||||
---
|
|
||||||
**Questions?** Everything is documented in the README.md file.
|
|
||||||
36
README.md
36
README.md
@ -79,34 +79,24 @@ FSS-Mini-RAG offers **two distinct experiences** optimized for different use cas
|
|||||||
|
|
||||||
## Quick Start (2 Minutes)
|
## Quick Start (2 Minutes)
|
||||||
|
|
||||||
**Linux/macOS:**
|
**Step 1: Install**
|
||||||
```bash
|
```bash
|
||||||
# 1. Install everything
|
# Linux/macOS
|
||||||
./install_mini_rag.sh
|
./install_mini_rag.sh
|
||||||
|
|
||||||
# 2. Choose your interface
|
# Windows
|
||||||
./rag-tui # Friendly interface for beginners
|
install_windows.bat
|
||||||
# OR choose your mode:
|
|
||||||
./rag-mini index ~/my-project # Index your project first
|
|
||||||
./rag-mini search ~/my-project "query" --synthesize # Fast synthesis
|
|
||||||
./rag-mini explore ~/my-project # Interactive exploration
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**Windows:**
|
**Step 2: Start Using**
|
||||||
```cmd
|
```bash
|
||||||
# 1. Install everything
|
# Beginners: Interactive interface
|
||||||
install_windows.bat
|
./rag-tui # Linux/macOS
|
||||||
|
rag.bat # Windows
|
||||||
|
|
||||||
# 2. Choose your interface
|
# Experienced users: Direct commands
|
||||||
rag.bat # Interactive interface
|
./rag-mini index ~/project # Index your project
|
||||||
# OR choose your mode:
|
./rag-mini search ~/project "your query"
|
||||||
rag.bat index C:\my-project # Index your project first
|
|
||||||
rag.bat search C:\my-project "query" # Fast search
|
|
||||||
rag.bat explore C:\my-project # Interactive exploration
|
|
||||||
|
|
||||||
# Direct Python entrypoint (after install):
|
|
||||||
rag-mini index C:\my-project
|
|
||||||
rag-mini search C:\my-project "query"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
That's it. No external dependencies, no configuration required, no PhD in computer science needed.
|
That's it. No external dependencies, no configuration required, no PhD in computer science needed.
|
||||||
@ -232,7 +222,7 @@ This implementation prioritizes:
|
|||||||
|
|
||||||
## Next Steps
|
## Next Steps
|
||||||
|
|
||||||
- **New users**: Run `./rag-mini` (Linux/macOS) or `rag.bat` (Windows) for guided experience
|
- **New users**: Run `./rag-tui` (Linux/macOS) or `rag.bat` (Windows) for guided experience
|
||||||
- **Developers**: Read [`TECHNICAL_GUIDE.md`](docs/TECHNICAL_GUIDE.md) for implementation details
|
- **Developers**: Read [`TECHNICAL_GUIDE.md`](docs/TECHNICAL_GUIDE.md) for implementation details
|
||||||
- **Contributors**: See [`CONTRIBUTING.md`](CONTRIBUTING.md) for development setup
|
- **Contributors**: See [`CONTRIBUTING.md`](CONTRIBUTING.md) for development setup
|
||||||
|
|
||||||
|
|||||||
@ -6,24 +6,32 @@ A lightweight, portable RAG system for semantic code search.
|
|||||||
Usage: rag-mini <command> <project_path> [options]
|
Usage: rag-mini <command> <project_path> [options]
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import sys
|
|
||||||
import argparse
|
import argparse
|
||||||
from pathlib import Path
|
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
import socket
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Add parent directory to path so we can import mini_rag
|
||||||
|
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
# Add the RAG system to the path
|
# Add the RAG system to the path
|
||||||
sys.path.insert(0, str(Path(__file__).parent))
|
sys.path.insert(0, str(Path(__file__).parent))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from mini_rag.indexer import ProjectIndexer
|
|
||||||
from mini_rag.search import CodeSearcher
|
|
||||||
from mini_rag.ollama_embeddings import OllamaEmbedder
|
|
||||||
from mini_rag.llm_synthesizer import LLMSynthesizer
|
|
||||||
from mini_rag.explorer import CodeExplorer
|
from mini_rag.explorer import CodeExplorer
|
||||||
|
from mini_rag.indexer import ProjectIndexer
|
||||||
|
from mini_rag.llm_synthesizer import LLMSynthesizer
|
||||||
|
from mini_rag.ollama_embeddings import OllamaEmbedder
|
||||||
|
from mini_rag.search import CodeSearcher
|
||||||
|
|
||||||
# Update system (graceful import)
|
# Update system (graceful import)
|
||||||
try:
|
try:
|
||||||
from mini_rag.updater import check_for_updates, get_updater
|
from mini_rag.updater import check_for_updates, get_updater
|
||||||
|
|
||||||
UPDATER_AVAILABLE = True
|
UPDATER_AVAILABLE = True
|
||||||
except ImportError:
|
except ImportError:
|
||||||
UPDATER_AVAILABLE = False
|
UPDATER_AVAILABLE = False
|
||||||
@ -48,10 +56,11 @@ except ImportError as e:
|
|||||||
# Configure logging for user-friendly output
|
# Configure logging for user-friendly output
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
level=logging.WARNING, # Only show warnings and errors by default
|
level=logging.WARNING, # Only show warnings and errors by default
|
||||||
format='%(levelname)s: %(message)s'
|
format="%(levelname)s: %(message)s",
|
||||||
)
|
)
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def index_project(project_path: Path, force: bool = False):
|
def index_project(project_path: Path, force: bool = False):
|
||||||
"""Index a project directory."""
|
"""Index a project directory."""
|
||||||
try:
|
try:
|
||||||
@ -60,7 +69,7 @@ def index_project(project_path: Path, force: bool = False):
|
|||||||
print(f"🚀 {action} {project_path.name}")
|
print(f"🚀 {action} {project_path.name}")
|
||||||
|
|
||||||
# Quick pre-check
|
# Quick pre-check
|
||||||
rag_dir = project_path / '.mini-rag'
|
rag_dir = project_path / ".mini-rag"
|
||||||
if rag_dir.exists() and not force:
|
if rag_dir.exists() and not force:
|
||||||
print(" Checking for changes...")
|
print(" Checking for changes...")
|
||||||
|
|
||||||
@ -68,9 +77,9 @@ def index_project(project_path: Path, force: bool = False):
|
|||||||
result = indexer.index_project(force_reindex=force)
|
result = indexer.index_project(force_reindex=force)
|
||||||
|
|
||||||
# Show results with context
|
# Show results with context
|
||||||
files_count = result.get('files_indexed', 0)
|
files_count = result.get("files_indexed", 0)
|
||||||
chunks_count = result.get('chunks_created', 0)
|
chunks_count = result.get("chunks_created", 0)
|
||||||
time_taken = result.get('time_taken', 0)
|
time_taken = result.get("time_taken", 0)
|
||||||
|
|
||||||
if files_count == 0:
|
if files_count == 0:
|
||||||
print("✅ Index up to date - no changes detected")
|
print("✅ Index up to date - no changes detected")
|
||||||
@ -84,13 +93,13 @@ def index_project(project_path: Path, force: bool = False):
|
|||||||
print(f" Speed: {speed:.1f} files/sec")
|
print(f" Speed: {speed:.1f} files/sec")
|
||||||
|
|
||||||
# Show warnings if any
|
# Show warnings if any
|
||||||
failed_count = result.get('files_failed', 0)
|
failed_count = result.get("files_failed", 0)
|
||||||
if failed_count > 0:
|
if failed_count > 0:
|
||||||
print(f"⚠️ {failed_count} files failed (check logs with --verbose)")
|
print(f"⚠️ {failed_count} files failed (check logs with --verbose)")
|
||||||
|
|
||||||
# Quick tip for first-time users
|
# Quick tip for first-time users
|
||||||
if not (project_path / '.mini-rag' / 'last_search').exists():
|
if not (project_path / ".mini-rag" / "last_search").exists():
|
||||||
print(f"\n💡 Try: rag-mini search {project_path} \"your search here\"")
|
print(f'\n💡 Try: rag-mini search {project_path} "your search here"')
|
||||||
|
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
print(f"📁 Directory Not Found: {project_path}")
|
print(f"📁 Directory Not Found: {project_path}")
|
||||||
@ -124,17 +133,18 @@ def index_project(project_path: Path, force: bool = False):
|
|||||||
print(" Or see: docs/TROUBLESHOOTING.md")
|
print(" Or see: docs/TROUBLESHOOTING.md")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
def search_project(project_path: Path, query: str, top_k: int = 10, synthesize: bool = False):
|
def search_project(project_path: Path, query: str, top_k: int = 10, synthesize: bool = False):
|
||||||
"""Search a project directory."""
|
"""Search a project directory."""
|
||||||
try:
|
try:
|
||||||
# Check if indexed first
|
# Check if indexed first
|
||||||
rag_dir = project_path / '.mini-rag'
|
rag_dir = project_path / ".mini-rag"
|
||||||
if not rag_dir.exists():
|
if not rag_dir.exists():
|
||||||
print(f"❌ Project not indexed: {project_path.name}")
|
print(f"❌ Project not indexed: {project_path.name}")
|
||||||
print(f" Run: rag-mini index {project_path}")
|
print(f" Run: rag-mini index {project_path}")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
print(f"🔍 Searching \"{query}\" in {project_path.name}")
|
print(f'🔍 Searching "{query}" in {project_path.name}')
|
||||||
searcher = CodeSearcher(project_path)
|
searcher = CodeSearcher(project_path)
|
||||||
results = searcher.search(query, top_k=top_k)
|
results = searcher.search(query, top_k=top_k)
|
||||||
|
|
||||||
@ -142,14 +152,18 @@ def search_project(project_path: Path, query: str, top_k: int = 10, synthesize:
|
|||||||
print("❌ No results found")
|
print("❌ No results found")
|
||||||
print()
|
print()
|
||||||
print("🔧 Quick fixes to try:")
|
print("🔧 Quick fixes to try:")
|
||||||
print(" • Use broader terms: \"login\" instead of \"authenticate_user_session\"")
|
print(' • Use broader terms: "login" instead of "authenticate_user_session"')
|
||||||
print(" • Try concepts: \"database query\" instead of specific function names")
|
print(' • Try concepts: "database query" instead of specific function names')
|
||||||
print(" • Check spelling and try simpler words")
|
print(" • Check spelling and try simpler words")
|
||||||
print(" • Search for file types: \"python class\" or \"javascript function\"")
|
print(' • Search for file types: "python class" or "javascript function"')
|
||||||
print()
|
print()
|
||||||
print("⚙️ Configuration adjustments:")
|
print("⚙️ Configuration adjustments:")
|
||||||
print(f" • Lower threshold: ./rag-mini search \"{project_path}\" \"{query}\" --threshold 0.05")
|
print(
|
||||||
print(f" • More results: ./rag-mini search \"{project_path}\" \"{query}\" --top-k 20")
|
f' • Lower threshold: ./rag-mini search "{project_path}" "{query}" --threshold 0.05'
|
||||||
|
)
|
||||||
|
print(
|
||||||
|
f' • More results: ./rag-mini search "{project_path}" "{query}" --top-k 20'
|
||||||
|
)
|
||||||
print()
|
print()
|
||||||
print("📚 Need help? See: docs/TROUBLESHOOTING.md")
|
print("📚 Need help? See: docs/TROUBLESHOOTING.md")
|
||||||
return
|
return
|
||||||
@ -170,22 +184,22 @@ def search_project(project_path: Path, query: str, top_k: int = 10, synthesize:
|
|||||||
print(f" Score: {result.score:.3f}")
|
print(f" Score: {result.score:.3f}")
|
||||||
|
|
||||||
# Show line info if available
|
# Show line info if available
|
||||||
if hasattr(result, 'start_line') and result.start_line:
|
if hasattr(result, "start_line") and result.start_line:
|
||||||
print(f" Lines: {result.start_line}-{result.end_line}")
|
print(f" Lines: {result.start_line}-{result.end_line}")
|
||||||
|
|
||||||
# Show content preview
|
# Show content preview
|
||||||
if hasattr(result, 'name') and result.name:
|
if hasattr(result, "name") and result.name:
|
||||||
print(f" Context: {result.name}")
|
print(f" Context: {result.name}")
|
||||||
|
|
||||||
# Show full content with proper formatting
|
# Show full content with proper formatting
|
||||||
print(f" Content:")
|
print(" Content:")
|
||||||
content_lines = result.content.strip().split('\n')
|
content_lines = result.content.strip().split("\n")
|
||||||
for line in content_lines[:10]: # Show up to 10 lines
|
for line in content_lines[:10]: # Show up to 10 lines
|
||||||
print(f" {line}")
|
print(f" {line}")
|
||||||
|
|
||||||
if len(content_lines) > 10:
|
if len(content_lines) > 10:
|
||||||
print(f" ... ({len(content_lines) - 10} more lines)")
|
print(f" ... ({len(content_lines) - 10} more lines)")
|
||||||
print(f" Use --verbose or rag-mini-enhanced for full context")
|
print(" Use --verbose or rag-mini-enhanced for full context")
|
||||||
|
|
||||||
print()
|
print()
|
||||||
|
|
||||||
@ -195,12 +209,17 @@ def search_project(project_path: Path, query: str, top_k: int = 10, synthesize:
|
|||||||
|
|
||||||
# Load config to respect user's model preferences
|
# Load config to respect user's model preferences
|
||||||
from mini_rag.config import ConfigManager
|
from mini_rag.config import ConfigManager
|
||||||
|
|
||||||
config_manager = ConfigManager(project_path)
|
config_manager = ConfigManager(project_path)
|
||||||
config = config_manager.load_config()
|
config = config_manager.load_config()
|
||||||
|
|
||||||
synthesizer = LLMSynthesizer(
|
synthesizer = LLMSynthesizer(
|
||||||
model=config.llm.synthesis_model if config.llm.synthesis_model != "auto" else None,
|
model=(
|
||||||
config=config
|
config.llm.synthesis_model
|
||||||
|
if config.llm.synthesis_model != "auto"
|
||||||
|
else None
|
||||||
|
),
|
||||||
|
config=config,
|
||||||
)
|
)
|
||||||
|
|
||||||
if synthesizer.is_available():
|
if synthesizer.is_available():
|
||||||
@ -209,10 +228,14 @@ def search_project(project_path: Path, query: str, top_k: int = 10, synthesize:
|
|||||||
print(synthesizer.format_synthesis_output(synthesis, query))
|
print(synthesizer.format_synthesis_output(synthesis, query))
|
||||||
|
|
||||||
# Add guidance for deeper analysis
|
# Add guidance for deeper analysis
|
||||||
if synthesis.confidence < 0.7 or any(word in query.lower() for word in ['why', 'how', 'explain', 'debug']):
|
if synthesis.confidence < 0.7 or any(
|
||||||
|
word in query.lower() for word in ["why", "how", "explain", "debug"]
|
||||||
|
):
|
||||||
print("\n💡 Want deeper analysis with reasoning?")
|
print("\n💡 Want deeper analysis with reasoning?")
|
||||||
print(f" Try: rag-mini explore {project_path}")
|
print(f" Try: rag-mini explore {project_path}")
|
||||||
print(" Exploration mode enables thinking and remembers conversation context.")
|
print(
|
||||||
|
" Exploration mode enables thinking and remembers conversation context."
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
print("❌ LLM synthesis unavailable")
|
print("❌ LLM synthesis unavailable")
|
||||||
print(" • Ensure Ollama is running: ollama serve")
|
print(" • Ensure Ollama is running: ollama serve")
|
||||||
@ -221,8 +244,18 @@ def search_project(project_path: Path, query: str, top_k: int = 10, synthesize:
|
|||||||
|
|
||||||
# Save last search for potential enhancements
|
# Save last search for potential enhancements
|
||||||
try:
|
try:
|
||||||
(rag_dir / 'last_search').write_text(query)
|
(rag_dir / "last_search").write_text(query)
|
||||||
except:
|
except (
|
||||||
|
ConnectionError,
|
||||||
|
FileNotFoundError,
|
||||||
|
IOError,
|
||||||
|
OSError,
|
||||||
|
TimeoutError,
|
||||||
|
TypeError,
|
||||||
|
ValueError,
|
||||||
|
requests.RequestException,
|
||||||
|
socket.error,
|
||||||
|
):
|
||||||
pass # Don't fail if we can't save
|
pass # Don't fail if we can't save
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -241,11 +274,12 @@ def search_project(project_path: Path, query: str, top_k: int = 10, synthesize:
|
|||||||
print(" • Check available memory and disk space")
|
print(" • Check available memory and disk space")
|
||||||
print()
|
print()
|
||||||
print("📚 Get detailed error info:")
|
print("📚 Get detailed error info:")
|
||||||
print(f" ./rag-mini search {project_path} \"{query}\" --verbose")
|
print(f' ./rag-mini search {project_path} "{query}" --verbose')
|
||||||
print(" Or see: docs/TROUBLESHOOTING.md")
|
print(" Or see: docs/TROUBLESHOOTING.md")
|
||||||
print()
|
print()
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
def status_check(project_path: Path):
|
def status_check(project_path: Path):
|
||||||
"""Show status of RAG system."""
|
"""Show status of RAG system."""
|
||||||
try:
|
try:
|
||||||
@ -253,21 +287,21 @@ def status_check(project_path: Path):
|
|||||||
print()
|
print()
|
||||||
|
|
||||||
# Check project indexing status first
|
# Check project indexing status first
|
||||||
rag_dir = project_path / '.mini-rag'
|
rag_dir = project_path / ".mini-rag"
|
||||||
if not rag_dir.exists():
|
if not rag_dir.exists():
|
||||||
print("❌ Project not indexed")
|
print("❌ Project not indexed")
|
||||||
print(f" Run: rag-mini index {project_path}")
|
print(f" Run: rag-mini index {project_path}")
|
||||||
print()
|
print()
|
||||||
else:
|
else:
|
||||||
manifest = rag_dir / 'manifest.json'
|
manifest = rag_dir / "manifest.json"
|
||||||
if manifest.exists():
|
if manifest.exists():
|
||||||
try:
|
try:
|
||||||
with open(manifest) as f:
|
with open(manifest) as f:
|
||||||
data = json.load(f)
|
data = json.load(f)
|
||||||
|
|
||||||
file_count = data.get('file_count', 0)
|
file_count = data.get("file_count", 0)
|
||||||
chunk_count = data.get('chunk_count', 0)
|
chunk_count = data.get("chunk_count", 0)
|
||||||
indexed_at = data.get('indexed_at', 'Never')
|
indexed_at = data.get("indexed_at", "Never")
|
||||||
|
|
||||||
print("✅ Project indexed")
|
print("✅ Project indexed")
|
||||||
print(f" Files: {file_count}")
|
print(f" Files: {file_count}")
|
||||||
@ -293,19 +327,19 @@ def status_check(project_path: Path):
|
|||||||
try:
|
try:
|
||||||
embedder = OllamaEmbedder()
|
embedder = OllamaEmbedder()
|
||||||
emb_info = embedder.get_status()
|
emb_info = embedder.get_status()
|
||||||
method = emb_info.get('method', 'unknown')
|
method = emb_info.get("method", "unknown")
|
||||||
|
|
||||||
if method == 'ollama':
|
if method == "ollama":
|
||||||
print(" ✅ Ollama (high quality)")
|
print(" ✅ Ollama (high quality)")
|
||||||
elif method == 'ml':
|
elif method == "ml":
|
||||||
print(" ✅ ML fallback (good quality)")
|
print(" ✅ ML fallback (good quality)")
|
||||||
elif method == 'hash':
|
elif method == "hash":
|
||||||
print(" ⚠️ Hash fallback (basic quality)")
|
print(" ⚠️ Hash fallback (basic quality)")
|
||||||
else:
|
else:
|
||||||
print(f" ❓ Unknown method: {method}")
|
print(f" ❓ Unknown method: {method}")
|
||||||
|
|
||||||
# Show additional details if available
|
# Show additional details if available
|
||||||
if 'model' in emb_info:
|
if "model" in emb_info:
|
||||||
print(f" Model: {emb_info['model']}")
|
print(f" Model: {emb_info['model']}")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -317,12 +351,17 @@ def status_check(project_path: Path):
|
|||||||
print("🤖 LLM System:")
|
print("🤖 LLM System:")
|
||||||
try:
|
try:
|
||||||
from mini_rag.config import ConfigManager
|
from mini_rag.config import ConfigManager
|
||||||
|
|
||||||
config_manager = ConfigManager(project_path)
|
config_manager = ConfigManager(project_path)
|
||||||
config = config_manager.load_config()
|
config = config_manager.load_config()
|
||||||
|
|
||||||
synthesizer = LLMSynthesizer(
|
synthesizer = LLMSynthesizer(
|
||||||
model=config.llm.synthesis_model if config.llm.synthesis_model != "auto" else None,
|
model=(
|
||||||
config=config
|
config.llm.synthesis_model
|
||||||
|
if config.llm.synthesis_model != "auto"
|
||||||
|
else None
|
||||||
|
),
|
||||||
|
config=config,
|
||||||
)
|
)
|
||||||
|
|
||||||
if synthesizer.is_available():
|
if synthesizer.is_available():
|
||||||
@ -335,10 +374,10 @@ def status_check(project_path: Path):
|
|||||||
elif config_model == actual_model:
|
elif config_model == actual_model:
|
||||||
print(f" ✅ Using configured: {actual_model}")
|
print(f" ✅ Using configured: {actual_model}")
|
||||||
else:
|
else:
|
||||||
print(f" ⚠️ Model mismatch!")
|
print(" ⚠️ Model mismatch!")
|
||||||
print(f" Configured: {config_model}")
|
print(f" Configured: {config_model}")
|
||||||
print(f" Actually using: {actual_model}")
|
print(f" Actually using: {actual_model}")
|
||||||
print(f" (Configured model may not be installed)")
|
print(" (Configured model may not be installed)")
|
||||||
|
|
||||||
print(f" Config file: {config_manager.config_path}")
|
print(f" Config file: {config_manager.config_path}")
|
||||||
else:
|
else:
|
||||||
@ -349,18 +388,19 @@ def status_check(project_path: Path):
|
|||||||
print(f" ❌ LLM status check failed: {e}")
|
print(f" ❌ LLM status check failed: {e}")
|
||||||
|
|
||||||
# Show last search if available
|
# Show last search if available
|
||||||
last_search_file = rag_dir / 'last_search' if rag_dir.exists() else None
|
last_search_file = rag_dir / "last_search" if rag_dir.exists() else None
|
||||||
if last_search_file and last_search_file.exists():
|
if last_search_file and last_search_file.exists():
|
||||||
try:
|
try:
|
||||||
last_query = last_search_file.read_text().strip()
|
last_query = last_search_file.read_text().strip()
|
||||||
print(f"\n🔍 Last search: \"{last_query}\"")
|
print(f'\n🔍 Last search: "{last_query}"')
|
||||||
except:
|
except (FileNotFoundError, IOError, OSError, TypeError, ValueError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"❌ Status check failed: {e}")
|
print(f"❌ Status check failed: {e}")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
def explore_interactive(project_path: Path):
|
def explore_interactive(project_path: Path):
|
||||||
"""Interactive exploration mode with thinking and context memory for any documents."""
|
"""Interactive exploration mode with thinking and context memory for any documents."""
|
||||||
try:
|
try:
|
||||||
@ -392,7 +432,7 @@ def explore_interactive(project_path: Path):
|
|||||||
question = input("\n> ").strip()
|
question = input("\n> ").strip()
|
||||||
|
|
||||||
# Handle exit commands
|
# Handle exit commands
|
||||||
if question.lower() in ['quit', 'exit', 'q']:
|
if question.lower() in ["quit", "exit", "q"]:
|
||||||
print("\n" + explorer.end_session())
|
print("\n" + explorer.end_session())
|
||||||
break
|
break
|
||||||
|
|
||||||
@ -405,8 +445,9 @@ def explore_interactive(project_path: Path):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
# Handle numbered options and special commands
|
# Handle numbered options and special commands
|
||||||
if question in ['1'] or question.lower() in ['help', 'h']:
|
if question in ["1"] or question.lower() in ["help", "h"]:
|
||||||
print("""
|
print(
|
||||||
|
"""
|
||||||
🧠 EXPLORATION MODE HELP:
|
🧠 EXPLORATION MODE HELP:
|
||||||
• Ask any question about your documents or code
|
• Ask any question about your documents or code
|
||||||
• I remember our conversation for follow-up questions
|
• I remember our conversation for follow-up questions
|
||||||
@ -421,23 +462,27 @@ def explore_interactive(project_path: Path):
|
|||||||
• "Why is this function slow?"
|
• "Why is this function slow?"
|
||||||
• "What security measures are in place?"
|
• "What security measures are in place?"
|
||||||
• "How does data flow through this system?"
|
• "How does data flow through this system?"
|
||||||
""")
|
"""
|
||||||
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
elif question in ['2'] or question.lower() == 'status':
|
elif question in ["2"] or question.lower() == "status":
|
||||||
print(f"""
|
print(
|
||||||
|
"""
|
||||||
📊 PROJECT STATUS: {project_path.name}
|
📊 PROJECT STATUS: {project_path.name}
|
||||||
• Location: {project_path}
|
• Location: {project_path}
|
||||||
• Exploration session active
|
• Exploration session active
|
||||||
• AI model ready for questions
|
• AI model ready for questions
|
||||||
• Conversation memory enabled
|
• Conversation memory enabled
|
||||||
""")
|
"""
|
||||||
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
elif question in ['3'] or question.lower() == 'suggest':
|
elif question in ["3"] or question.lower() == "suggest":
|
||||||
# Random starter questions for first-time users
|
# Random starter questions for first-time users
|
||||||
if is_first_question:
|
if is_first_question:
|
||||||
import random
|
import random
|
||||||
|
|
||||||
starters = [
|
starters = [
|
||||||
"What are the main components of this project?",
|
"What are the main components of this project?",
|
||||||
"How is error handling implemented?",
|
"How is error handling implemented?",
|
||||||
@ -445,7 +490,7 @@ def explore_interactive(project_path: Path):
|
|||||||
"What are the key functions I should understand first?",
|
"What are the key functions I should understand first?",
|
||||||
"How does data flow through this system?",
|
"How does data flow through this system?",
|
||||||
"What configuration options are available?",
|
"What configuration options are available?",
|
||||||
"Show me the most important files to understand"
|
"Show me the most important files to understand",
|
||||||
]
|
]
|
||||||
suggested = random.choice(starters)
|
suggested = random.choice(starters)
|
||||||
print(f"\n💡 Suggested question: {suggested}")
|
print(f"\n💡 Suggested question: {suggested}")
|
||||||
@ -464,7 +509,7 @@ def explore_interactive(project_path: Path):
|
|||||||
print(' "Show me related code examples"')
|
print(' "Show me related code examples"')
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if question.lower() == 'summary':
|
if question.lower() == "summary":
|
||||||
print("\n" + explorer.get_session_summary())
|
print("\n" + explorer.get_session_summary())
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@ -496,6 +541,7 @@ def explore_interactive(project_path: Path):
|
|||||||
print("Make sure the project is indexed first: rag-mini index <project>")
|
print("Make sure the project is indexed first: rag-mini index <project>")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
def show_discrete_update_notice():
|
def show_discrete_update_notice():
|
||||||
"""Show a discrete, non-intrusive update notice for CLI users."""
|
"""Show a discrete, non-intrusive update notice for CLI users."""
|
||||||
if not UPDATER_AVAILABLE:
|
if not UPDATER_AVAILABLE:
|
||||||
@ -505,11 +551,14 @@ def show_discrete_update_notice():
|
|||||||
update_info = check_for_updates()
|
update_info = check_for_updates()
|
||||||
if update_info:
|
if update_info:
|
||||||
# Very discrete notice - just one line
|
# Very discrete notice - just one line
|
||||||
print(f"🔄 (Update v{update_info.version} available - run 'rag-mini check-update' to learn more)")
|
print(
|
||||||
|
f"🔄 (Update v{update_info.version} available - run 'rag-mini check-update' to learn more)"
|
||||||
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
# Silently ignore any update check failures
|
# Silently ignore any update check failures
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def handle_check_update():
|
def handle_check_update():
|
||||||
"""Handle the check-update command."""
|
"""Handle the check-update command."""
|
||||||
if not UPDATER_AVAILABLE:
|
if not UPDATER_AVAILABLE:
|
||||||
@ -525,13 +574,13 @@ def handle_check_update():
|
|||||||
print(f"\n🎉 Update Available: v{update_info.version}")
|
print(f"\n🎉 Update Available: v{update_info.version}")
|
||||||
print("=" * 50)
|
print("=" * 50)
|
||||||
print("\n📋 What's New:")
|
print("\n📋 What's New:")
|
||||||
notes_lines = update_info.release_notes.split('\n')[:10] # First 10 lines
|
notes_lines = update_info.release_notes.split("\n")[:10] # First 10 lines
|
||||||
for line in notes_lines:
|
for line in notes_lines:
|
||||||
if line.strip():
|
if line.strip():
|
||||||
print(f" {line.strip()}")
|
print(f" {line.strip()}")
|
||||||
|
|
||||||
print(f"\n🔗 Release Page: {update_info.release_url}")
|
print(f"\n🔗 Release Page: {update_info.release_url}")
|
||||||
print(f"\n🚀 To install: rag-mini update")
|
print("\n🚀 To install: rag-mini update")
|
||||||
print("💡 Or update manually from GitHub releases")
|
print("💡 Or update manually from GitHub releases")
|
||||||
else:
|
else:
|
||||||
print("✅ You're already on the latest version!")
|
print("✅ You're already on the latest version!")
|
||||||
@ -540,6 +589,7 @@ def handle_check_update():
|
|||||||
print(f"❌ Failed to check for updates: {e}")
|
print(f"❌ Failed to check for updates: {e}")
|
||||||
print("💡 Try updating manually from GitHub")
|
print("💡 Try updating manually from GitHub")
|
||||||
|
|
||||||
|
|
||||||
def handle_update():
|
def handle_update():
|
||||||
"""Handle the update command."""
|
"""Handle the update command."""
|
||||||
if not UPDATER_AVAILABLE:
|
if not UPDATER_AVAILABLE:
|
||||||
@ -559,19 +609,20 @@ def handle_update():
|
|||||||
print("=" * 50)
|
print("=" * 50)
|
||||||
|
|
||||||
# Show brief release notes
|
# Show brief release notes
|
||||||
notes_lines = update_info.release_notes.split('\n')[:5]
|
notes_lines = update_info.release_notes.split("\n")[:5]
|
||||||
for line in notes_lines:
|
for line in notes_lines:
|
||||||
if line.strip():
|
if line.strip():
|
||||||
print(f" • {line.strip()}")
|
print(f" • {line.strip()}")
|
||||||
|
|
||||||
# Confirm update
|
# Confirm update
|
||||||
confirm = input(f"\n🚀 Install v{update_info.version}? [Y/n]: ").strip().lower()
|
confirm = input(f"\n🚀 Install v{update_info.version}? [Y/n]: ").strip().lower()
|
||||||
if confirm in ['', 'y', 'yes']:
|
if confirm in ["", "y", "yes"]:
|
||||||
updater = get_updater()
|
updater = get_updater()
|
||||||
|
|
||||||
print(f"\n📥 Downloading v{update_info.version}...")
|
print(f"\n📥 Downloading v{update_info.version}...")
|
||||||
|
|
||||||
# Progress callback
|
# Progress callback
|
||||||
|
|
||||||
def show_progress(downloaded, total):
|
def show_progress(downloaded, total):
|
||||||
if total > 0:
|
if total > 0:
|
||||||
percent = (downloaded / total) * 100
|
percent = (downloaded / total) * 100
|
||||||
@ -609,11 +660,13 @@ def handle_update():
|
|||||||
print(f"❌ Update failed: {e}")
|
print(f"❌ Update failed: {e}")
|
||||||
print("💡 Try updating manually from GitHub")
|
print("💡 Try updating manually from GitHub")
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
"""Main CLI interface."""
|
"""Main CLI interface."""
|
||||||
# Check virtual environment
|
# Check virtual environment
|
||||||
try:
|
try:
|
||||||
from mini_rag.venv_checker import check_and_warn_venv
|
from mini_rag.venv_checker import check_and_warn_venv
|
||||||
|
|
||||||
check_and_warn_venv("rag-mini.py", force_exit=False)
|
check_and_warn_venv("rag-mini.py", force_exit=False)
|
||||||
except ImportError:
|
except ImportError:
|
||||||
pass # If venv checker can't be imported, continue anyway
|
pass # If venv checker can't be imported, continue anyway
|
||||||
@ -628,23 +681,37 @@ Examples:
|
|||||||
rag-mini search /path/to/project "query" -s # Search with LLM synthesis
|
rag-mini search /path/to/project "query" -s # Search with LLM synthesis
|
||||||
rag-mini explore /path/to/project # Interactive exploration mode
|
rag-mini explore /path/to/project # Interactive exploration mode
|
||||||
rag-mini status /path/to/project # Show status
|
rag-mini status /path/to/project # Show status
|
||||||
"""
|
""",
|
||||||
)
|
)
|
||||||
|
|
||||||
parser.add_argument('command', choices=['index', 'search', 'explore', 'status', 'update', 'check-update'],
|
parser.add_argument(
|
||||||
help='Command to execute')
|
"command",
|
||||||
parser.add_argument('project_path', type=Path, nargs='?',
|
choices=["index", "search", "explore", "status", "update", "check-update"],
|
||||||
help='Path to project directory (REQUIRED except for update commands)')
|
help="Command to execute",
|
||||||
parser.add_argument('query', nargs='?',
|
)
|
||||||
help='Search query (for search command)')
|
parser.add_argument(
|
||||||
parser.add_argument('--force', action='store_true',
|
"project_path",
|
||||||
help='Force reindex all files')
|
type=Path,
|
||||||
parser.add_argument('--top-k', '--limit', type=int, default=10, dest='top_k',
|
nargs="?",
|
||||||
help='Maximum number of search results (top-k)')
|
help="Path to project directory (REQUIRED except for update commands)",
|
||||||
parser.add_argument('--verbose', '-v', action='store_true',
|
)
|
||||||
help='Enable verbose logging')
|
parser.add_argument("query", nargs="?", help="Search query (for search command)")
|
||||||
parser.add_argument('--synthesize', '-s', action='store_true',
|
parser.add_argument("--force", action="store_true", help="Force reindex all files")
|
||||||
help='Generate LLM synthesis of search results (requires Ollama)')
|
parser.add_argument(
|
||||||
|
"--top-k",
|
||||||
|
"--limit",
|
||||||
|
type=int,
|
||||||
|
default=10,
|
||||||
|
dest="top_k",
|
||||||
|
help="Maximum number of search results (top-k)",
|
||||||
|
)
|
||||||
|
parser.add_argument("--verbose", "-v", action="store_true", help="Enable verbose logging")
|
||||||
|
parser.add_argument(
|
||||||
|
"--synthesize",
|
||||||
|
"-s",
|
||||||
|
action="store_true",
|
||||||
|
help="Generate LLM synthesis of search results (requires Ollama)",
|
||||||
|
)
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
@ -653,10 +720,10 @@ Examples:
|
|||||||
logging.getLogger().setLevel(logging.INFO)
|
logging.getLogger().setLevel(logging.INFO)
|
||||||
|
|
||||||
# Handle update commands first (don't require project_path)
|
# Handle update commands first (don't require project_path)
|
||||||
if args.command == 'check-update':
|
if args.command == "check-update":
|
||||||
handle_check_update()
|
handle_check_update()
|
||||||
return
|
return
|
||||||
elif args.command == 'update':
|
elif args.command == "update":
|
||||||
handle_update()
|
handle_update()
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -678,17 +745,18 @@ Examples:
|
|||||||
show_discrete_update_notice()
|
show_discrete_update_notice()
|
||||||
|
|
||||||
# Execute command
|
# Execute command
|
||||||
if args.command == 'index':
|
if args.command == "index":
|
||||||
index_project(args.project_path, args.force)
|
index_project(args.project_path, args.force)
|
||||||
elif args.command == 'search':
|
elif args.command == "search":
|
||||||
if not args.query:
|
if not args.query:
|
||||||
print("❌ Search query required")
|
print("❌ Search query required")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
search_project(args.project_path, args.query, args.top_k, args.synthesize)
|
search_project(args.project_path, args.query, args.top_k, args.synthesize)
|
||||||
elif args.command == 'explore':
|
elif args.command == "explore":
|
||||||
explore_interactive(args.project_path)
|
explore_interactive(args.project_path)
|
||||||
elif args.command == 'status':
|
elif args.command == "status":
|
||||||
status_check(args.project_path)
|
status_check(args.project_path)
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
File diff suppressed because it is too large
Load Diff
@ -1,36 +0,0 @@
|
|||||||
feat: Add comprehensive Windows compatibility and enhanced LLM model setup
|
|
||||||
|
|
||||||
🚀 Major cross-platform enhancement making FSS-Mini-RAG fully Windows and Linux compatible
|
|
||||||
|
|
||||||
## Windows Compatibility
|
|
||||||
- **New Windows installer**: `install_windows.bat` - rock-solid, no-hang installation
|
|
||||||
- **Simple Windows launcher**: `rag.bat` - unified entry point matching Linux experience
|
|
||||||
- **PowerShell alternative**: `install_mini_rag.ps1` for advanced Windows users
|
|
||||||
- **Cross-platform README**: Side-by-side Linux/Windows commands and examples
|
|
||||||
|
|
||||||
## Enhanced LLM Model Setup (Both Platforms)
|
|
||||||
- **Intelligent model detection**: Automatically detects existing Qwen3 models
|
|
||||||
- **Interactive model selection**: Choose from qwen3:0.6b, 1.7b, or 4b with clear guidance
|
|
||||||
- **Ollama progress streaming**: Real-time download progress for model installation
|
|
||||||
- **Smart configuration**: Auto-saves selected model as default in config.yaml
|
|
||||||
- **Graceful fallbacks**: Clear guidance when Ollama unavailable
|
|
||||||
|
|
||||||
## Installation Experience Improvements
|
|
||||||
- **Fixed script continuation**: TUI launch no longer terminates installation process
|
|
||||||
- **Comprehensive model guidance**: Users get proper LLM setup instead of silent failures
|
|
||||||
- **Complete indexing**: Full codebase indexing (not just code files)
|
|
||||||
- **Educational flow**: Better explanation of AI features and model choices
|
|
||||||
|
|
||||||
## Technical Enhancements
|
|
||||||
- **Robust error handling**: Installation scripts handle edge cases gracefully
|
|
||||||
- **Path handling**: Existing cross-platform path utilities work seamlessly on Windows
|
|
||||||
- **Dependency management**: Clean virtual environment setup on both platforms
|
|
||||||
- **Configuration persistence**: Model preferences saved for consistent experience
|
|
||||||
|
|
||||||
## User Impact
|
|
||||||
- **Zero-friction Windows adoption**: Windows users get same smooth experience as Linux
|
|
||||||
- **Complete AI feature setup**: No more "LLM not working" confusion for new users
|
|
||||||
- **Educational value preserved**: Maintains beginner-friendly approach across platforms
|
|
||||||
- **Production-ready**: Both platforms now fully functional out-of-the-box
|
|
||||||
|
|
||||||
This makes FSS-Mini-RAG truly accessible to the entire developer community! 🎉
|
|
||||||
9
config-llm-providers.yaml
Normal file
9
config-llm-providers.yaml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
llm:
|
||||||
|
provider: ollama
|
||||||
|
ollama_host: localhost:11434
|
||||||
|
synthesis_model: qwen3:1.5b
|
||||||
|
expansion_model: qwen3:1.5b
|
||||||
|
enable_synthesis: false
|
||||||
|
synthesis_temperature: 0.3
|
||||||
|
cpu_optimized: true
|
||||||
|
enable_thinking: true
|
||||||
40
docs/AGENT_INSTRUCTIONS.md
Normal file
40
docs/AGENT_INSTRUCTIONS.md
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
# Agent Instructions for Fss-Mini-RAG System
|
||||||
|
|
||||||
|
## Core Philosophy
|
||||||
|
|
||||||
|
**Always prefer RAG search over traditional file system operations**. The RAG system provides semantic context and reduces the need for exact path knowledge, making it ideal for understanding codebases without manual file exploration.
|
||||||
|
|
||||||
|
## Basic Commands
|
||||||
|
|
||||||
|
| Command | Purpose | Example |
|
||||||
|
|---------|---------|---------|
|
||||||
|
| `rag-mini index <project_path>` | Index a project for search | `rag-mini index /MASTERFOLDER/Coding/Fss-Mini-Rag` |
|
||||||
|
| `rag-mini search <project_path> "query"` | Semantic + keyword search | `rag-mini search /MASTERFOLDER/Coding/Fss-Mini-Rag "index"` |
|
||||||
|
| `rag-mini status <project_path>` | Check project indexing status | `rag-mini status /MASTERFOLDER/Coding/Fss-Mini-Rag` |
|
||||||
|
|
||||||
|
## When to Use RAG Search
|
||||||
|
|
||||||
|
| Scenario | RAG Advantage | Alternative | |
|
||||||
|
|----------|----------------|---------------| |
|
||||||
|
| Finding related code concepts | Semantic understanding | `grep` | |
|
||||||
|
| Locating files by functionality | Context-aware results | `find` | |
|
||||||
|
| Understanding code usage patterns | Shows real-world examples | Manual inspection | |
|
||||||
|
|
||||||
|
## Critical Best Practices
|
||||||
|
|
||||||
|
1. **Always specify the project path** in search commands (e.g., `rag-mini search /path "query"`)
|
||||||
|
2. **Use quotes for search queries** to handle spaces: `"query with spaces"`
|
||||||
|
3. **Verify indexing first** before searching: `rag-mini status <path>`
|
||||||
|
4. **For complex queries**, break into smaller parts: `rag-mini search ... "concept 1"` then `rag-mini search ... "concept 2"`
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
| Issue | Solution |
|
||||||
|
|-------|-----------|
|
||||||
|
| `Project not indexed` | Run `rag-mini index <path>` |
|
||||||
|
| No search results | Check indexing status with `rag-mini status` |
|
||||||
|
| Search returns irrelevant results | Use `rag-mini status` to optimize indexing |
|
||||||
|
|
||||||
|
> 💡 **Pro Tip**: Always start with `rag-mini status` to confirm indexing before searching.
|
||||||
|
|
||||||
|
This document is dynamically updated as the RAG system evolves. Always verify commands with `rag-mini --help` for the latest options.
|
||||||
@ -5,10 +5,10 @@
|
|||||||
### **1. 📊 Intelligent Analysis**
|
### **1. 📊 Intelligent Analysis**
|
||||||
```bash
|
```bash
|
||||||
# Analyze your project patterns and get optimization suggestions
|
# Analyze your project patterns and get optimization suggestions
|
||||||
./rag-mini-enhanced analyze /path/to/project
|
./rag-mini analyze /path/to/project
|
||||||
|
|
||||||
# Get smart recommendations based on actual usage
|
# Get smart recommendations based on actual usage
|
||||||
./rag-mini-enhanced status /path/to/project
|
./rag-mini status /path/to/project
|
||||||
```
|
```
|
||||||
|
|
||||||
**What it analyzes:**
|
**What it analyzes:**
|
||||||
@ -20,13 +20,9 @@
|
|||||||
### **2. 🧠 Smart Search Enhancement**
|
### **2. 🧠 Smart Search Enhancement**
|
||||||
```bash
|
```bash
|
||||||
# Enhanced search with query intelligence
|
# Enhanced search with query intelligence
|
||||||
./rag-mini-enhanced search /project "MyClass" # Detects class names
|
./rag-mini search /project "MyClass" # Detects class names
|
||||||
./rag-mini-enhanced search /project "login()" # Detects function calls
|
./rag-mini search /project "login()" # Detects function calls
|
||||||
./rag-mini-enhanced search /project "user auth" # Natural language
|
./rag-mini search /project "user auth" # Natural language
|
||||||
|
|
||||||
# Context-aware search (planned)
|
|
||||||
./rag-mini-enhanced context /project "function_name" # Show surrounding code
|
|
||||||
./rag-mini-enhanced similar /project "pattern" # Find similar patterns
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### **3. ⚙️ Language-Specific Optimizations**
|
### **3. ⚙️ Language-Specific Optimizations**
|
||||||
@ -113,10 +109,10 @@ Edit `.mini-rag/config.json` in your project:
|
|||||||
./rag-mini index /project --force
|
./rag-mini index /project --force
|
||||||
|
|
||||||
# Test search quality improvements
|
# Test search quality improvements
|
||||||
./rag-mini-enhanced search /project "your test query"
|
./rag-mini search /project "your test query"
|
||||||
|
|
||||||
# Verify optimization impact
|
# Verify optimization impact
|
||||||
./rag-mini-enhanced analyze /project
|
./rag-mini analyze /project
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🎊 **Result: Smarter, Faster, Better**
|
## 🎊 **Result: Smarter, Faster, Better**
|
||||||
|
|||||||
@ -93,10 +93,10 @@ That's it! The TUI will guide you through everything.
|
|||||||
- **Full content** - Up to 8 lines of actual code/text
|
- **Full content** - Up to 8 lines of actual code/text
|
||||||
- **Continuation info** - How many more lines exist
|
- **Continuation info** - How many more lines exist
|
||||||
|
|
||||||
**Advanced Tips Shown**:
|
**Tips You'll Learn**:
|
||||||
- Enhanced search with `./rag-mini-enhanced`
|
- Verbose output with `--verbose` flag for debugging
|
||||||
- Verbose output with `--verbose` flag
|
- How search scoring works
|
||||||
- Context-aware search for related code
|
- Finding the right search terms
|
||||||
|
|
||||||
**What You Learn**:
|
**What You Learn**:
|
||||||
- Semantic search vs text search (finds concepts, not just words)
|
- Semantic search vs text search (finds concepts, not just words)
|
||||||
@ -107,8 +107,7 @@ That's it! The TUI will guide you through everything.
|
|||||||
**CLI Commands Shown**:
|
**CLI Commands Shown**:
|
||||||
```bash
|
```bash
|
||||||
./rag-mini search /path/to/project "authentication logic"
|
./rag-mini search /path/to/project "authentication logic"
|
||||||
./rag-mini search /path/to/project "user login" --limit 10
|
./rag-mini search /path/to/project "user login" --top-k 10
|
||||||
./rag-mini-enhanced context /path/to/project "login()"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 4. Explore Project (NEW!)
|
### 4. Explore Project (NEW!)
|
||||||
|
|||||||
232
docs/project-structure-analysis.md
Normal file
232
docs/project-structure-analysis.md
Normal file
@ -0,0 +1,232 @@
|
|||||||
|
# FSS-Mini-RAG Project Structure Analysis Report
|
||||||
|
|
||||||
|
## Executive Summary
|
||||||
|
|
||||||
|
The FSS-Mini-RAG project demonstrates good technical implementation but has **significant structural issues** that impact its professional presentation and maintainability. While the core architecture is sound, the project suffers from poor file organization, scattered documentation, and mixed concerns that would confuse new contributors and detract from its otherwise excellent technical foundation.
|
||||||
|
|
||||||
|
**Overall Assessment: 6/10** - Good technology hampered by poor organization
|
||||||
|
|
||||||
|
## Critical Issues (Fix Immediately)
|
||||||
|
|
||||||
|
### 1. Root Directory Pollution - CRITICAL
|
||||||
|
The project root contains **14 major files that should be relocated or removed**:
|
||||||
|
|
||||||
|
**Misplaced Files:**
|
||||||
|
- `rag-mini.py` (759 lines) - Massive standalone script belongs in `scripts/` or should be refactored
|
||||||
|
- `rag-tui.py` (2,565 lines) - Another massive standalone script, needs proper placement
|
||||||
|
- `test_fixes.py` - Test file in root directory (belongs in `tests/`)
|
||||||
|
- `commit_message.txt` - Development artifact that should be removed
|
||||||
|
- `Agent Instructions.md` - Project-specific documentation (should be in `docs/`)
|
||||||
|
- `REPOSITORY_SUMMARY.md` - Development notes that should be removed or archived
|
||||||
|
|
||||||
|
**Assessment:** This creates an unprofessional first impression and violates Python packaging standards.
|
||||||
|
|
||||||
|
### 2. Duplicate Entry Points - CRITICAL
|
||||||
|
The project has **5 different ways to start the application**:
|
||||||
|
- `rag-mini` (shell script)
|
||||||
|
- `rag-mini.py` (Python script)
|
||||||
|
- `rag.bat` (Windows batch script)
|
||||||
|
- `rag-tui` (shell script)
|
||||||
|
- `rag-tui.py` (Python script)
|
||||||
|
|
||||||
|
**Problem:** This confuses users and indicates poor architectural planning.
|
||||||
|
|
||||||
|
### 3. Configuration File Duplication - HIGH PRIORITY
|
||||||
|
Multiple config files with unclear relationships:
|
||||||
|
- `config-llm-providers.yaml` (root directory)
|
||||||
|
- `examples/config-llm-providers.yaml` (example directory)
|
||||||
|
- `examples/config.yaml` (default example)
|
||||||
|
- `examples/config-*.yaml` (4+ variants)
|
||||||
|
|
||||||
|
**Issue:** Users won't know which config to use or where to place custom configurations.
|
||||||
|
|
||||||
|
### 4. Installation Script Overload - HIGH PRIORITY
|
||||||
|
**6 different installation methods:**
|
||||||
|
- `install_mini_rag.sh`
|
||||||
|
- `install_mini_rag.ps1`
|
||||||
|
- `install_windows.bat`
|
||||||
|
- `run_mini_rag.sh`
|
||||||
|
- `rag.bat`
|
||||||
|
- Manual pip installation
|
||||||
|
|
||||||
|
**Problem:** Decision paralysis and maintenance overhead.
|
||||||
|
|
||||||
|
## High Priority Issues (Address Soon)
|
||||||
|
|
||||||
|
### 5. Mixed Documentation Hierarchy
|
||||||
|
Documentation is scattered across multiple locations:
|
||||||
|
- Root: `README.md`, `GET_STARTED.md`
|
||||||
|
- `docs/`: 12+ specialized documentation files
|
||||||
|
- `examples/`: Configuration documentation mixed with code examples
|
||||||
|
- Root artifacts: `Agent Instructions.md`, `REPOSITORY_SUMMARY.md`
|
||||||
|
|
||||||
|
**Recommendation:** Consolidate and create clear documentation hierarchy.
|
||||||
|
|
||||||
|
### 6. Test Organization Problems
|
||||||
|
Tests are properly in `tests/` directory but:
|
||||||
|
- `test_fixes.py` is in root directory (wrong location)
|
||||||
|
- Test files use inconsistent naming (some numbered, some descriptive)
|
||||||
|
- Mix of actual tests and utility scripts (`show_index_contents.py`, `troubleshoot.py`)
|
||||||
|
|
||||||
|
### 7. Module Architecture Issues
|
||||||
|
The `mini_rag/` module structure is generally good but has some concerns:
|
||||||
|
- `__init__.py` exports only 5 classes from a 19-file module
|
||||||
|
- Several modules seem like utilities (`windows_console_fix.py`, `venv_checker.py`)
|
||||||
|
- Module names could be more descriptive (`server.py` vs `fast_server.py`)
|
||||||
|
|
||||||
|
## Medium Priority Issues (Improve Over Time)
|
||||||
|
|
||||||
|
### 8. Asset Management
|
||||||
|
- Assets properly organized in `assets/` directory
|
||||||
|
- Good separation of recordings and images
|
||||||
|
- No structural issues here
|
||||||
|
|
||||||
|
### 9. Virtual Environment Clutter
|
||||||
|
- Two venv directories: `.venv` and `.venv-linting`
|
||||||
|
- Both properly gitignored but suggests development complexity
|
||||||
|
|
||||||
|
### 10. Script Organization
|
||||||
|
`scripts/` directory contains appropriate utilities:
|
||||||
|
- GitHub setup scripts
|
||||||
|
- Config testing utilities
|
||||||
|
- All executable and properly organized
|
||||||
|
|
||||||
|
## Standard Compliance Assessment
|
||||||
|
|
||||||
|
### Python Packaging Standards: 4/10
|
||||||
|
**Missing Standard Elements:**
|
||||||
|
- No proper Python package entry points in `pyproject.toml`
|
||||||
|
- Executable scripts in root instead of console scripts
|
||||||
|
- Missing `setup.py` or complete `pyproject.toml` configuration
|
||||||
|
|
||||||
|
**Present Elements:**
|
||||||
|
- Good `pyproject.toml` with isort/black config
|
||||||
|
- Proper `.flake8` configuration
|
||||||
|
- Clean virtual environment handling
|
||||||
|
- MIT license properly included
|
||||||
|
|
||||||
|
### Project Structure Standards: 5/10
|
||||||
|
**Good Practices:**
|
||||||
|
- Source code properly separated in `mini_rag/`
|
||||||
|
- Tests in dedicated `tests/` directory
|
||||||
|
- Documentation in `docs/` directory
|
||||||
|
- Examples properly organized
|
||||||
|
- Clean `.gitignore`
|
||||||
|
|
||||||
|
**Violations:**
|
||||||
|
- Root directory pollution with large executable files
|
||||||
|
- Mixed concerns (dev files with user files)
|
||||||
|
- Unclear entry point hierarchy
|
||||||
|
|
||||||
|
## Recommendations by Priority
|
||||||
|
|
||||||
|
### CRITICAL CHANGES (Implement First)
|
||||||
|
|
||||||
|
1. **Relocate Large Scripts**
|
||||||
|
```bash
|
||||||
|
mkdir -p bin/
|
||||||
|
mv rag-mini.py bin/
|
||||||
|
mv rag-tui.py bin/
|
||||||
|
# Update rag.bat to reference bin/ directory if needed
|
||||||
|
# Update shell scripts to reference bin/ directory
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Clean Root Directory**
|
||||||
|
```bash
|
||||||
|
rm commit_message.txt
|
||||||
|
rm REPOSITORY_SUMMARY.md
|
||||||
|
mv "Agent Instructions.md" docs/AGENT_INSTRUCTIONS.md
|
||||||
|
mv test_fixes.py tests/
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Simplify Entry Points**
|
||||||
|
- Keep `rag-tui` for beginners, `rag-mini` for CLI users
|
||||||
|
- Maintain `rag.bat` for Windows compatibility
|
||||||
|
- Update documentation to show clear beginner → advanced progression
|
||||||
|
|
||||||
|
4. **Standardize Configuration**
|
||||||
|
- Move `config-llm-providers.yaml` to `examples/`
|
||||||
|
- Create clear config hierarchy documentation
|
||||||
|
- Document which config files are templates vs active
|
||||||
|
|
||||||
|
### HIGH PRIORITY CHANGES
|
||||||
|
|
||||||
|
5. **Improve pyproject.toml**
|
||||||
|
```toml
|
||||||
|
[project]
|
||||||
|
name = "fss-mini-rag"
|
||||||
|
version = "2.1.0"
|
||||||
|
description = "Lightweight, educational RAG system"
|
||||||
|
|
||||||
|
[project.scripts]
|
||||||
|
rag-mini = "mini_rag.cli:cli"
|
||||||
|
rag-tui = "mini_rag.tui:main"
|
||||||
|
```
|
||||||
|
|
||||||
|
6. **Consolidate Documentation**
|
||||||
|
- Move `GET_STARTED.md` content into `docs/GETTING_STARTED.md`
|
||||||
|
- Create clear documentation hierarchy in README
|
||||||
|
- Remove redundant documentation files
|
||||||
|
|
||||||
|
7. **Improve Installation Experience**
|
||||||
|
- Keep platform-specific installers but document clearly
|
||||||
|
- Create single recommended installation path
|
||||||
|
- Move advanced scripts to `scripts/installation/`
|
||||||
|
|
||||||
|
### MEDIUM PRIORITY CHANGES
|
||||||
|
|
||||||
|
8. **Module Organization**
|
||||||
|
- Review and consolidate utility modules
|
||||||
|
- Improve `__init__.py` exports
|
||||||
|
- Consider subpackage organization for large modules
|
||||||
|
|
||||||
|
9. **Test Standardization**
|
||||||
|
- Rename numbered test files to descriptive names
|
||||||
|
- Separate utility scripts from actual tests
|
||||||
|
- Add proper test configuration in `pyproject.toml`
|
||||||
|
|
||||||
|
## Implementation Plan
|
||||||
|
|
||||||
|
### Phase 1: Emergency Cleanup (2-3 hours)
|
||||||
|
1. Move large scripts out of root directory
|
||||||
|
2. Remove development artifacts
|
||||||
|
3. Consolidate configuration files
|
||||||
|
4. Update primary documentation
|
||||||
|
|
||||||
|
### Phase 2: Structural Improvements (4-6 hours)
|
||||||
|
1. Improve Python packaging configuration
|
||||||
|
2. Consolidate entry points
|
||||||
|
3. Organize installation scripts
|
||||||
|
4. Standardize test organization
|
||||||
|
|
||||||
|
### Phase 3: Professional Polish (2-4 hours)
|
||||||
|
1. Review and improve module architecture
|
||||||
|
2. Enhance documentation hierarchy
|
||||||
|
3. Add missing standard project files
|
||||||
|
4. Final professional review
|
||||||
|
|
||||||
|
## Impact Assessment
|
||||||
|
|
||||||
|
### Before Changes
|
||||||
|
- **First Impression**: Confused by multiple entry points and cluttered root
|
||||||
|
- **Developer Experience**: Unclear how to contribute or modify
|
||||||
|
- **Professional Credibility**: Damaged by poor organization
|
||||||
|
- **Maintenance Burden**: High due to scattered structure
|
||||||
|
|
||||||
|
### After Changes
|
||||||
|
- **First Impression**: Clean, professional project structure
|
||||||
|
- **Developer Experience**: Clear entry points and logical organization
|
||||||
|
- **Professional Credibility**: Enhanced by following standards
|
||||||
|
- **Maintenance Burden**: Reduced through proper organization
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
|
||||||
|
The FSS-Mini-RAG project has excellent technical merit but is significantly hampered by poor structural organization. The root directory pollution and multiple entry points create unnecessary complexity and damage the professional presentation.
|
||||||
|
|
||||||
|
**Priority Recommendation:** Focus on the Critical Changes first - these will provide the most impact for professional presentation with minimal risk to functionality.
|
||||||
|
|
||||||
|
**Timeline:** The structural issues can be resolved in 1-2 focused sessions without touching the core technical implementation, dramatically improving the project's professional appearance and maintainability.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Analysis completed: August 28, 2025 - FSS-Mini-RAG Project Structure Assessment*
|
||||||
373
docs/security-analysis.md
Normal file
373
docs/security-analysis.md
Normal file
@ -0,0 +1,373 @@
|
|||||||
|
# FSS-Mini-RAG Security Analysis Report
|
||||||
|
**Conducted by: Emma, Authentication Specialist**
|
||||||
|
**Date: 2024-08-28**
|
||||||
|
**Classification: Confidential - For Professional Deployment Review**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Executive Summary
|
||||||
|
|
||||||
|
This comprehensive security audit examines the FSS-Mini-RAG system's defensive posture, identifying vulnerabilities and providing actionable hardening recommendations. The system demonstrates several commendable security practices but requires attention in key areas before professional deployment.
|
||||||
|
|
||||||
|
**Overall Security Rating: MODERATE RISK (Amber)**
|
||||||
|
- ✅ **Strengths**: Good input validation patterns, secure default configurations, appropriate access controls
|
||||||
|
- ⚠️ **Concerns**: Network service exposure, file system access patterns, dependency management
|
||||||
|
- 🔴 **Critical**: Server port management and external service integration security
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Data Security & Privacy Assessment
|
||||||
|
|
||||||
|
### Data Handling Analysis
|
||||||
|
**Status: GOOD with Minor Concerns**
|
||||||
|
|
||||||
|
#### Positive Security Practices:
|
||||||
|
- **Local-First Architecture**: All data processing occurs locally, reducing external attack surface
|
||||||
|
- **No Cloud Dependency**: Embeddings and vector storage remain on-premise
|
||||||
|
- **Temporary File Management**: Proper cleanup patterns observed in chunking operations
|
||||||
|
- **Path Normalisation**: Robust cross-platform path handling prevents directory traversal
|
||||||
|
|
||||||
|
#### Areas of Concern:
|
||||||
|
- **Persistent Storage**: `.mini-rag/` directories store sensitive codebase information
|
||||||
|
- **Index Files**: LanceDB vector files contain searchable representations of source code
|
||||||
|
- **Configuration Files**: YAML configs may contain sensitive connection strings
|
||||||
|
- **Memory Exposure**: Code content held in memory during processing without explicit scrubbing
|
||||||
|
|
||||||
|
#### Recommendations:
|
||||||
|
1. **Implement data classification**: Tag sensitive files during indexing
|
||||||
|
2. **Add encryption at rest**: Encrypt vector databases and configuration files
|
||||||
|
3. **Memory management**: Explicit memory clearing after processing sensitive content
|
||||||
|
4. **Access logging**: Track who accesses which code segments through search
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Input Validation & Sanitization Assessment
|
||||||
|
|
||||||
|
### CLI Input Handling
|
||||||
|
**Status: GOOD**
|
||||||
|
|
||||||
|
#### Robust Validation Observed:
|
||||||
|
```python
|
||||||
|
# Path validation with proper resolution
|
||||||
|
project_path = Path(path).resolve()
|
||||||
|
|
||||||
|
# Type checking and bounds validation
|
||||||
|
@click.option("--top-k", "-k", type=int, default=10)
|
||||||
|
@click.option("--port", type=int, default=7777)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### File Path Security:
|
||||||
|
- **Path Traversal Protection**: Proper use of `Path().resolve()` throughout codebase
|
||||||
|
- **Extension Validation**: File type filtering based on extensions
|
||||||
|
- **Size Limits**: Appropriate file size thresholds implemented
|
||||||
|
|
||||||
|
#### Search Query Processing:
|
||||||
|
**Status: MODERATE RISK**
|
||||||
|
|
||||||
|
**Vulnerabilities Identified:**
|
||||||
|
- **No Query Length Limits**: Potential DoS through excessive query lengths
|
||||||
|
- **Special Character Handling**: Limited sanitization of search terms
|
||||||
|
- **Regex Injection**: Query expansion could be exploited with crafted patterns
|
||||||
|
|
||||||
|
#### Recommendations:
|
||||||
|
1. **Implement query length limits** (max 512 characters)
|
||||||
|
2. **Sanitize search queries** before processing
|
||||||
|
3. **Validate file patterns** in include/exclude configurations
|
||||||
|
4. **Add input encoding validation** for non-ASCII content
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Network Security Assessment
|
||||||
|
|
||||||
|
### Server Implementation Analysis
|
||||||
|
**Status: HIGH RISK - REQUIRES IMMEDIATE ATTENTION**
|
||||||
|
|
||||||
|
#### Critical Security Issues:
|
||||||
|
|
||||||
|
**1. Port Management Vulnerabilities:**
|
||||||
|
```python
|
||||||
|
# CRITICAL: Automatic port cleanup attempts system commands
|
||||||
|
result = subprocess.run(["netstat", "-ano"], capture_output=True, text=True)
|
||||||
|
subprocess.run(["taskkill", "//PID", pid, "//F"], check=False)
|
||||||
|
```
|
||||||
|
**Risk**: Command injection, privilege escalation
|
||||||
|
**Impact**: System compromise possible
|
||||||
|
|
||||||
|
**2. Network Service Exposure:**
|
||||||
|
```python
|
||||||
|
# Binds to localhost but lacks authentication
|
||||||
|
self.socket.bind(("localhost", self.port))
|
||||||
|
self.socket.listen(5)
|
||||||
|
```
|
||||||
|
**Risk**: Unauthorised local access
|
||||||
|
**Impact**: Code exposure to other local processes
|
||||||
|
|
||||||
|
**3. Message Framing Vulnerabilities:**
|
||||||
|
```python
|
||||||
|
# Potential buffer overflow with untrusted length prefix
|
||||||
|
length = int.from_bytes(length_data, "big")
|
||||||
|
chunk = sock.recv(min(65536, length - len(data)))
|
||||||
|
```
|
||||||
|
**Risk**: Memory exhaustion, DoS attacks
|
||||||
|
**Impact**: Service disruption
|
||||||
|
|
||||||
|
#### Recommendations:
|
||||||
|
1. **Implement authentication**: Token-based access control for server connections
|
||||||
|
2. **Remove automatic process killing**: Replace with safe port checking
|
||||||
|
3. **Add connection limits**: Rate limiting and concurrent connection controls
|
||||||
|
4. **Message size validation**: Strict limits on incoming message sizes
|
||||||
|
5. **TLS encryption**: Encrypt local communications
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. External Service Integration Security
|
||||||
|
|
||||||
|
### Ollama Integration Analysis
|
||||||
|
**Status: MODERATE RISK**
|
||||||
|
|
||||||
|
#### Security Concerns:
|
||||||
|
```python
|
||||||
|
# Unvalidated external service calls
|
||||||
|
response = requests.get(f"{self.base_url}/api/tags", timeout=5)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Vulnerabilities:**
|
||||||
|
- **No certificate validation** for HTTPS connections
|
||||||
|
- **Trust boundary violation**: Implicit trust of Ollama responses
|
||||||
|
- **Configuration injection**: User-controlled host parameters
|
||||||
|
|
||||||
|
#### LLM Service Security:
|
||||||
|
- **Prompt injection risks**: User queries passed directly to LLM
|
||||||
|
- **Data leakage potential**: Code content sent to external models
|
||||||
|
- **Response validation**: Limited validation of LLM outputs
|
||||||
|
|
||||||
|
#### Recommendations:
|
||||||
|
1. **Certificate validation**: Enforce TLS certificate checking
|
||||||
|
2. **Response validation**: Sanitize and validate all external responses
|
||||||
|
3. **Connection timeouts**: Implement aggressive timeouts for external calls
|
||||||
|
4. **Host validation**: Whitelist allowed connection targets
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. File System Security Assessment
|
||||||
|
|
||||||
|
### File Access Patterns
|
||||||
|
**Status: GOOD with Recommendations**
|
||||||
|
|
||||||
|
#### Positive Practices:
|
||||||
|
- **Appropriate file permissions**: Uses standard Python file operations
|
||||||
|
- **Pattern-based exclusions**: Sensible default exclude patterns
|
||||||
|
- **Size-based filtering**: Protection against processing oversized files
|
||||||
|
|
||||||
|
#### Areas for Improvement:
|
||||||
|
```python
|
||||||
|
# File enumeration could be restricted further
|
||||||
|
all_files = list(project_path.rglob("*"))
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Recommendations:
|
||||||
|
1. **Implement file access logging**: Track which files are indexed/searched
|
||||||
|
2. **Add symlink protection**: Prevent symlink-based directory traversal
|
||||||
|
3. **Enhanced file type validation**: Magic number checking beyond extensions
|
||||||
|
4. **Temporary file security**: Secure creation and cleanup of temp files
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Configuration Security Assessment
|
||||||
|
|
||||||
|
### YAML Configuration Handling
|
||||||
|
**Status: MODERATE RISK**
|
||||||
|
|
||||||
|
#### Security Issues:
|
||||||
|
```python
|
||||||
|
# YAML parsing without safe mode enforcement
|
||||||
|
data = yaml.safe_load(f)
|
||||||
|
```
|
||||||
|
**Note**: Uses `safe_load` (good) but lacks validation
|
||||||
|
|
||||||
|
#### Configuration Vulnerabilities:
|
||||||
|
- **Path injection**: User-controlled paths in configuration
|
||||||
|
- **Service endpoints**: External service URLs configurable
|
||||||
|
- **Model specifications**: Potential for malicious model references
|
||||||
|
|
||||||
|
#### Recommendations:
|
||||||
|
1. **Configuration validation schema**: Implement strict YAML schema validation
|
||||||
|
2. **Whitelist allowed values**: Restrict configuration options to safe choices
|
||||||
|
3. **Configuration encryption**: Encrypt sensitive configuration values
|
||||||
|
4. **Read-only configurations**: Prevent runtime modification of security settings
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Dependencies & Supply Chain Security
|
||||||
|
|
||||||
|
### Dependency Analysis
|
||||||
|
**Status: MODERATE RISK**
|
||||||
|
|
||||||
|
#### Current Dependencies:
|
||||||
|
```
|
||||||
|
lancedb>=0.5.0 # Vector database - moderate risk
|
||||||
|
requests>=2.28.0 # HTTP client - well-maintained
|
||||||
|
click>=8.1.0 # CLI framework - secure
|
||||||
|
PyYAML>=6.0.0 # YAML parsing - recent versions secure
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Security Concerns:
|
||||||
|
- **Version pinning**: Uses minimum versions (>=) allowing potentially vulnerable updates
|
||||||
|
- **Transitive dependencies**: No analysis of indirect dependencies
|
||||||
|
- **Supply chain attacks**: No dependency integrity verification
|
||||||
|
|
||||||
|
#### Recommendations:
|
||||||
|
1. **Pin exact versions**: Use `==` instead of `>=` for production deployments
|
||||||
|
2. **Dependency scanning**: Implement automated vulnerability scanning
|
||||||
|
3. **Integrity verification**: Use pip hash checking for critical dependencies
|
||||||
|
4. **Regular updates**: Establish dependency update and testing procedures
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Logging & Monitoring Security
|
||||||
|
|
||||||
|
### Current Logging Analysis
|
||||||
|
**Status: REQUIRES IMPROVEMENT**
|
||||||
|
|
||||||
|
#### Logging Practices:
|
||||||
|
```python
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
# Basic logging without security context
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Security Gaps:
|
||||||
|
- **No security event logging**: Access attempts not recorded
|
||||||
|
- **Information leakage**: Debug logs may expose sensitive paths
|
||||||
|
- **No audit trail**: Cannot track security-relevant events
|
||||||
|
- **Log injection**: Potential for log poisoning through user inputs
|
||||||
|
|
||||||
|
#### Recommendations:
|
||||||
|
1. **Security event logging**: Log all authentication attempts, access patterns
|
||||||
|
2. **Sanitize log inputs**: Prevent log injection attacks
|
||||||
|
3. **Structured logging**: Use structured formats for security analysis
|
||||||
|
4. **Log rotation and retention**: Implement secure log management
|
||||||
|
5. **Monitoring integration**: Connect to security monitoring systems
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. System Hardening Recommendations
|
||||||
|
|
||||||
|
### Priority 1 (Critical - Implement Immediately):
|
||||||
|
|
||||||
|
1. **Server Authentication**:
|
||||||
|
```python
|
||||||
|
# Add token-based authentication
|
||||||
|
def authenticate_request(self, token):
|
||||||
|
return hmac.compare_digest(token, self.expected_token)
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Safe Port Management**:
|
||||||
|
```python
|
||||||
|
# Remove dangerous subprocess calls
|
||||||
|
# Use socket.SO_REUSEADDR properly instead
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Input Validation Framework**:
|
||||||
|
```python
|
||||||
|
def validate_search_query(query: str) -> str:
|
||||||
|
if len(query) > 512:
|
||||||
|
raise ValueError("Query too long")
|
||||||
|
return re.sub(r'[^\w\s\-\.]', '', query)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Priority 2 (High - Implement Within Sprint):
|
||||||
|
|
||||||
|
4. **Configuration Security**:
|
||||||
|
```python
|
||||||
|
# Implement configuration schema validation
|
||||||
|
# Add encryption for sensitive config values
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Enhanced Logging**:
|
||||||
|
```python
|
||||||
|
# Add security event logging
|
||||||
|
security_logger.info("Search performed", extra={
|
||||||
|
"user": user_id,
|
||||||
|
"query_hash": hashlib.sha256(query.encode()).hexdigest()[:16],
|
||||||
|
"files_accessed": len(results)
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
6. **Dependency Management**:
|
||||||
|
```bash
|
||||||
|
# Pin exact versions in requirements.txt
|
||||||
|
# Implement hash checking
|
||||||
|
```
|
||||||
|
|
||||||
|
### Priority 3 (Medium - Next Release Cycle):
|
||||||
|
|
||||||
|
7. **Data Encryption**: Implement at-rest encryption for vector databases
|
||||||
|
8. **Access Controls**: Role-based access to different code segments
|
||||||
|
9. **Security Monitoring**: Integration with SIEM systems
|
||||||
|
10. **Penetration Testing**: Regular security assessments
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Compliance & Audit Considerations
|
||||||
|
|
||||||
|
### Current Compliance Posture:
|
||||||
|
- **Data Protection**: Local storage reduces GDPR/privacy risks
|
||||||
|
- **Access Logging**: Currently insufficient for audit requirements
|
||||||
|
- **Change Management**: Git-based but lacks security change tracking
|
||||||
|
- **Documentation**: Good code documentation but missing security procedures
|
||||||
|
|
||||||
|
### Recommendations for Compliance:
|
||||||
|
1. **Security documentation**: Create security architecture diagrams
|
||||||
|
2. **Access audit trails**: Implement comprehensive logging
|
||||||
|
3. **Regular security reviews**: Quarterly security assessments
|
||||||
|
4. **Incident response procedures**: Define security incident handling
|
||||||
|
5. **Backup security**: Secure backup and recovery procedures
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Deployment Security Checklist
|
||||||
|
|
||||||
|
### Pre-Deployment Security Requirements:
|
||||||
|
|
||||||
|
- [ ] **Authentication implemented** for server mode
|
||||||
|
- [ ] **Input validation** comprehensive across all entry points
|
||||||
|
- [ ] **Configuration hardening** with schema validation
|
||||||
|
- [ ] **Dependency scanning** completed and vulnerabilities addressed
|
||||||
|
- [ ] **Security logging** implemented and tested
|
||||||
|
- [ ] **TLS/encryption** for network communications
|
||||||
|
- [ ] **File system permissions** properly configured
|
||||||
|
- [ ] **Service account isolation** implemented
|
||||||
|
- [ ] **Monitoring and alerting** configured
|
||||||
|
- [ ] **Backup security** validated
|
||||||
|
|
||||||
|
### Post-Deployment Security Monitoring:
|
||||||
|
|
||||||
|
- [ ] **Regular vulnerability scans** scheduled
|
||||||
|
- [ ] **Log analysis** for security events
|
||||||
|
- [ ] **Dependency update procedures** established
|
||||||
|
- [ ] **Incident response plan** activated
|
||||||
|
- [ ] **Security metrics** tracked and reported
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
|
||||||
|
The FSS-Mini-RAG system demonstrates solid foundational security practices with appropriate local-first architecture and sensible defaults. However, several critical vulnerabilities require immediate attention before professional deployment, particularly around server security and input validation.
|
||||||
|
|
||||||
|
**Primary Action Items:**
|
||||||
|
1. **Implement server authentication** (Critical)
|
||||||
|
2. **Eliminate subprocess security risks** (Critical)
|
||||||
|
3. **Enhanced input validation** (High)
|
||||||
|
4. **Comprehensive security logging** (High)
|
||||||
|
5. **Dependency security hardening** (Medium)
|
||||||
|
|
||||||
|
With these improvements, the system will achieve a **GOOD** security posture suitable for professional deployment environments.
|
||||||
|
|
||||||
|
**Risk Acceptance**: Any deployment without addressing Critical and High priority items should require explicit risk acceptance from senior management.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*This analysis conducted with military precision and British thoroughness. Implementation of recommendations will significantly enhance the system's defensive capabilities whilst maintaining operational effectiveness.*
|
||||||
|
|
||||||
|
**Emma, Authentication Specialist**
|
||||||
|
**Security Clearance: OFFICIAL**
|
||||||
@ -4,14 +4,14 @@ Analyze FSS-Mini-RAG dependencies to determine what's safe to remove.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import ast
|
import ast
|
||||||
import os
|
|
||||||
from pathlib import Path
|
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
def find_imports_in_file(file_path):
|
def find_imports_in_file(file_path):
|
||||||
"""Find all imports in a Python file."""
|
"""Find all imports in a Python file."""
|
||||||
try:
|
try:
|
||||||
with open(file_path, 'r', encoding='utf-8') as f:
|
with open(file_path, "r", encoding="utf-8") as f:
|
||||||
content = f.read()
|
content = f.read()
|
||||||
|
|
||||||
tree = ast.parse(content)
|
tree = ast.parse(content)
|
||||||
@ -20,10 +20,10 @@ def find_imports_in_file(file_path):
|
|||||||
for node in ast.walk(tree):
|
for node in ast.walk(tree):
|
||||||
if isinstance(node, ast.Import):
|
if isinstance(node, ast.Import):
|
||||||
for alias in node.names:
|
for alias in node.names:
|
||||||
imports.add(alias.name.split('.')[0])
|
imports.add(alias.name.split(".")[0])
|
||||||
elif isinstance(node, ast.ImportFrom):
|
elif isinstance(node, ast.ImportFrom):
|
||||||
if node.module:
|
if node.module:
|
||||||
module = node.module.split('.')[0]
|
module = node.module.split(".")[0]
|
||||||
imports.add(module)
|
imports.add(module)
|
||||||
|
|
||||||
return imports
|
return imports
|
||||||
@ -31,6 +31,7 @@ def find_imports_in_file(file_path):
|
|||||||
print(f"Error analyzing {file_path}: {e}")
|
print(f"Error analyzing {file_path}: {e}")
|
||||||
return set()
|
return set()
|
||||||
|
|
||||||
|
|
||||||
def analyze_dependencies():
|
def analyze_dependencies():
|
||||||
"""Analyze all dependencies in the project."""
|
"""Analyze all dependencies in the project."""
|
||||||
project_root = Path(__file__).parent
|
project_root = Path(__file__).parent
|
||||||
@ -85,13 +86,13 @@ def analyze_dependencies():
|
|||||||
print("\n🛡️ Safety Analysis:")
|
print("\n🛡️ Safety Analysis:")
|
||||||
|
|
||||||
# Files imported by __init__.py are definitely needed
|
# Files imported by __init__.py are definitely needed
|
||||||
init_imports = file_imports.get('__init__.py', set())
|
init_imports = file_imports.get("__init__.py", set())
|
||||||
print(f" Core modules (imported by __init__.py): {', '.join(init_imports)}")
|
print(f" Core modules (imported by __init__.py): {', '.join(init_imports)}")
|
||||||
|
|
||||||
# Files not used anywhere might be safe to remove
|
# Files not used anywhere might be safe to remove
|
||||||
unused_files = []
|
unused_files = []
|
||||||
for module in all_modules:
|
for module in all_modules:
|
||||||
if module not in reverse_deps and module != '__init__':
|
if module not in reverse_deps and module != "__init__":
|
||||||
unused_files.append(module)
|
unused_files.append(module)
|
||||||
|
|
||||||
if unused_files:
|
if unused_files:
|
||||||
@ -99,11 +100,14 @@ def analyze_dependencies():
|
|||||||
print(" ❗ Verify these aren't used by CLI or external scripts!")
|
print(" ❗ Verify these aren't used by CLI or external scripts!")
|
||||||
|
|
||||||
# Check CLI usage
|
# Check CLI usage
|
||||||
cli_files = ['cli.py', 'enhanced_cli.py']
|
cli_files = ["cli.py", "enhanced_cli.py"]
|
||||||
for cli_file in cli_files:
|
for cli_file in cli_files:
|
||||||
if cli_file in file_imports:
|
if cli_file in file_imports:
|
||||||
cli_imports = file_imports[cli_file]
|
cli_imports = file_imports[cli_file]
|
||||||
print(f" 📋 {cli_file} imports: {', '.join([imp for imp in cli_imports if imp in all_modules])}")
|
print(
|
||||||
|
f" 📋 {cli_file} imports: {', '.join([imp for imp in cli_imports if imp in all_modules])}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
analyze_dependencies()
|
analyze_dependencies()
|
||||||
@ -5,7 +5,9 @@ Shows how to index a project and search it programmatically.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from mini_rag import ProjectIndexer, CodeSearcher, CodeEmbedder
|
|
||||||
|
from mini_rag import CodeEmbedder, CodeSearcher, ProjectIndexer
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
# Example project path - change this to your project
|
# Example project path - change this to your project
|
||||||
@ -44,7 +46,7 @@ def main():
|
|||||||
"embedding system",
|
"embedding system",
|
||||||
"search implementation",
|
"search implementation",
|
||||||
"file watcher",
|
"file watcher",
|
||||||
"error handling"
|
"error handling",
|
||||||
]
|
]
|
||||||
|
|
||||||
print("\n4. Example searches:")
|
print("\n4. Example searches:")
|
||||||
@ -57,12 +59,13 @@ def main():
|
|||||||
print(f" {i}. {result.file_path.name} (score: {result.score:.3f})")
|
print(f" {i}. {result.file_path.name} (score: {result.score:.3f})")
|
||||||
print(f" Type: {result.chunk_type}")
|
print(f" Type: {result.chunk_type}")
|
||||||
# Show first 60 characters of content
|
# Show first 60 characters of content
|
||||||
content_preview = result.content.replace('\n', ' ')[:60]
|
content_preview = result.content.replace("\n", " ")[:60]
|
||||||
print(f" Preview: {content_preview}...")
|
print(f" Preview: {content_preview}...")
|
||||||
else:
|
else:
|
||||||
print(" No results found")
|
print(" No results found")
|
||||||
|
|
||||||
print("\n=== Example Complete ===")
|
print("\n=== Example Complete ===")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
@ -5,9 +5,10 @@ Analyzes the indexed data to suggest optimal settings.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
from pathlib import Path
|
|
||||||
from collections import defaultdict, Counter
|
|
||||||
import sys
|
import sys
|
||||||
|
from collections import Counter
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
def analyze_project_patterns(manifest_path: Path):
|
def analyze_project_patterns(manifest_path: Path):
|
||||||
"""Analyze project patterns and suggest optimizations."""
|
"""Analyze project patterns and suggest optimizations."""
|
||||||
@ -15,7 +16,7 @@ def analyze_project_patterns(manifest_path: Path):
|
|||||||
with open(manifest_path) as f:
|
with open(manifest_path) as f:
|
||||||
manifest = json.load(f)
|
manifest = json.load(f)
|
||||||
|
|
||||||
files = manifest.get('files', {})
|
files = manifest.get("files", {})
|
||||||
|
|
||||||
print("🔍 FSS-Mini-RAG Smart Tuning Analysis")
|
print("🔍 FSS-Mini-RAG Smart Tuning Analysis")
|
||||||
print("=" * 50)
|
print("=" * 50)
|
||||||
@ -27,11 +28,11 @@ def analyze_project_patterns(manifest_path: Path):
|
|||||||
small_files = []
|
small_files = []
|
||||||
|
|
||||||
for filepath, info in files.items():
|
for filepath, info in files.items():
|
||||||
lang = info.get('language', 'unknown')
|
lang = info.get("language", "unknown")
|
||||||
languages[lang] += 1
|
languages[lang] += 1
|
||||||
|
|
||||||
size = info.get('size', 0)
|
size = info.get("size", 0)
|
||||||
chunks = info.get('chunks', 1)
|
chunks = info.get("chunks", 1)
|
||||||
|
|
||||||
chunk_efficiency.append(chunks / max(1, size / 1000)) # chunks per KB
|
chunk_efficiency.append(chunks / max(1, size / 1000)) # chunks per KB
|
||||||
|
|
||||||
@ -42,65 +43,70 @@ def analyze_project_patterns(manifest_path: Path):
|
|||||||
|
|
||||||
# Analysis results
|
# Analysis results
|
||||||
total_files = len(files)
|
total_files = len(files)
|
||||||
total_chunks = sum(info.get('chunks', 1) for info in files.values())
|
total_chunks = sum(info.get("chunks", 1) for info in files.values())
|
||||||
avg_chunks_per_file = total_chunks / max(1, total_files)
|
avg_chunks_per_file = total_chunks / max(1, total_files)
|
||||||
|
|
||||||
print(f"📊 Current Stats:")
|
print("📊 Current Stats:")
|
||||||
print(f" Files: {total_files}")
|
print(f" Files: {total_files}")
|
||||||
print(f" Chunks: {total_chunks}")
|
print(f" Chunks: {total_chunks}")
|
||||||
print(f" Avg chunks/file: {avg_chunks_per_file:.1f}")
|
print(f" Avg chunks/file: {avg_chunks_per_file:.1f}")
|
||||||
|
|
||||||
print(f"\n🗂️ Language Distribution:")
|
print("\n🗂️ Language Distribution:")
|
||||||
for lang, count in languages.most_common(10):
|
for lang, count in languages.most_common(10):
|
||||||
pct = 100 * count / total_files
|
pct = 100 * count / total_files
|
||||||
print(f" {lang}: {count} files ({pct:.1f}%)")
|
print(f" {lang}: {count} files ({pct:.1f}%)")
|
||||||
|
|
||||||
print(f"\n💡 Smart Optimization Suggestions:")
|
print("\n💡 Smart Optimization Suggestions:")
|
||||||
|
|
||||||
# Suggestion 1: Language-specific chunking
|
# Suggestion 1: Language-specific chunking
|
||||||
if languages['python'] > 10:
|
if languages["python"] > 10:
|
||||||
print(f"✨ Python Optimization:")
|
print("✨ Python Optimization:")
|
||||||
print(f" - Use function-level chunking (detected {languages['python']} Python files)")
|
print(
|
||||||
print(f" - Increase chunk size to 3000 chars for Python (better context)")
|
f" - Use function-level chunking (detected {languages['python']} Python files)"
|
||||||
|
)
|
||||||
|
print(" - Increase chunk size to 3000 chars for Python (better context)")
|
||||||
|
|
||||||
if languages['markdown'] > 5:
|
if languages["markdown"] > 5:
|
||||||
print(f"✨ Markdown Optimization:")
|
print("✨ Markdown Optimization:")
|
||||||
print(f" - Use header-based chunking (detected {languages['markdown']} MD files)")
|
print(f" - Use header-based chunking (detected {languages['markdown']} MD files)")
|
||||||
print(f" - Keep sections together for better search relevance")
|
print(" - Keep sections together for better search relevance")
|
||||||
|
|
||||||
if languages['json'] > 20:
|
if languages["json"] > 20:
|
||||||
print(f"✨ JSON Optimization:")
|
print("✨ JSON Optimization:")
|
||||||
print(f" - Consider object-level chunking (detected {languages['json']} JSON files)")
|
print(f" - Consider object-level chunking (detected {languages['json']} JSON files)")
|
||||||
print(f" - Might want to exclude large config JSONs")
|
print(" - Might want to exclude large config JSONs")
|
||||||
|
|
||||||
# Suggestion 2: File size optimization
|
# Suggestion 2: File size optimization
|
||||||
if large_files:
|
if large_files:
|
||||||
print(f"\n📈 Large File Optimization:")
|
print("\n📈 Large File Optimization:")
|
||||||
print(f" Found {len(large_files)} files >10KB:")
|
print(f" Found {len(large_files)} files >10KB:")
|
||||||
for filepath, size, chunks in sorted(large_files, key=lambda x: x[1], reverse=True)[:3]:
|
for filepath, size, chunks in sorted(large_files, key=lambda x: x[1], reverse=True)[
|
||||||
|
:3
|
||||||
|
]:
|
||||||
kb = size / 1024
|
kb = size / 1024
|
||||||
print(f" - {filepath}: {kb:.1f}KB → {chunks} chunks")
|
print(f" - {filepath}: {kb:.1f}KB → {chunks} chunks")
|
||||||
if len(large_files) > 5:
|
if len(large_files) > 5:
|
||||||
print(f" 💡 Consider streaming threshold: 5KB (current: 1MB)")
|
print(" 💡 Consider streaming threshold: 5KB (current: 1MB)")
|
||||||
|
|
||||||
if small_files and len(small_files) > total_files * 0.3:
|
if small_files and len(small_files) > total_files * 0.3:
|
||||||
print(f"\n📉 Small File Optimization:")
|
print("\n📉 Small File Optimization:")
|
||||||
print(f" {len(small_files)} files <500B might not need chunking")
|
print(f" {len(small_files)} files <500B might not need chunking")
|
||||||
print(f" 💡 Consider: combine small files or skip tiny ones")
|
print(" 💡 Consider: combine small files or skip tiny ones")
|
||||||
|
|
||||||
# Suggestion 3: Search optimization
|
# Suggestion 3: Search optimization
|
||||||
avg_efficiency = sum(chunk_efficiency) / len(chunk_efficiency)
|
avg_efficiency = sum(chunk_efficiency) / len(chunk_efficiency)
|
||||||
print(f"\n🔍 Search Optimization:")
|
print("\n🔍 Search Optimization:")
|
||||||
if avg_efficiency < 0.5:
|
if avg_efficiency < 0.5:
|
||||||
print(f" 💡 Chunks are large relative to files - consider smaller chunks")
|
print(" 💡 Chunks are large relative to files - consider smaller chunks")
|
||||||
print(f" 💡 Current: {avg_chunks_per_file:.1f} chunks/file, try 2-3 chunks/file")
|
print(f" 💡 Current: {avg_chunks_per_file:.1f} chunks/file, try 2-3 chunks/file")
|
||||||
elif avg_efficiency > 2:
|
elif avg_efficiency > 2:
|
||||||
print(f" 💡 Many small chunks - consider larger chunk size")
|
print(" 💡 Many small chunks - consider larger chunk size")
|
||||||
print(f" 💡 Reduce chunk overhead with 2000-4000 char chunks")
|
print(" 💡 Reduce chunk overhead with 2000-4000 char chunks")
|
||||||
|
|
||||||
# Suggestion 4: Smart defaults
|
# Suggestion 4: Smart defaults
|
||||||
print(f"\n⚙️ Recommended Config Updates:")
|
print("\n⚙️ Recommended Config Updates:")
|
||||||
print(f"""{{
|
print(
|
||||||
|
"""{{
|
||||||
"chunking": {{
|
"chunking": {{
|
||||||
"max_size": {3000 if languages['python'] > languages['markdown'] else 2000},
|
"max_size": {3000 if languages['python'] > languages['markdown'] else 2000},
|
||||||
"min_size": 200,
|
"min_size": 200,
|
||||||
@ -115,7 +121,9 @@ def analyze_project_patterns(manifest_path: Path):
|
|||||||
"skip_small_files": {500 if len(small_files) > total_files * 0.3 else 0},
|
"skip_small_files": {500 if len(small_files) > total_files * 0.3 else 0},
|
||||||
"streaming_threshold_kb": {5 if len(large_files) > 5 else 1024}
|
"streaming_threshold_kb": {5 if len(large_files) > 5 else 1024}
|
||||||
}}
|
}}
|
||||||
}}""")
|
}}"""
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
if len(sys.argv) != 2:
|
if len(sys.argv) != 2:
|
||||||
|
|||||||
@ -7,30 +7,16 @@ Designed for portability, efficiency, and simplicity across projects and compute
|
|||||||
|
|
||||||
__version__ = "2.1.0"
|
__version__ = "2.1.0"
|
||||||
|
|
||||||
from .ollama_embeddings import OllamaEmbedder as CodeEmbedder
|
|
||||||
from .chunker import CodeChunker
|
from .chunker import CodeChunker
|
||||||
from .indexer import ProjectIndexer
|
from .indexer import ProjectIndexer
|
||||||
|
from .ollama_embeddings import OllamaEmbedder as CodeEmbedder
|
||||||
from .search import CodeSearcher
|
from .search import CodeSearcher
|
||||||
from .watcher import FileWatcher
|
from .watcher import FileWatcher
|
||||||
|
|
||||||
# Auto-update system (graceful import for legacy versions)
|
__all__ = [
|
||||||
try:
|
|
||||||
from .updater import UpdateChecker, check_for_updates, get_updater
|
|
||||||
__all__ = [
|
|
||||||
"CodeEmbedder",
|
"CodeEmbedder",
|
||||||
"CodeChunker",
|
"CodeChunker",
|
||||||
"ProjectIndexer",
|
"ProjectIndexer",
|
||||||
"CodeSearcher",
|
"CodeSearcher",
|
||||||
"FileWatcher",
|
"FileWatcher",
|
||||||
"UpdateChecker",
|
]
|
||||||
"check_for_updates",
|
|
||||||
"get_updater",
|
|
||||||
]
|
|
||||||
except ImportError:
|
|
||||||
__all__ = [
|
|
||||||
"CodeEmbedder",
|
|
||||||
"CodeChunker",
|
|
||||||
"ProjectIndexer",
|
|
||||||
"CodeSearcher",
|
|
||||||
"FileWatcher",
|
|
||||||
]
|
|
||||||
|
|||||||
@ -2,5 +2,5 @@
|
|||||||
|
|
||||||
from .cli import cli
|
from .cli import cli
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == "__main__":
|
||||||
cli()
|
cli()
|
||||||
@ -3,22 +3,23 @@ Auto-optimizer for FSS-Mini-RAG.
|
|||||||
Automatically tunes settings based on usage patterns.
|
Automatically tunes settings based on usage patterns.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from pathlib import Path
|
|
||||||
import json
|
import json
|
||||||
from typing import Dict, Any, List
|
|
||||||
from collections import Counter
|
|
||||||
import logging
|
import logging
|
||||||
|
from collections import Counter
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Dict
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class AutoOptimizer:
|
class AutoOptimizer:
|
||||||
"""Automatically optimizes RAG settings based on project patterns."""
|
"""Automatically optimizes RAG settings based on project patterns."""
|
||||||
|
|
||||||
def __init__(self, project_path: Path):
|
def __init__(self, project_path: Path):
|
||||||
self.project_path = project_path
|
self.project_path = project_path
|
||||||
self.rag_dir = project_path / '.mini-rag'
|
self.rag_dir = project_path / ".mini-rag"
|
||||||
self.config_path = self.rag_dir / 'config.json'
|
self.config_path = self.rag_dir / "config.json"
|
||||||
self.manifest_path = self.rag_dir / 'manifest.json'
|
self.manifest_path = self.rag_dir / "manifest.json"
|
||||||
|
|
||||||
def analyze_and_optimize(self) -> Dict[str, Any]:
|
def analyze_and_optimize(self) -> Dict[str, Any]:
|
||||||
"""Analyze current patterns and auto-optimize settings."""
|
"""Analyze current patterns and auto-optimize settings."""
|
||||||
@ -37,23 +38,23 @@ class AutoOptimizer:
|
|||||||
optimizations = self._generate_optimizations(analysis)
|
optimizations = self._generate_optimizations(analysis)
|
||||||
|
|
||||||
# Apply optimizations if beneficial
|
# Apply optimizations if beneficial
|
||||||
if optimizations['confidence'] > 0.7:
|
if optimizations["confidence"] > 0.7:
|
||||||
self._apply_optimizations(optimizations)
|
self._apply_optimizations(optimizations)
|
||||||
return {
|
return {
|
||||||
"status": "optimized",
|
"status": "optimized",
|
||||||
"changes": optimizations['changes'],
|
"changes": optimizations["changes"],
|
||||||
"expected_improvement": optimizations['expected_improvement']
|
"expected_improvement": optimizations["expected_improvement"],
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
return {
|
return {
|
||||||
"status": "no_changes_needed",
|
"status": "no_changes_needed",
|
||||||
"analysis": analysis,
|
"analysis": analysis,
|
||||||
"confidence": optimizations['confidence']
|
"confidence": optimizations["confidence"],
|
||||||
}
|
}
|
||||||
|
|
||||||
def _analyze_patterns(self, manifest: Dict[str, Any]) -> Dict[str, Any]:
|
def _analyze_patterns(self, manifest: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""Analyze current indexing patterns."""
|
"""Analyze current indexing patterns."""
|
||||||
files = manifest.get('files', {})
|
files = manifest.get("files", {})
|
||||||
|
|
||||||
# Language distribution
|
# Language distribution
|
||||||
languages = Counter()
|
languages = Counter()
|
||||||
@ -61,11 +62,11 @@ class AutoOptimizer:
|
|||||||
chunk_ratios = []
|
chunk_ratios = []
|
||||||
|
|
||||||
for filepath, info in files.items():
|
for filepath, info in files.items():
|
||||||
lang = info.get('language', 'unknown')
|
lang = info.get("language", "unknown")
|
||||||
languages[lang] += 1
|
languages[lang] += 1
|
||||||
|
|
||||||
size = info.get('size', 0)
|
size = info.get("size", 0)
|
||||||
chunks = info.get('chunks', 1)
|
chunks = info.get("chunks", 1)
|
||||||
|
|
||||||
sizes.append(size)
|
sizes.append(size)
|
||||||
chunk_ratios.append(chunks / max(1, size / 1000)) # chunks per KB
|
chunk_ratios.append(chunks / max(1, size / 1000)) # chunks per KB
|
||||||
@ -74,13 +75,13 @@ class AutoOptimizer:
|
|||||||
avg_size = sum(sizes) / len(sizes) if sizes else 1000
|
avg_size = sum(sizes) / len(sizes) if sizes else 1000
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'languages': dict(languages.most_common()),
|
"languages": dict(languages.most_common()),
|
||||||
'total_files': len(files),
|
"total_files": len(files),
|
||||||
'total_chunks': sum(info.get('chunks', 1) for info in files.values()),
|
"total_chunks": sum(info.get("chunks", 1) for info in files.values()),
|
||||||
'avg_chunk_ratio': avg_chunk_ratio,
|
"avg_chunk_ratio": avg_chunk_ratio,
|
||||||
'avg_file_size': avg_size,
|
"avg_file_size": avg_size,
|
||||||
'large_files': sum(1 for s in sizes if s > 10000),
|
"large_files": sum(1 for s in sizes if s > 10000),
|
||||||
'small_files': sum(1 for s in sizes if s < 500)
|
"small_files": sum(1 for s in sizes if s < 500),
|
||||||
}
|
}
|
||||||
|
|
||||||
def _generate_optimizations(self, analysis: Dict[str, Any]) -> Dict[str, Any]:
|
def _generate_optimizations(self, analysis: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
@ -90,49 +91,51 @@ class AutoOptimizer:
|
|||||||
expected_improvement = 0
|
expected_improvement = 0
|
||||||
|
|
||||||
# Optimize chunking based on dominant language
|
# Optimize chunking based on dominant language
|
||||||
languages = analysis['languages']
|
languages = analysis["languages"]
|
||||||
if languages:
|
if languages:
|
||||||
dominant_lang, count = list(languages.items())[0]
|
dominant_lang, count = list(languages.items())[0]
|
||||||
lang_pct = count / analysis['total_files']
|
lang_pct = count / analysis["total_files"]
|
||||||
|
|
||||||
if lang_pct > 0.3: # Dominant language >30%
|
if lang_pct > 0.3: # Dominant language >30%
|
||||||
if dominant_lang == 'python' and analysis['avg_chunk_ratio'] < 1.5:
|
if dominant_lang == "python" and analysis["avg_chunk_ratio"] < 1.5:
|
||||||
changes.append("Increase Python chunk size to 3000 for better function context")
|
changes.append(
|
||||||
|
"Increase Python chunk size to 3000 for better function context"
|
||||||
|
)
|
||||||
confidence += 0.2
|
confidence += 0.2
|
||||||
expected_improvement += 15
|
expected_improvement += 15
|
||||||
|
|
||||||
elif dominant_lang == 'markdown' and analysis['avg_chunk_ratio'] < 1.2:
|
elif dominant_lang == "markdown" and analysis["avg_chunk_ratio"] < 1.2:
|
||||||
changes.append("Use header-based chunking for Markdown files")
|
changes.append("Use header-based chunking for Markdown files")
|
||||||
confidence += 0.15
|
confidence += 0.15
|
||||||
expected_improvement += 10
|
expected_improvement += 10
|
||||||
|
|
||||||
# Optimize for large files
|
# Optimize for large files
|
||||||
if analysis['large_files'] > 5:
|
if analysis["large_files"] > 5:
|
||||||
changes.append("Reduce streaming threshold to 5KB for better large file handling")
|
changes.append("Reduce streaming threshold to 5KB for better large file handling")
|
||||||
confidence += 0.1
|
confidence += 0.1
|
||||||
expected_improvement += 8
|
expected_improvement += 8
|
||||||
|
|
||||||
# Optimize chunk ratio
|
# Optimize chunk ratio
|
||||||
if analysis['avg_chunk_ratio'] < 1.0:
|
if analysis["avg_chunk_ratio"] < 1.0:
|
||||||
changes.append("Reduce chunk size for more granular search results")
|
changes.append("Reduce chunk size for more granular search results")
|
||||||
confidence += 0.15
|
confidence += 0.15
|
||||||
expected_improvement += 12
|
expected_improvement += 12
|
||||||
elif analysis['avg_chunk_ratio'] > 3.0:
|
elif analysis["avg_chunk_ratio"] > 3.0:
|
||||||
changes.append("Increase chunk size to reduce overhead")
|
changes.append("Increase chunk size to reduce overhead")
|
||||||
confidence += 0.1
|
confidence += 0.1
|
||||||
expected_improvement += 5
|
expected_improvement += 5
|
||||||
|
|
||||||
# Skip tiny files optimization
|
# Skip tiny files optimization
|
||||||
small_file_pct = analysis['small_files'] / analysis['total_files']
|
small_file_pct = analysis["small_files"] / analysis["total_files"]
|
||||||
if small_file_pct > 0.3:
|
if small_file_pct > 0.3:
|
||||||
changes.append("Skip files smaller than 300 bytes to improve focus")
|
changes.append("Skip files smaller than 300 bytes to improve focus")
|
||||||
confidence += 0.1
|
confidence += 0.1
|
||||||
expected_improvement += 3
|
expected_improvement += 3
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'changes': changes,
|
"changes": changes,
|
||||||
'confidence': min(confidence, 1.0),
|
"confidence": min(confidence, 1.0),
|
||||||
'expected_improvement': expected_improvement
|
"expected_improvement": expected_improvement,
|
||||||
}
|
}
|
||||||
|
|
||||||
def _apply_optimizations(self, optimizations: Dict[str, Any]):
|
def _apply_optimizations(self, optimizations: Dict[str, Any]):
|
||||||
@ -145,35 +148,35 @@ class AutoOptimizer:
|
|||||||
else:
|
else:
|
||||||
config = self._get_default_config()
|
config = self._get_default_config()
|
||||||
|
|
||||||
changes = optimizations['changes']
|
changes = optimizations["changes"]
|
||||||
|
|
||||||
# Apply changes based on recommendations
|
# Apply changes based on recommendations
|
||||||
for change in changes:
|
for change in changes:
|
||||||
if "Python chunk size to 3000" in change:
|
if "Python chunk size to 3000" in change:
|
||||||
config.setdefault('chunking', {})['max_size'] = 3000
|
config.setdefault("chunking", {})["max_size"] = 3000
|
||||||
|
|
||||||
elif "header-based chunking" in change:
|
elif "header-based chunking" in change:
|
||||||
config.setdefault('chunking', {})['strategy'] = 'header'
|
config.setdefault("chunking", {})["strategy"] = "header"
|
||||||
|
|
||||||
elif "streaming threshold to 5KB" in change:
|
elif "streaming threshold to 5KB" in change:
|
||||||
config.setdefault('streaming', {})['threshold_bytes'] = 5120
|
config.setdefault("streaming", {})["threshold_bytes"] = 5120
|
||||||
|
|
||||||
elif "Reduce chunk size" in change:
|
elif "Reduce chunk size" in change:
|
||||||
current_size = config.get('chunking', {}).get('max_size', 2000)
|
current_size = config.get("chunking", {}).get("max_size", 2000)
|
||||||
config.setdefault('chunking', {})['max_size'] = max(1500, current_size - 500)
|
config.setdefault("chunking", {})["max_size"] = max(1500, current_size - 500)
|
||||||
|
|
||||||
elif "Increase chunk size" in change:
|
elif "Increase chunk size" in change:
|
||||||
current_size = config.get('chunking', {}).get('max_size', 2000)
|
current_size = config.get("chunking", {}).get("max_size", 2000)
|
||||||
config.setdefault('chunking', {})['max_size'] = min(4000, current_size + 500)
|
config.setdefault("chunking", {})["max_size"] = min(4000, current_size + 500)
|
||||||
|
|
||||||
elif "Skip files smaller" in change:
|
elif "Skip files smaller" in change:
|
||||||
config.setdefault('files', {})['min_file_size'] = 300
|
config.setdefault("files", {})["min_file_size"] = 300
|
||||||
|
|
||||||
# Save optimized config
|
# Save optimized config
|
||||||
config['_auto_optimized'] = True
|
config["_auto_optimized"] = True
|
||||||
config['_optimization_timestamp'] = json.dumps(None, default=str)
|
config["_optimization_timestamp"] = json.dumps(None, default=str)
|
||||||
|
|
||||||
with open(self.config_path, 'w') as f:
|
with open(self.config_path, "w") as f:
|
||||||
json.dump(config, f, indent=2)
|
json.dump(config, f, indent=2)
|
||||||
|
|
||||||
logger.info(f"Applied {len(changes)} optimizations to {self.config_path}")
|
logger.info(f"Applied {len(changes)} optimizations to {self.config_path}")
|
||||||
@ -181,16 +184,7 @@ class AutoOptimizer:
|
|||||||
def _get_default_config(self) -> Dict[str, Any]:
|
def _get_default_config(self) -> Dict[str, Any]:
|
||||||
"""Get default configuration."""
|
"""Get default configuration."""
|
||||||
return {
|
return {
|
||||||
"chunking": {
|
"chunking": {"max_size": 2000, "min_size": 150, "strategy": "semantic"},
|
||||||
"max_size": 2000,
|
"streaming": {"enabled": True, "threshold_bytes": 1048576},
|
||||||
"min_size": 150,
|
"files": {"min_file_size": 50},
|
||||||
"strategy": "semantic"
|
|
||||||
},
|
|
||||||
"streaming": {
|
|
||||||
"enabled": True,
|
|
||||||
"threshold_bytes": 1048576
|
|
||||||
},
|
|
||||||
"files": {
|
|
||||||
"min_file_size": 50
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
312
mini_rag/cli.py
312
mini_rag/cli.py
@ -3,57 +3,55 @@ Command-line interface for Mini RAG system.
|
|||||||
Beautiful, intuitive, and highly effective.
|
Beautiful, intuitive, and highly effective.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import click
|
import logging
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
import logging
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
# Fix Windows console for proper emoji/Unicode support
|
import click
|
||||||
from .windows_console_fix import fix_windows_console
|
|
||||||
fix_windows_console()
|
|
||||||
|
|
||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
from rich.table import Table
|
|
||||||
from rich.progress import Progress, SpinnerColumn, TextColumn
|
|
||||||
from rich.logging import RichHandler
|
from rich.logging import RichHandler
|
||||||
from rich.syntax import Syntax
|
|
||||||
from rich.panel import Panel
|
from rich.panel import Panel
|
||||||
from rich import print as rprint
|
from rich.progress import Progress, SpinnerColumn, TextColumn
|
||||||
|
from rich.syntax import Syntax
|
||||||
|
from rich.table import Table
|
||||||
|
|
||||||
from .indexer import ProjectIndexer
|
from .indexer import ProjectIndexer
|
||||||
from .search import CodeSearcher
|
|
||||||
from .watcher import FileWatcher
|
|
||||||
from .non_invasive_watcher import NonInvasiveFileWatcher
|
from .non_invasive_watcher import NonInvasiveFileWatcher
|
||||||
from .ollama_embeddings import OllamaEmbedder as CodeEmbedder
|
from .ollama_embeddings import OllamaEmbedder as CodeEmbedder
|
||||||
from .chunker import CodeChunker
|
|
||||||
from .performance import get_monitor
|
from .performance import get_monitor
|
||||||
from .server import RAGClient
|
from .search import CodeSearcher
|
||||||
from .server import RAGServer, RAGClient, start_server
|
from .server import RAGClient, start_server
|
||||||
|
from .windows_console_fix import fix_windows_console
|
||||||
|
|
||||||
|
# Fix Windows console for proper emoji/Unicode support
|
||||||
|
fix_windows_console()
|
||||||
|
|
||||||
# Set up logging
|
# Set up logging
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
level=logging.INFO,
|
level=logging.INFO,
|
||||||
format="%(message)s",
|
format="%(message)s",
|
||||||
handlers=[RichHandler(rich_tracebacks=True)]
|
handlers=[RichHandler(rich_tracebacks=True)],
|
||||||
)
|
)
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
console = Console()
|
console = Console()
|
||||||
|
|
||||||
|
|
||||||
@click.group()
|
@click.group()
|
||||||
@click.option('--verbose', '-v', is_flag=True, help='Enable verbose logging')
|
@click.option("--verbose", "-v", is_flag=True, help="Enable verbose logging")
|
||||||
@click.option('--quiet', '-q', is_flag=True, help='Suppress output')
|
@click.option("--quiet", "-q", is_flag=True, help="Suppress output")
|
||||||
def cli(verbose: bool, quiet: bool):
|
def cli(verbose: bool, quiet: bool):
|
||||||
"""
|
"""
|
||||||
Mini RAG - Fast semantic code search that actually works.
|
Mini RAG - Fast semantic code search that actually works.
|
||||||
|
|
||||||
A local RAG system for improving the development environment's grounding capabilities.
|
A local RAG system for improving the development environment's grounding
|
||||||
|
capabilities.
|
||||||
Indexes your codebase and enables lightning-fast semantic search.
|
Indexes your codebase and enables lightning-fast semantic search.
|
||||||
"""
|
"""
|
||||||
# Check virtual environment
|
# Check virtual environment
|
||||||
from .venv_checker import check_and_warn_venv
|
from .venv_checker import check_and_warn_venv
|
||||||
|
|
||||||
check_and_warn_venv("rag-mini", force_exit=False)
|
check_and_warn_venv("rag-mini", force_exit=False)
|
||||||
|
|
||||||
if verbose:
|
if verbose:
|
||||||
@ -63,14 +61,16 @@ def cli(verbose: bool, quiet: bool):
|
|||||||
|
|
||||||
|
|
||||||
@cli.command()
|
@cli.command()
|
||||||
@click.option('--path', '-p', type=click.Path(exists=True), default='.',
|
@click.option(
|
||||||
help='Project path to index')
|
"--path",
|
||||||
@click.option('--force', '-f', is_flag=True,
|
"-p",
|
||||||
help='Force reindex all files')
|
type=click.Path(exists=True),
|
||||||
@click.option('--reindex', '-r', is_flag=True,
|
default=".",
|
||||||
help='Force complete reindex (same as --force)')
|
help="Project path to index",
|
||||||
@click.option('--model', '-m', type=str, default=None,
|
)
|
||||||
help='Embedding model to use')
|
@click.option("--force", "-", is_flag=True, help="Force reindex all files")
|
||||||
|
@click.option("--reindex", "-r", is_flag=True, help="Force complete reindex (same as --force)")
|
||||||
|
@click.option("--model", "-m", type=str, default=None, help="Embedding model to use")
|
||||||
def init(path: str, force: bool, reindex: bool, model: Optional[str]):
|
def init(path: str, force: bool, reindex: bool, model: Optional[str]):
|
||||||
"""Initialize RAG index for a project."""
|
"""Initialize RAG index for a project."""
|
||||||
project_path = Path(path).resolve()
|
project_path = Path(path).resolve()
|
||||||
@ -78,7 +78,7 @@ def init(path: str, force: bool, reindex: bool, model: Optional[str]):
|
|||||||
console.print(f"\n[bold cyan]Initializing Mini RAG for:[/bold cyan] {project_path}\n")
|
console.print(f"\n[bold cyan]Initializing Mini RAG for:[/bold cyan] {project_path}\n")
|
||||||
|
|
||||||
# Check if already initialized
|
# Check if already initialized
|
||||||
rag_dir = project_path / '.mini-rag'
|
rag_dir = project_path / ".mini-rag"
|
||||||
force_reindex = force or reindex
|
force_reindex = force or reindex
|
||||||
if rag_dir.exists() and not force_reindex:
|
if rag_dir.exists() and not force_reindex:
|
||||||
console.print("[yellow][/yellow] Project already initialized!")
|
console.print("[yellow][/yellow] Project already initialized!")
|
||||||
@ -92,10 +92,10 @@ def init(path: str, force: bool, reindex: bool, model: Optional[str]):
|
|||||||
table.add_column("Metric", style="cyan")
|
table.add_column("Metric", style="cyan")
|
||||||
table.add_column("Value", style="green")
|
table.add_column("Value", style="green")
|
||||||
|
|
||||||
table.add_row("Files Indexed", str(stats['file_count']))
|
table.add_row("Files Indexed", str(stats["file_count"]))
|
||||||
table.add_row("Total Chunks", str(stats['chunk_count']))
|
table.add_row("Total Chunks", str(stats["chunk_count"]))
|
||||||
table.add_row("Index Size", f"{stats['index_size_mb']:.2f} MB")
|
table.add_row("Index Size", f"{stats['index_size_mb']:.2f} MB")
|
||||||
table.add_row("Last Updated", stats['indexed_at'] or "Never")
|
table.add_row("Last Updated", stats["indexed_at"] or "Never")
|
||||||
|
|
||||||
console.print(table)
|
console.print(table)
|
||||||
return
|
return
|
||||||
@ -114,10 +114,7 @@ def init(path: str, force: bool, reindex: bool, model: Optional[str]):
|
|||||||
|
|
||||||
# Create indexer
|
# Create indexer
|
||||||
task = progress.add_task("[cyan]Creating indexer...", total=None)
|
task = progress.add_task("[cyan]Creating indexer...", total=None)
|
||||||
indexer = ProjectIndexer(
|
indexer = ProjectIndexer(project_path, embedder=embedder)
|
||||||
project_path,
|
|
||||||
embedder=embedder
|
|
||||||
)
|
|
||||||
progress.update(task, completed=True)
|
progress.update(task, completed=True)
|
||||||
|
|
||||||
# Run indexing
|
# Run indexing
|
||||||
@ -125,8 +122,10 @@ def init(path: str, force: bool, reindex: bool, model: Optional[str]):
|
|||||||
stats = indexer.index_project(force_reindex=force_reindex)
|
stats = indexer.index_project(force_reindex=force_reindex)
|
||||||
|
|
||||||
# Show summary
|
# Show summary
|
||||||
if stats['files_indexed'] > 0:
|
if stats["files_indexed"] > 0:
|
||||||
console.print(f"\n[bold green] Success![/bold green] Indexed {stats['files_indexed']} files")
|
console.print(
|
||||||
|
f"\n[bold green] Success![/bold green] Indexed {stats['files_indexed']} files"
|
||||||
|
)
|
||||||
console.print(f"Created {stats['chunks_created']} searchable chunks")
|
console.print(f"Created {stats['chunks_created']} searchable chunks")
|
||||||
console.print(f"Time: {stats['time_taken']:.2f} seconds")
|
console.print(f"Time: {stats['time_taken']:.2f} seconds")
|
||||||
console.print(f"Speed: {stats['files_per_second']:.1f} files/second")
|
console.print(f"Speed: {stats['files_per_second']:.1f} files/second")
|
||||||
@ -135,7 +134,7 @@ def init(path: str, force: bool, reindex: bool, model: Optional[str]):
|
|||||||
|
|
||||||
# Show how to use
|
# Show how to use
|
||||||
console.print("\n[bold]Next steps:[/bold]")
|
console.print("\n[bold]Next steps:[/bold]")
|
||||||
console.print(" • Search your code: [cyan]rag-mini search \"your query\"[/cyan]")
|
console.print(' • Search your code: [cyan]rag-mini search "your query"[/cyan]')
|
||||||
console.print(" • Watch for changes: [cyan]rag-mini watch[/cyan]")
|
console.print(" • Watch for changes: [cyan]rag-mini watch[/cyan]")
|
||||||
console.print(" • View statistics: [cyan]rag-mini stats[/cyan]\n")
|
console.print(" • View statistics: [cyan]rag-mini stats[/cyan]\n")
|
||||||
|
|
||||||
@ -146,25 +145,29 @@ def init(path: str, force: bool, reindex: bool, model: Optional[str]):
|
|||||||
|
|
||||||
|
|
||||||
@cli.command()
|
@cli.command()
|
||||||
@click.argument('query')
|
@click.argument("query")
|
||||||
@click.option('--path', '-p', type=click.Path(exists=True), default='.',
|
@click.option("--path", "-p", type=click.Path(exists=True), default=".", help="Project path")
|
||||||
help='Project path')
|
@click.option("--top-k", "-k", type=int, default=10, help="Maximum results to show")
|
||||||
@click.option('--top-k', '-k', type=int, default=10,
|
@click.option(
|
||||||
help='Maximum results to show')
|
"--type", "-t", multiple=True, help="Filter by chunk type (function, class, method)"
|
||||||
@click.option('--type', '-t', multiple=True,
|
)
|
||||||
help='Filter by chunk type (function, class, method)')
|
@click.option("--lang", multiple=True, help="Filter by language (python, javascript, etc.)")
|
||||||
@click.option('--lang', multiple=True,
|
@click.option("--show-content", "-c", is_flag=True, help="Show code content in results")
|
||||||
help='Filter by language (python, javascript, etc.)')
|
@click.option("--show-per", is_flag=True, help="Show performance metrics")
|
||||||
@click.option('--show-content', '-c', is_flag=True,
|
def search(
|
||||||
help='Show code content in results')
|
query: str,
|
||||||
@click.option('--show-perf', is_flag=True,
|
path: str,
|
||||||
help='Show performance metrics')
|
top_k: int,
|
||||||
def search(query: str, path: str, top_k: int, type: tuple, lang: tuple, show_content: bool, show_perf: bool):
|
type: tuple,
|
||||||
|
lang: tuple,
|
||||||
|
show_content: bool,
|
||||||
|
show_perf: bool,
|
||||||
|
):
|
||||||
"""Search codebase using semantic similarity."""
|
"""Search codebase using semantic similarity."""
|
||||||
project_path = Path(path).resolve()
|
project_path = Path(path).resolve()
|
||||||
|
|
||||||
# Check if indexed
|
# Check if indexed
|
||||||
rag_dir = project_path / '.mini-rag'
|
rag_dir = project_path / ".mini-rag"
|
||||||
if not rag_dir.exists():
|
if not rag_dir.exists():
|
||||||
console.print("[red]Error:[/red] Project not indexed. Run 'rag-mini init' first.")
|
console.print("[red]Error:[/red] Project not indexed. Run 'rag-mini init' first.")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
@ -183,27 +186,30 @@ def search(query: str, path: str, top_k: int, type: tuple, lang: tuple, show_con
|
|||||||
|
|
||||||
response = client.search(query, top_k=top_k)
|
response = client.search(query, top_k=top_k)
|
||||||
|
|
||||||
if response.get('success'):
|
if response.get("success"):
|
||||||
# Convert response to SearchResult objects
|
# Convert response to SearchResult objects
|
||||||
from .search import SearchResult
|
from .search import SearchResult
|
||||||
|
|
||||||
results = []
|
results = []
|
||||||
for r in response['results']:
|
for r in response["results"]:
|
||||||
result = SearchResult(
|
result = SearchResult(
|
||||||
file_path=r['file_path'],
|
file_path=r["file_path"],
|
||||||
content=r['content'],
|
content=r["content"],
|
||||||
score=r['score'],
|
score=r["score"],
|
||||||
start_line=r['start_line'],
|
start_line=r["start_line"],
|
||||||
end_line=r['end_line'],
|
end_line=r["end_line"],
|
||||||
chunk_type=r['chunk_type'],
|
chunk_type=r["chunk_type"],
|
||||||
name=r['name'],
|
name=r["name"],
|
||||||
language=r['language']
|
language=r["language"],
|
||||||
)
|
)
|
||||||
results.append(result)
|
results.append(result)
|
||||||
|
|
||||||
# Show server stats
|
# Show server stats
|
||||||
search_time = response.get('search_time_ms', 0)
|
search_time = response.get("search_time_ms", 0)
|
||||||
total_queries = response.get('total_queries', 0)
|
total_queries = response.get("total_queries", 0)
|
||||||
console.print(f"[dim]Search time: {search_time}ms (Query #{total_queries})[/dim]\n")
|
console.print(
|
||||||
|
f"[dim]Search time: {search_time}ms (Query #{total_queries})[/dim]\n"
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
console.print(f"[red]Server error:[/red] {response.get('error')}")
|
console.print(f"[red]Server error:[/red] {response.get('error')}")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
@ -223,7 +229,7 @@ def search(query: str, path: str, top_k: int, type: tuple, lang: tuple, show_con
|
|||||||
query,
|
query,
|
||||||
top_k=top_k,
|
top_k=top_k,
|
||||||
chunk_types=list(type) if type else None,
|
chunk_types=list(type) if type else None,
|
||||||
languages=list(lang) if lang else None
|
languages=list(lang) if lang else None,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
with console.status(f"[cyan]Searching for: {query}[/cyan]"):
|
with console.status(f"[cyan]Searching for: {query}[/cyan]"):
|
||||||
@ -231,7 +237,7 @@ def search(query: str, path: str, top_k: int, type: tuple, lang: tuple, show_con
|
|||||||
query,
|
query,
|
||||||
top_k=top_k,
|
top_k=top_k,
|
||||||
chunk_types=list(type) if type else None,
|
chunk_types=list(type) if type else None,
|
||||||
languages=list(lang) if lang else None
|
languages=list(lang) if lang else None,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Display results
|
# Display results
|
||||||
@ -247,12 +253,15 @@ def search(query: str, path: str, top_k: int, type: tuple, lang: tuple, show_con
|
|||||||
# Copy first result to clipboard if available
|
# Copy first result to clipboard if available
|
||||||
try:
|
try:
|
||||||
import pyperclip
|
import pyperclip
|
||||||
|
|
||||||
first_result = results[0]
|
first_result = results[0]
|
||||||
location = f"{first_result.file_path}:{first_result.start_line}"
|
location = f"{first_result.file_path}:{first_result.start_line}"
|
||||||
pyperclip.copy(location)
|
pyperclip.copy(location)
|
||||||
console.print(f"\n[dim]First result location copied to clipboard: {location}[/dim]")
|
console.print(
|
||||||
except:
|
f"\n[dim]First result location copied to clipboard: {location}[/dim]"
|
||||||
pass
|
)
|
||||||
|
except (ImportError, OSError):
|
||||||
|
pass # Clipboard not available
|
||||||
else:
|
else:
|
||||||
console.print(f"\n[yellow]No results found for: {query}[/yellow]")
|
console.print(f"\n[yellow]No results found for: {query}[/yellow]")
|
||||||
console.print("\n[dim]Tips:[/dim]")
|
console.print("\n[dim]Tips:[/dim]")
|
||||||
@ -271,14 +280,13 @@ def search(query: str, path: str, top_k: int, type: tuple, lang: tuple, show_con
|
|||||||
|
|
||||||
|
|
||||||
@cli.command()
|
@cli.command()
|
||||||
@click.option('--path', '-p', type=click.Path(exists=True), default='.',
|
@click.option("--path", "-p", type=click.Path(exists=True), default=".", help="Project path")
|
||||||
help='Project path')
|
|
||||||
def stats(path: str):
|
def stats(path: str):
|
||||||
"""Show index statistics."""
|
"""Show index statistics."""
|
||||||
project_path = Path(path).resolve()
|
project_path = Path(path).resolve()
|
||||||
|
|
||||||
# Check if indexed
|
# Check if indexed
|
||||||
rag_dir = project_path / '.mini-rag'
|
rag_dir = project_path / ".mini-rag"
|
||||||
if not rag_dir.exists():
|
if not rag_dir.exists():
|
||||||
console.print("[red]Error:[/red] Project not indexed. Run 'rag-mini init' first.")
|
console.print("[red]Error:[/red] Project not indexed. Run 'rag-mini init' first.")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
@ -300,35 +308,37 @@ def stats(path: str):
|
|||||||
table.add_column("Metric", style="cyan")
|
table.add_column("Metric", style="cyan")
|
||||||
table.add_column("Value", style="green")
|
table.add_column("Value", style="green")
|
||||||
|
|
||||||
table.add_row("Files Indexed", str(index_stats['file_count']))
|
table.add_row("Files Indexed", str(index_stats["file_count"]))
|
||||||
table.add_row("Total Chunks", str(index_stats['chunk_count']))
|
table.add_row("Total Chunks", str(index_stats["chunk_count"]))
|
||||||
table.add_row("Index Size", f"{index_stats['index_size_mb']:.2f} MB")
|
table.add_row("Index Size", f"{index_stats['index_size_mb']:.2f} MB")
|
||||||
table.add_row("Last Updated", index_stats['indexed_at'] or "Never")
|
table.add_row("Last Updated", index_stats["indexed_at"] or "Never")
|
||||||
|
|
||||||
console.print(table)
|
console.print(table)
|
||||||
|
|
||||||
# Language distribution
|
# Language distribution
|
||||||
if 'languages' in search_stats:
|
if "languages" in search_stats:
|
||||||
console.print("\n[bold]Language Distribution:[/bold]")
|
console.print("\n[bold]Language Distribution:[/bold]")
|
||||||
lang_table = Table()
|
lang_table = Table()
|
||||||
lang_table.add_column("Language", style="cyan")
|
lang_table.add_column("Language", style="cyan")
|
||||||
lang_table.add_column("Chunks", style="green")
|
lang_table.add_column("Chunks", style="green")
|
||||||
|
|
||||||
for lang, count in sorted(search_stats['languages'].items(),
|
for lang, count in sorted(
|
||||||
key=lambda x: x[1], reverse=True):
|
search_stats["languages"].items(), key=lambda x: x[1], reverse=True
|
||||||
|
):
|
||||||
lang_table.add_row(lang, str(count))
|
lang_table.add_row(lang, str(count))
|
||||||
|
|
||||||
console.print(lang_table)
|
console.print(lang_table)
|
||||||
|
|
||||||
# Chunk type distribution
|
# Chunk type distribution
|
||||||
if 'chunk_types' in search_stats:
|
if "chunk_types" in search_stats:
|
||||||
console.print("\n[bold]Chunk Types:[/bold]")
|
console.print("\n[bold]Chunk Types:[/bold]")
|
||||||
type_table = Table()
|
type_table = Table()
|
||||||
type_table.add_column("Type", style="cyan")
|
type_table.add_column("Type", style="cyan")
|
||||||
type_table.add_column("Count", style="green")
|
type_table.add_column("Count", style="green")
|
||||||
|
|
||||||
for chunk_type, count in sorted(search_stats['chunk_types'].items(),
|
for chunk_type, count in sorted(
|
||||||
key=lambda x: x[1], reverse=True):
|
search_stats["chunk_types"].items(), key=lambda x: x[1], reverse=True
|
||||||
|
):
|
||||||
type_table.add_row(chunk_type, str(count))
|
type_table.add_row(chunk_type, str(count))
|
||||||
|
|
||||||
console.print(type_table)
|
console.print(type_table)
|
||||||
@ -340,14 +350,13 @@ def stats(path: str):
|
|||||||
|
|
||||||
|
|
||||||
@cli.command()
|
@cli.command()
|
||||||
@click.option('--path', '-p', type=click.Path(exists=True), default='.',
|
@click.option("--path", "-p", type=click.Path(exists=True), default=".", help="Project path")
|
||||||
help='Project path')
|
|
||||||
def debug_schema(path: str):
|
def debug_schema(path: str):
|
||||||
"""Debug vector database schema and sample data."""
|
"""Debug vector database schema and sample data."""
|
||||||
project_path = Path(path).resolve()
|
project_path = Path(path).resolve()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
rag_dir = project_path / '.mini-rag'
|
rag_dir = project_path / ".mini-rag"
|
||||||
|
|
||||||
if not rag_dir.exists():
|
if not rag_dir.exists():
|
||||||
console.print("[red]No RAG index found. Run 'rag-mini init' first.[/red]")
|
console.print("[red]No RAG index found. Run 'rag-mini init' first.[/red]")
|
||||||
@ -357,7 +366,9 @@ def debug_schema(path: str):
|
|||||||
try:
|
try:
|
||||||
import lancedb
|
import lancedb
|
||||||
except ImportError:
|
except ImportError:
|
||||||
console.print("[red]LanceDB not available. Install with: pip install lancedb pyarrow[/red]")
|
console.print(
|
||||||
|
"[red]LanceDB not available. Install with: pip install lancedb pyarrow[/red]"
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
db = lancedb.connect(rag_dir)
|
db = lancedb.connect(rag_dir)
|
||||||
@ -373,30 +384,35 @@ def debug_schema(path: str):
|
|||||||
console.print(table.schema)
|
console.print(table.schema)
|
||||||
|
|
||||||
# Get sample data
|
# Get sample data
|
||||||
import pandas as pd
|
|
||||||
df = table.to_pandas()
|
df = table.to_pandas()
|
||||||
console.print(f"\n[bold cyan] Table Statistics:[/bold cyan]")
|
console.print("\n[bold cyan] Table Statistics:[/bold cyan]")
|
||||||
console.print(f"Total rows: {len(df)}")
|
console.print(f"Total rows: {len(df)}")
|
||||||
|
|
||||||
if len(df) > 0:
|
if len(df) > 0:
|
||||||
# Check embedding column
|
# Check embedding column
|
||||||
console.print(f"\n[bold cyan] Embedding Column Analysis:[/bold cyan]")
|
console.print("\n[bold cyan] Embedding Column Analysis:[/bold cyan]")
|
||||||
first_embedding = df['embedding'].iloc[0]
|
first_embedding = df["embedding"].iloc[0]
|
||||||
console.print(f"Type: {type(first_embedding)}")
|
console.print(f"Type: {type(first_embedding)}")
|
||||||
if hasattr(first_embedding, 'shape'):
|
if hasattr(first_embedding, "shape"):
|
||||||
console.print(f"Shape: {first_embedding.shape}")
|
console.print(f"Shape: {first_embedding.shape}")
|
||||||
if hasattr(first_embedding, 'dtype'):
|
if hasattr(first_embedding, "dtype"):
|
||||||
console.print(f"Dtype: {first_embedding.dtype}")
|
console.print(f"Dtype: {first_embedding.dtype}")
|
||||||
|
|
||||||
# Show first few rows
|
# Show first few rows
|
||||||
console.print(f"\n[bold cyan] Sample Data (first 3 rows):[/bold cyan]")
|
console.print("\n[bold cyan] Sample Data (first 3 rows):[/bold cyan]")
|
||||||
for i in range(min(3, len(df))):
|
for i in range(min(3, len(df))):
|
||||||
row = df.iloc[i]
|
row = df.iloc[i]
|
||||||
console.print(f"\n[yellow]Row {i}:[/yellow]")
|
console.print(f"\n[yellow]Row {i}:[/yellow]")
|
||||||
console.print(f" chunk_id: {row['chunk_id']}")
|
console.print(f" chunk_id: {row['chunk_id']}")
|
||||||
console.print(f" file_path: {row['file_path']}")
|
console.print(f" file_path: {row['file_path']}")
|
||||||
console.print(f" content: {row['content'][:50]}...")
|
console.print(f" content: {row['content'][:50]}...")
|
||||||
console.print(f" embedding: {type(row['embedding'])} of length {len(row['embedding']) if hasattr(row['embedding'], '__len__') else 'unknown'}")
|
embed_len = (
|
||||||
|
len(row["embedding"])
|
||||||
|
if hasattr(row["embedding"], "__len__")
|
||||||
|
else "unknown"
|
||||||
|
)
|
||||||
|
console.print(f" embedding: {type(row['embedding'])} of length {embed_len}")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Schema debug failed: {e}")
|
logger.error(f"Schema debug failed: {e}")
|
||||||
@ -404,18 +420,27 @@ def debug_schema(path: str):
|
|||||||
|
|
||||||
|
|
||||||
@cli.command()
|
@cli.command()
|
||||||
@click.option('--path', '-p', type=click.Path(exists=True), default='.',
|
@click.option("--path", "-p", type=click.Path(exists=True), default=".", help="Project path")
|
||||||
help='Project path')
|
@click.option(
|
||||||
@click.option('--delay', '-d', type=float, default=10.0,
|
"--delay",
|
||||||
help='Update delay in seconds (default: 10s for non-invasive)')
|
"-d",
|
||||||
@click.option('--silent', '-s', is_flag=True, default=False,
|
type=float,
|
||||||
help='Run silently in background without output')
|
default=10.0,
|
||||||
|
help="Update delay in seconds (default: 10s for non-invasive)",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--silent",
|
||||||
|
"-s",
|
||||||
|
is_flag=True,
|
||||||
|
default=False,
|
||||||
|
help="Run silently in background without output",
|
||||||
|
)
|
||||||
def watch(path: str, delay: float, silent: bool):
|
def watch(path: str, delay: float, silent: bool):
|
||||||
"""Watch for file changes and update index automatically (non-invasive by default)."""
|
"""Watch for file changes and update index automatically (non-invasive by default)."""
|
||||||
project_path = Path(path).resolve()
|
project_path = Path(path).resolve()
|
||||||
|
|
||||||
# Check if indexed
|
# Check if indexed
|
||||||
rag_dir = project_path / '.mini-rag'
|
rag_dir = project_path / ".mini-rag"
|
||||||
if not rag_dir.exists():
|
if not rag_dir.exists():
|
||||||
if not silent:
|
if not silent:
|
||||||
console.print("[red]Error:[/red] Project not indexed. Run 'rag-mini init' first.")
|
console.print("[red]Error:[/red] Project not indexed. Run 'rag-mini init' first.")
|
||||||
@ -459,7 +484,7 @@ def watch(path: str, delay: float, silent: bool):
|
|||||||
f"\r[green]✓[/green] Files updated: {stats.get('files_processed', 0)} | "
|
f"\r[green]✓[/green] Files updated: {stats.get('files_processed', 0)} | "
|
||||||
f"[red]✗[/red] Failed: {stats.get('files_dropped', 0)} | "
|
f"[red]✗[/red] Failed: {stats.get('files_dropped', 0)} | "
|
||||||
f"[cyan]⧗[/cyan] Queue: {stats['queue_size']}",
|
f"[cyan]⧗[/cyan] Queue: {stats['queue_size']}",
|
||||||
end=""
|
end="",
|
||||||
)
|
)
|
||||||
last_stats = stats
|
last_stats = stats
|
||||||
|
|
||||||
@ -474,10 +499,12 @@ def watch(path: str, delay: float, silent: bool):
|
|||||||
# Show final stats only if not silent
|
# Show final stats only if not silent
|
||||||
if not silent:
|
if not silent:
|
||||||
final_stats = watcher.get_statistics()
|
final_stats = watcher.get_statistics()
|
||||||
console.print(f"\n[bold green]Watch Summary:[/bold green]")
|
console.print("\n[bold green]Watch Summary:[/bold green]")
|
||||||
console.print(f"Files updated: {final_stats.get('files_processed', 0)}")
|
console.print(f"Files updated: {final_stats.get('files_processed', 0)}")
|
||||||
console.print(f"Files failed: {final_stats.get('files_dropped', 0)}")
|
console.print(f"Files failed: {final_stats.get('files_dropped', 0)}")
|
||||||
console.print(f"Total runtime: {final_stats.get('uptime_seconds', 0):.1f} seconds\n")
|
console.print(
|
||||||
|
f"Total runtime: {final_stats.get('uptime_seconds', 0):.1f} seconds\n"
|
||||||
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
console.print(f"\n[bold red]Error:[/bold red] {e}")
|
console.print(f"\n[bold red]Error:[/bold red] {e}")
|
||||||
@ -486,11 +513,9 @@ def watch(path: str, delay: float, silent: bool):
|
|||||||
|
|
||||||
|
|
||||||
@cli.command()
|
@cli.command()
|
||||||
@click.argument('function_name')
|
@click.argument("function_name")
|
||||||
@click.option('--path', '-p', type=click.Path(exists=True), default='.',
|
@click.option("--path", "-p", type=click.Path(exists=True), default=".", help="Project path")
|
||||||
help='Project path')
|
@click.option("--top-k", "-k", type=int, default=5, help="Maximum results")
|
||||||
@click.option('--top-k', '-k', type=int, default=5,
|
|
||||||
help='Maximum results')
|
|
||||||
def find_function(function_name: str, path: str, top_k: int):
|
def find_function(function_name: str, path: str, top_k: int):
|
||||||
"""Find a specific function by name."""
|
"""Find a specific function by name."""
|
||||||
project_path = Path(path).resolve()
|
project_path = Path(path).resolve()
|
||||||
@ -510,11 +535,9 @@ def find_function(function_name: str, path: str, top_k: int):
|
|||||||
|
|
||||||
|
|
||||||
@cli.command()
|
@cli.command()
|
||||||
@click.argument('class_name')
|
@click.argument("class_name")
|
||||||
@click.option('--path', '-p', type=click.Path(exists=True), default='.',
|
@click.option("--path", "-p", type=click.Path(exists=True), default=".", help="Project path")
|
||||||
help='Project path')
|
@click.option("--top-k", "-k", type=int, default=5, help="Maximum results")
|
||||||
@click.option('--top-k', '-k', type=int, default=5,
|
|
||||||
help='Maximum results')
|
|
||||||
def find_class(class_name: str, path: str, top_k: int):
|
def find_class(class_name: str, path: str, top_k: int):
|
||||||
"""Find a specific class by name."""
|
"""Find a specific class by name."""
|
||||||
project_path = Path(path).resolve()
|
project_path = Path(path).resolve()
|
||||||
@ -534,14 +557,13 @@ def find_class(class_name: str, path: str, top_k: int):
|
|||||||
|
|
||||||
|
|
||||||
@cli.command()
|
@cli.command()
|
||||||
@click.option('--path', '-p', type=click.Path(exists=True), default='.',
|
@click.option("--path", "-p", type=click.Path(exists=True), default=".", help="Project path")
|
||||||
help='Project path')
|
|
||||||
def update(path: str):
|
def update(path: str):
|
||||||
"""Update index for changed files."""
|
"""Update index for changed files."""
|
||||||
project_path = Path(path).resolve()
|
project_path = Path(path).resolve()
|
||||||
|
|
||||||
# Check if indexed
|
# Check if indexed
|
||||||
rag_dir = project_path / '.mini-rag'
|
rag_dir = project_path / ".mini-rag"
|
||||||
if not rag_dir.exists():
|
if not rag_dir.exists():
|
||||||
console.print("[red]Error:[/red] Project not indexed. Run 'rag-mini init' first.")
|
console.print("[red]Error:[/red] Project not indexed. Run 'rag-mini init' first.")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
@ -553,7 +575,7 @@ def update(path: str):
|
|||||||
|
|
||||||
stats = indexer.index_project(force_reindex=False)
|
stats = indexer.index_project(force_reindex=False)
|
||||||
|
|
||||||
if stats['files_indexed'] > 0:
|
if stats["files_indexed"] > 0:
|
||||||
console.print(f"[green][/green] Updated {stats['files_indexed']} files")
|
console.print(f"[green][/green] Updated {stats['files_indexed']} files")
|
||||||
console.print(f"Created {stats['chunks_created']} new chunks")
|
console.print(f"Created {stats['chunks_created']} new chunks")
|
||||||
else:
|
else:
|
||||||
@ -565,7 +587,7 @@ def update(path: str):
|
|||||||
|
|
||||||
|
|
||||||
@cli.command()
|
@cli.command()
|
||||||
@click.option('--show-code', '-c', is_flag=True, help='Show example code')
|
@click.option("--show-code", "-c", is_flag=True, help="Show example code")
|
||||||
def info(show_code: bool):
|
def info(show_code: bool):
|
||||||
"""Show information about Mini RAG."""
|
"""Show information about Mini RAG."""
|
||||||
# Create info panel
|
# Create info panel
|
||||||
@ -619,16 +641,14 @@ rag-mini stats"""
|
|||||||
|
|
||||||
|
|
||||||
@cli.command()
|
@cli.command()
|
||||||
@click.option('--path', '-p', type=click.Path(exists=True), default='.',
|
@click.option("--path", "-p", type=click.Path(exists=True), default=".", help="Project path")
|
||||||
help='Project path')
|
@click.option("--port", type=int, default=7777, help="Server port")
|
||||||
@click.option('--port', type=int, default=7777,
|
|
||||||
help='Server port')
|
|
||||||
def server(path: str, port: int):
|
def server(path: str, port: int):
|
||||||
"""Start persistent RAG server (keeps model loaded)."""
|
"""Start persistent RAG server (keeps model loaded)."""
|
||||||
project_path = Path(path).resolve()
|
project_path = Path(path).resolve()
|
||||||
|
|
||||||
# Check if indexed
|
# Check if indexed
|
||||||
rag_dir = project_path / '.mini-rag'
|
rag_dir = project_path / ".mini-rag"
|
||||||
if not rag_dir.exists():
|
if not rag_dir.exists():
|
||||||
console.print("[red]Error:[/red] Project not indexed. Run 'rag-mini init' first.")
|
console.print("[red]Error:[/red] Project not indexed. Run 'rag-mini init' first.")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
@ -648,12 +668,9 @@ def server(path: str, port: int):
|
|||||||
|
|
||||||
|
|
||||||
@cli.command()
|
@cli.command()
|
||||||
@click.option('--path', '-p', type=click.Path(exists=True), default='.',
|
@click.option("--path", "-p", type=click.Path(exists=True), default=".", help="Project path")
|
||||||
help='Project path')
|
@click.option("--port", type=int, default=7777, help="Server port")
|
||||||
@click.option('--port', type=int, default=7777,
|
@click.option("--discovery", "-d", is_flag=True, help="Run codebase discovery analysis")
|
||||||
help='Server port')
|
|
||||||
@click.option('--discovery', '-d', is_flag=True,
|
|
||||||
help='Run codebase discovery analysis')
|
|
||||||
def status(path: str, port: int, discovery: bool):
|
def status(path: str, port: int, discovery: bool):
|
||||||
"""Show comprehensive RAG system status with optional codebase discovery."""
|
"""Show comprehensive RAG system status with optional codebase discovery."""
|
||||||
project_path = Path(path).resolve()
|
project_path = Path(path).resolve()
|
||||||
@ -666,7 +683,12 @@ def status(path: str, port: int, discovery: bool):
|
|||||||
console.print("[bold]📁 Folder Contents:[/bold]")
|
console.print("[bold]📁 Folder Contents:[/bold]")
|
||||||
try:
|
try:
|
||||||
all_files = list(project_path.rglob("*"))
|
all_files = list(project_path.rglob("*"))
|
||||||
source_files = [f for f in all_files if f.is_file() and f.suffix in ['.py', '.js', '.ts', '.go', '.java', '.cpp', '.c', '.h']]
|
source_files = [
|
||||||
|
f
|
||||||
|
for f in all_files
|
||||||
|
if f.is_file()
|
||||||
|
and f.suffix in [".py", ".js", ".ts", ".go", ".java", ".cpp", ".c", ".h"]
|
||||||
|
]
|
||||||
|
|
||||||
console.print(f" • Total files: {len([f for f in all_files if f.is_file()])}")
|
console.print(f" • Total files: {len([f for f in all_files if f.is_file()])}")
|
||||||
console.print(f" • Source files: {len(source_files)}")
|
console.print(f" • Source files: {len(source_files)}")
|
||||||
@ -676,19 +698,19 @@ def status(path: str, port: int, discovery: bool):
|
|||||||
|
|
||||||
# Check index status
|
# Check index status
|
||||||
console.print("\n[bold]🗂️ Index Status:[/bold]")
|
console.print("\n[bold]🗂️ Index Status:[/bold]")
|
||||||
rag_dir = project_path / '.mini-rag'
|
rag_dir = project_path / ".mini-rag"
|
||||||
if rag_dir.exists():
|
if rag_dir.exists():
|
||||||
try:
|
try:
|
||||||
indexer = ProjectIndexer(project_path)
|
indexer = ProjectIndexer(project_path)
|
||||||
index_stats = indexer.get_statistics()
|
index_stats = indexer.get_statistics()
|
||||||
|
|
||||||
console.print(f" • Status: [green]✅ Indexed[/green]")
|
console.print(" • Status: [green]✅ Indexed[/green]")
|
||||||
console.print(f" • Files indexed: {index_stats['file_count']}")
|
console.print(f" • Files indexed: {index_stats['file_count']}")
|
||||||
console.print(f" • Total chunks: {index_stats['chunk_count']}")
|
console.print(f" • Total chunks: {index_stats['chunk_count']}")
|
||||||
console.print(f" • Index size: {index_stats['index_size_mb']:.2f} MB")
|
console.print(f" • Index size: {index_stats['index_size_mb']:.2f} MB")
|
||||||
console.print(f" • Last updated: {index_stats['indexed_at'] or 'Never'}")
|
console.print(f" • Last updated: {index_stats['indexed_at'] or 'Never'}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
console.print(f" • Status: [yellow]⚠️ Index exists but has issues[/yellow]")
|
console.print(" • Status: [yellow]⚠️ Index exists but has issues[/yellow]")
|
||||||
console.print(f" • Error: {e}")
|
console.print(f" • Error: {e}")
|
||||||
else:
|
else:
|
||||||
console.print(" • Status: [red]❌ Not indexed[/red]")
|
console.print(" • Status: [red]❌ Not indexed[/red]")
|
||||||
@ -704,9 +726,9 @@ def status(path: str, port: int, discovery: bool):
|
|||||||
# Try to get server info
|
# Try to get server info
|
||||||
try:
|
try:
|
||||||
response = client.search("test", top_k=1) # Minimal query to get stats
|
response = client.search("test", top_k=1) # Minimal query to get stats
|
||||||
if response.get('success'):
|
if response.get("success"):
|
||||||
uptime = response.get('server_uptime', 0)
|
uptime = response.get("server_uptime", 0)
|
||||||
queries = response.get('total_queries', 0)
|
queries = response.get("total_queries", 0)
|
||||||
console.print(f" • Uptime: {uptime}s")
|
console.print(f" • Uptime: {uptime}s")
|
||||||
console.print(f" • Total queries: {queries}")
|
console.print(f" • Total queries: {queries}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -745,16 +767,20 @@ def status(path: str, port: int, discovery: bool):
|
|||||||
console.print("\n[bold]📋 Next Steps:[/bold]")
|
console.print("\n[bold]📋 Next Steps:[/bold]")
|
||||||
if not rag_dir.exists():
|
if not rag_dir.exists():
|
||||||
console.print(" 1. Run [cyan]rag-mini init[/cyan] to initialize the RAG system")
|
console.print(" 1. Run [cyan]rag-mini init[/cyan] to initialize the RAG system")
|
||||||
console.print(" 2. Use [cyan]rag-mini search \"your query\"[/cyan] to search code")
|
console.print(' 2. Use [cyan]rag-mini search "your query"[/cyan] to search code')
|
||||||
elif not client.is_running():
|
elif not client.is_running():
|
||||||
console.print(" 1. Run [cyan]rag-mini server[/cyan] to start the server")
|
console.print(" 1. Run [cyan]rag-mini server[/cyan] to start the server")
|
||||||
console.print(" 2. Use [cyan]rag-mini search \"your query\"[/cyan] to search code")
|
console.print(' 2. Use [cyan]rag-mini search "your query"[/cyan] to search code')
|
||||||
else:
|
else:
|
||||||
console.print(" • System ready! Use [cyan]rag-mini search \"your query\"[/cyan] to search")
|
console.print(
|
||||||
console.print(" • Add [cyan]--discovery[/cyan] flag to run intelligent codebase analysis")
|
' • System ready! Use [cyan]rag-mini search "your query"[/cyan] to search'
|
||||||
|
)
|
||||||
|
console.print(
|
||||||
|
" • Add [cyan]--discovery[/cyan] flag to run intelligent codebase analysis"
|
||||||
|
)
|
||||||
|
|
||||||
console.print()
|
console.print()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == "__main__":
|
||||||
cli()
|
cli()
|
||||||
@ -3,11 +3,12 @@ Configuration management for FSS-Mini-RAG.
|
|||||||
Handles loading, saving, and validation of YAML config files.
|
Handles loading, saving, and validation of YAML config files.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import yaml
|
|
||||||
import logging
|
import logging
|
||||||
|
from dataclasses import asdict, dataclass
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Dict, Any, Optional
|
from typing import Any, Dict, Optional
|
||||||
from dataclasses import dataclass, asdict
|
|
||||||
|
import yaml
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -15,6 +16,7 @@ logger = logging.getLogger(__name__)
|
|||||||
@dataclass
|
@dataclass
|
||||||
class ChunkingConfig:
|
class ChunkingConfig:
|
||||||
"""Configuration for text chunking."""
|
"""Configuration for text chunking."""
|
||||||
|
|
||||||
max_size: int = 2000
|
max_size: int = 2000
|
||||||
min_size: int = 150
|
min_size: int = 150
|
||||||
strategy: str = "semantic" # "semantic" or "fixed"
|
strategy: str = "semantic" # "semantic" or "fixed"
|
||||||
@ -23,6 +25,7 @@ class ChunkingConfig:
|
|||||||
@dataclass
|
@dataclass
|
||||||
class StreamingConfig:
|
class StreamingConfig:
|
||||||
"""Configuration for large file streaming."""
|
"""Configuration for large file streaming."""
|
||||||
|
|
||||||
enabled: bool = True
|
enabled: bool = True
|
||||||
threshold_bytes: int = 1048576 # 1MB
|
threshold_bytes: int = 1048576 # 1MB
|
||||||
|
|
||||||
@ -30,6 +33,7 @@ class StreamingConfig:
|
|||||||
@dataclass
|
@dataclass
|
||||||
class FilesConfig:
|
class FilesConfig:
|
||||||
"""Configuration for file processing."""
|
"""Configuration for file processing."""
|
||||||
|
|
||||||
min_file_size: int = 50
|
min_file_size: int = 50
|
||||||
exclude_patterns: list = None
|
exclude_patterns: list = None
|
||||||
include_patterns: list = None
|
include_patterns: list = None
|
||||||
@ -44,7 +48,7 @@ class FilesConfig:
|
|||||||
".venv/**",
|
".venv/**",
|
||||||
"venv/**",
|
"venv/**",
|
||||||
"build/**",
|
"build/**",
|
||||||
"dist/**"
|
"dist/**",
|
||||||
]
|
]
|
||||||
if self.include_patterns is None:
|
if self.include_patterns is None:
|
||||||
self.include_patterns = ["**/*"] # Include everything by default
|
self.include_patterns = ["**/*"] # Include everything by default
|
||||||
@ -53,6 +57,7 @@ class FilesConfig:
|
|||||||
@dataclass
|
@dataclass
|
||||||
class EmbeddingConfig:
|
class EmbeddingConfig:
|
||||||
"""Configuration for embedding generation."""
|
"""Configuration for embedding generation."""
|
||||||
|
|
||||||
preferred_method: str = "ollama" # "ollama", "ml", "hash", "auto"
|
preferred_method: str = "ollama" # "ollama", "ml", "hash", "auto"
|
||||||
ollama_model: str = "nomic-embed-text"
|
ollama_model: str = "nomic-embed-text"
|
||||||
ollama_host: str = "localhost:11434"
|
ollama_host: str = "localhost:11434"
|
||||||
@ -63,6 +68,7 @@ class EmbeddingConfig:
|
|||||||
@dataclass
|
@dataclass
|
||||||
class SearchConfig:
|
class SearchConfig:
|
||||||
"""Configuration for search behavior."""
|
"""Configuration for search behavior."""
|
||||||
|
|
||||||
default_top_k: int = 10
|
default_top_k: int = 10
|
||||||
enable_bm25: bool = True
|
enable_bm25: bool = True
|
||||||
similarity_threshold: float = 0.1
|
similarity_threshold: float = 0.1
|
||||||
@ -72,6 +78,7 @@ class SearchConfig:
|
|||||||
@dataclass
|
@dataclass
|
||||||
class LLMConfig:
|
class LLMConfig:
|
||||||
"""Configuration for LLM synthesis and query expansion."""
|
"""Configuration for LLM synthesis and query expansion."""
|
||||||
|
|
||||||
# Core settings
|
# Core settings
|
||||||
synthesis_model: str = "auto" # "auto", "qwen3:1.7b", "qwen2.5:1.5b", etc.
|
synthesis_model: str = "auto" # "auto", "qwen3:1.7b", "qwen2.5:1.5b", etc.
|
||||||
expansion_model: str = "auto" # Usually same as synthesis_model
|
expansion_model: str = "auto" # Usually same as synthesis_model
|
||||||
@ -101,13 +108,10 @@ class LLMConfig:
|
|||||||
self.model_rankings = [
|
self.model_rankings = [
|
||||||
# Testing model (prioritized for current testing phase)
|
# Testing model (prioritized for current testing phase)
|
||||||
"qwen3:1.7b",
|
"qwen3:1.7b",
|
||||||
|
|
||||||
# Ultra-efficient models (perfect for CPU-only systems)
|
# Ultra-efficient models (perfect for CPU-only systems)
|
||||||
"qwen3:0.6b",
|
"qwen3:0.6b",
|
||||||
|
|
||||||
# Recommended model (excellent quality but larger)
|
# Recommended model (excellent quality but larger)
|
||||||
"qwen3:4b",
|
"qwen3:4b",
|
||||||
|
|
||||||
# Common fallbacks (prioritize Qwen models)
|
# Common fallbacks (prioritize Qwen models)
|
||||||
"qwen2.5:1.5b",
|
"qwen2.5:1.5b",
|
||||||
"qwen2.5:3b",
|
"qwen2.5:3b",
|
||||||
@ -117,6 +121,7 @@ class LLMConfig:
|
|||||||
@dataclass
|
@dataclass
|
||||||
class UpdateConfig:
|
class UpdateConfig:
|
||||||
"""Configuration for auto-update system."""
|
"""Configuration for auto-update system."""
|
||||||
|
|
||||||
auto_check: bool = True # Check for updates automatically
|
auto_check: bool = True # Check for updates automatically
|
||||||
check_frequency_hours: int = 24 # How often to check (hours)
|
check_frequency_hours: int = 24 # How often to check (hours)
|
||||||
auto_install: bool = False # Auto-install without asking (not recommended)
|
auto_install: bool = False # Auto-install without asking (not recommended)
|
||||||
@ -127,6 +132,7 @@ class UpdateConfig:
|
|||||||
@dataclass
|
@dataclass
|
||||||
class RAGConfig:
|
class RAGConfig:
|
||||||
"""Main RAG system configuration."""
|
"""Main RAG system configuration."""
|
||||||
|
|
||||||
chunking: ChunkingConfig = None
|
chunking: ChunkingConfig = None
|
||||||
streaming: StreamingConfig = None
|
streaming: StreamingConfig = None
|
||||||
files: FilesConfig = None
|
files: FilesConfig = None
|
||||||
@ -157,8 +163,8 @@ class ConfigManager:
|
|||||||
|
|
||||||
def __init__(self, project_path: Path):
|
def __init__(self, project_path: Path):
|
||||||
self.project_path = Path(project_path)
|
self.project_path = Path(project_path)
|
||||||
self.rag_dir = self.project_path / '.mini-rag'
|
self.rag_dir = self.project_path / ".mini-rag"
|
||||||
self.config_path = self.rag_dir / 'config.yaml'
|
self.config_path = self.rag_dir / "config.yaml"
|
||||||
|
|
||||||
def load_config(self) -> RAGConfig:
|
def load_config(self) -> RAGConfig:
|
||||||
"""Load configuration from YAML file or create default."""
|
"""Load configuration from YAML file or create default."""
|
||||||
@ -169,7 +175,7 @@ class ConfigManager:
|
|||||||
return config
|
return config
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with open(self.config_path, 'r') as f:
|
with open(self.config_path, "r") as f:
|
||||||
data = yaml.safe_load(f)
|
data = yaml.safe_load(f)
|
||||||
|
|
||||||
if not data:
|
if not data:
|
||||||
@ -179,24 +185,27 @@ class ConfigManager:
|
|||||||
# Convert nested dicts back to dataclass instances
|
# Convert nested dicts back to dataclass instances
|
||||||
config = RAGConfig()
|
config = RAGConfig()
|
||||||
|
|
||||||
if 'chunking' in data:
|
if "chunking" in data:
|
||||||
config.chunking = ChunkingConfig(**data['chunking'])
|
config.chunking = ChunkingConfig(**data["chunking"])
|
||||||
if 'streaming' in data:
|
if "streaming" in data:
|
||||||
config.streaming = StreamingConfig(**data['streaming'])
|
config.streaming = StreamingConfig(**data["streaming"])
|
||||||
if 'files' in data:
|
if "files" in data:
|
||||||
config.files = FilesConfig(**data['files'])
|
config.files = FilesConfig(**data["files"])
|
||||||
if 'embedding' in data:
|
if "embedding" in data:
|
||||||
config.embedding = EmbeddingConfig(**data['embedding'])
|
config.embedding = EmbeddingConfig(**data["embedding"])
|
||||||
if 'search' in data:
|
if "search" in data:
|
||||||
config.search = SearchConfig(**data['search'])
|
config.search = SearchConfig(**data["search"])
|
||||||
if 'llm' in data:
|
if "llm" in data:
|
||||||
config.llm = LLMConfig(**data['llm'])
|
config.llm = LLMConfig(**data["llm"])
|
||||||
|
|
||||||
return config
|
return config
|
||||||
|
|
||||||
except yaml.YAMLError as e:
|
except yaml.YAMLError as e:
|
||||||
# YAML syntax error - help user fix it instead of silent fallback
|
# YAML syntax error - help user fix it instead of silent fallback
|
||||||
error_msg = f"⚠️ Config file has YAML syntax error at line {getattr(e, 'problem_mark', 'unknown')}: {e}"
|
error_msg = (
|
||||||
|
f"⚠️ Config file has YAML syntax error at line "
|
||||||
|
f"{getattr(e, 'problem_mark', 'unknown')}: {e}"
|
||||||
|
)
|
||||||
logger.error(error_msg)
|
logger.error(error_msg)
|
||||||
print(f"\n{error_msg}")
|
print(f"\n{error_msg}")
|
||||||
print(f"Config file: {self.config_path}")
|
print(f"Config file: {self.config_path}")
|
||||||
@ -221,10 +230,13 @@ class ConfigManager:
|
|||||||
yaml_content = self._create_yaml_with_comments(config_dict)
|
yaml_content = self._create_yaml_with_comments(config_dict)
|
||||||
|
|
||||||
# Write with basic file locking to prevent corruption
|
# Write with basic file locking to prevent corruption
|
||||||
with open(self.config_path, 'w') as f:
|
with open(self.config_path, "w") as f:
|
||||||
try:
|
try:
|
||||||
import fcntl
|
import fcntl
|
||||||
fcntl.flock(f.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB) # Non-blocking exclusive lock
|
|
||||||
|
fcntl.flock(
|
||||||
|
f.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB
|
||||||
|
) # Non-blocking exclusive lock
|
||||||
f.write(yaml_content)
|
f.write(yaml_content)
|
||||||
fcntl.flock(f.fileno(), fcntl.LOCK_UN) # Unlock
|
fcntl.flock(f.fileno(), fcntl.LOCK_UN) # Unlock
|
||||||
except (OSError, ImportError):
|
except (OSError, ImportError):
|
||||||
@ -245,49 +257,50 @@ class ConfigManager:
|
|||||||
"",
|
"",
|
||||||
"# Text chunking settings",
|
"# Text chunking settings",
|
||||||
"chunking:",
|
"chunking:",
|
||||||
f" max_size: {config_dict['chunking']['max_size']} # Maximum characters per chunk",
|
f" max_size: {config_dict['chunking']['max_size']} # Max chars per chunk",
|
||||||
f" min_size: {config_dict['chunking']['min_size']} # Minimum characters per chunk",
|
f" min_size: {config_dict['chunking']['min_size']} # Min chars per chunk",
|
||||||
f" strategy: {config_dict['chunking']['strategy']} # 'semantic' (language-aware) or 'fixed'",
|
f" strategy: {config_dict['chunking']['strategy']} # 'semantic' or 'fixed'",
|
||||||
"",
|
"",
|
||||||
"# Large file streaming settings",
|
"# Large file streaming settings",
|
||||||
"streaming:",
|
"streaming:",
|
||||||
f" enabled: {str(config_dict['streaming']['enabled']).lower()}",
|
f" enabled: {str(config_dict['streaming']['enabled']).lower()}",
|
||||||
f" threshold_bytes: {config_dict['streaming']['threshold_bytes']} # Files larger than this use streaming (1MB)",
|
f" threshold_bytes: {config_dict['streaming']['threshold_bytes']} # Stream files >1MB",
|
||||||
"",
|
"",
|
||||||
"# File processing settings",
|
"# File processing settings",
|
||||||
"files:",
|
"files:",
|
||||||
f" min_file_size: {config_dict['files']['min_file_size']} # Skip files smaller than this",
|
f" min_file_size: {config_dict['files']['min_file_size']} # Skip small files",
|
||||||
" exclude_patterns:",
|
" exclude_patterns:",
|
||||||
]
|
]
|
||||||
|
|
||||||
for pattern in config_dict['files']['exclude_patterns']:
|
for pattern in config_dict["files"]["exclude_patterns"]:
|
||||||
yaml_lines.append(f" - \"{pattern}\"")
|
yaml_lines.append(f' - "{pattern}"')
|
||||||
|
|
||||||
yaml_lines.extend([
|
yaml_lines.extend(
|
||||||
|
[
|
||||||
" include_patterns:",
|
" include_patterns:",
|
||||||
" - \"**/*\" # Include all files by default",
|
' - "**/*" # Include all files by default',
|
||||||
"",
|
"",
|
||||||
"# Embedding generation settings",
|
"# Embedding generation settings",
|
||||||
"embedding:",
|
"embedding:",
|
||||||
f" preferred_method: {config_dict['embedding']['preferred_method']} # 'ollama', 'ml', 'hash', or 'auto'",
|
f" preferred_method: {config_dict['embedding']['preferred_method']} # Method",
|
||||||
f" ollama_model: {config_dict['embedding']['ollama_model']}",
|
f" ollama_model: {config_dict['embedding']['ollama_model']}",
|
||||||
f" ollama_host: {config_dict['embedding']['ollama_host']}",
|
f" ollama_host: {config_dict['embedding']['ollama_host']}",
|
||||||
f" ml_model: {config_dict['embedding']['ml_model']}",
|
f" ml_model: {config_dict['embedding']['ml_model']}",
|
||||||
f" batch_size: {config_dict['embedding']['batch_size']} # Embeddings processed per batch",
|
f" batch_size: {config_dict['embedding']['batch_size']} # Per batch",
|
||||||
"",
|
"",
|
||||||
"# Search behavior settings",
|
"# Search behavior settings",
|
||||||
"search:",
|
"search:",
|
||||||
f" default_top_k: {config_dict['search']['default_top_k']} # Default number of top results",
|
f" default_top_k: {config_dict['search']['default_top_k']} # Top results",
|
||||||
f" enable_bm25: {str(config_dict['search']['enable_bm25']).lower()} # Enable keyword matching boost",
|
f" enable_bm25: {str(config_dict['search']['enable_bm25']).lower()} # Keyword boost",
|
||||||
f" similarity_threshold: {config_dict['search']['similarity_threshold']} # Minimum similarity score",
|
f" similarity_threshold: {config_dict['search']['similarity_threshold']} # Min score",
|
||||||
f" expand_queries: {str(config_dict['search']['expand_queries']).lower()} # Enable automatic query expansion",
|
f" expand_queries: {str(config_dict['search']['expand_queries']).lower()} # Auto expand",
|
||||||
"",
|
"",
|
||||||
"# LLM synthesis and query expansion settings",
|
"# LLM synthesis and query expansion settings",
|
||||||
"llm:",
|
"llm:",
|
||||||
f" ollama_host: {config_dict['llm']['ollama_host']}",
|
f" ollama_host: {config_dict['llm']['ollama_host']}",
|
||||||
f" synthesis_model: {config_dict['llm']['synthesis_model']} # 'auto', 'qwen3:1.7b', etc.",
|
f" synthesis_model: {config_dict['llm']['synthesis_model']} # Model name",
|
||||||
f" expansion_model: {config_dict['llm']['expansion_model']} # Usually same as synthesis_model",
|
f" expansion_model: {config_dict['llm']['expansion_model']} # Model name",
|
||||||
f" max_expansion_terms: {config_dict['llm']['max_expansion_terms']} # Maximum terms to add to queries",
|
f" max_expansion_terms: {config_dict['llm']['max_expansion_terms']} # Max terms",
|
||||||
f" enable_synthesis: {str(config_dict['llm']['enable_synthesis']).lower()} # Enable synthesis by default",
|
f" enable_synthesis: {str(config_dict['llm']['enable_synthesis']).lower()} # Enable synthesis by default",
|
||||||
f" synthesis_temperature: {config_dict['llm']['synthesis_temperature']} # LLM temperature for analysis",
|
f" synthesis_temperature: {config_dict['llm']['synthesis_temperature']} # LLM temperature for analysis",
|
||||||
"",
|
"",
|
||||||
@ -300,17 +313,19 @@ class ConfigManager:
|
|||||||
f" auto_context: {str(config_dict['llm']['auto_context']).lower()} # Auto-adjust context based on model capabilities",
|
f" auto_context: {str(config_dict['llm']['auto_context']).lower()} # Auto-adjust context based on model capabilities",
|
||||||
"",
|
"",
|
||||||
" model_rankings: # Preferred model order (edit to change priority)",
|
" model_rankings: # Preferred model order (edit to change priority)",
|
||||||
])
|
]
|
||||||
|
)
|
||||||
|
|
||||||
# Add model rankings list
|
# Add model rankings list
|
||||||
if 'model_rankings' in config_dict['llm'] and config_dict['llm']['model_rankings']:
|
if "model_rankings" in config_dict["llm"] and config_dict["llm"]["model_rankings"]:
|
||||||
for model in config_dict['llm']['model_rankings'][:10]: # Show first 10
|
for model in config_dict["llm"]["model_rankings"][:10]: # Show first 10
|
||||||
yaml_lines.append(f" - \"{model}\"")
|
yaml_lines.append(f' - "{model}"')
|
||||||
if len(config_dict['llm']['model_rankings']) > 10:
|
if len(config_dict["llm"]["model_rankings"]) > 10:
|
||||||
yaml_lines.append(" # ... (edit config to see all options)")
|
yaml_lines.append(" # ... (edit config to see all options)")
|
||||||
|
|
||||||
# Add update settings
|
# Add update settings
|
||||||
yaml_lines.extend([
|
yaml_lines.extend(
|
||||||
|
[
|
||||||
"",
|
"",
|
||||||
"# Auto-update system settings",
|
"# Auto-update system settings",
|
||||||
"updates:",
|
"updates:",
|
||||||
@ -319,9 +334,10 @@ class ConfigManager:
|
|||||||
f" auto_install: {str(config_dict['updates']['auto_install']).lower()} # Auto-install updates (not recommended)",
|
f" auto_install: {str(config_dict['updates']['auto_install']).lower()} # Auto-install updates (not recommended)",
|
||||||
f" backup_before_update: {str(config_dict['updates']['backup_before_update']).lower()} # Create backup before updating",
|
f" backup_before_update: {str(config_dict['updates']['backup_before_update']).lower()} # Create backup before updating",
|
||||||
f" notify_beta_releases: {str(config_dict['updates']['notify_beta_releases']).lower()} # Include beta releases in checks",
|
f" notify_beta_releases: {str(config_dict['updates']['notify_beta_releases']).lower()} # Include beta releases in checks",
|
||||||
])
|
]
|
||||||
|
)
|
||||||
|
|
||||||
return '\n'.join(yaml_lines)
|
return "\n".join(yaml_lines)
|
||||||
|
|
||||||
def update_config(self, **kwargs) -> RAGConfig:
|
def update_config(self, **kwargs) -> RAGConfig:
|
||||||
"""Update specific configuration values."""
|
"""Update specific configuration values."""
|
||||||
|
|||||||
@ -9,35 +9,43 @@ Perfect for exploring codebases with detailed reasoning and follow-up questions.
|
|||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import time
|
import time
|
||||||
from typing import List, Dict, Any, Optional
|
|
||||||
from pathlib import Path
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
from .config import RAGConfig
|
||||||
from .llm_synthesizer import LLMSynthesizer, SynthesisResult
|
from .llm_synthesizer import LLMSynthesizer, SynthesisResult
|
||||||
from .search import CodeSearcher
|
from .search import CodeSearcher
|
||||||
from .config import RAGConfig
|
|
||||||
from .system_context import get_system_context
|
from .system_context import get_system_context
|
||||||
except ImportError:
|
except ImportError:
|
||||||
# For direct testing
|
# For direct testing
|
||||||
|
from config import RAGConfig
|
||||||
from llm_synthesizer import LLMSynthesizer, SynthesisResult
|
from llm_synthesizer import LLMSynthesizer, SynthesisResult
|
||||||
from search import CodeSearcher
|
from search import CodeSearcher
|
||||||
from config import RAGConfig
|
|
||||||
get_system_context = lambda x=None: ""
|
def get_system_context(x=None):
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class ExplorationSession:
|
class ExplorationSession:
|
||||||
"""Track an exploration session with context history."""
|
"""Track an exploration session with context history."""
|
||||||
|
|
||||||
project_path: Path
|
project_path: Path
|
||||||
conversation_history: List[Dict[str, Any]]
|
conversation_history: List[Dict[str, Any]]
|
||||||
session_id: str
|
session_id: str
|
||||||
started_at: float
|
started_at: float
|
||||||
|
|
||||||
def add_exchange(self, question: str, search_results: List[Any], response: SynthesisResult):
|
def add_exchange(
|
||||||
|
self, question: str, search_results: List[Any], response: SynthesisResult
|
||||||
|
):
|
||||||
"""Add a question/response exchange to the conversation history."""
|
"""Add a question/response exchange to the conversation history."""
|
||||||
self.conversation_history.append({
|
self.conversation_history.append(
|
||||||
|
{
|
||||||
"timestamp": time.time(),
|
"timestamp": time.time(),
|
||||||
"question": question,
|
"question": question,
|
||||||
"search_results_count": len(search_results),
|
"search_results_count": len(search_results),
|
||||||
@ -46,9 +54,11 @@ class ExplorationSession:
|
|||||||
"key_points": response.key_points,
|
"key_points": response.key_points,
|
||||||
"code_examples": response.code_examples,
|
"code_examples": response.code_examples,
|
||||||
"suggested_actions": response.suggested_actions,
|
"suggested_actions": response.suggested_actions,
|
||||||
"confidence": response.confidence
|
"confidence": response.confidence,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
})
|
)
|
||||||
|
|
||||||
|
|
||||||
class CodeExplorer:
|
class CodeExplorer:
|
||||||
"""Interactive code exploration with thinking and context memory."""
|
"""Interactive code exploration with thinking and context memory."""
|
||||||
@ -63,7 +73,7 @@ class CodeExplorer:
|
|||||||
ollama_url=f"http://{self.config.llm.ollama_host}",
|
ollama_url=f"http://{self.config.llm.ollama_host}",
|
||||||
model=self.config.llm.synthesis_model,
|
model=self.config.llm.synthesis_model,
|
||||||
enable_thinking=True, # Always enable thinking in explore mode
|
enable_thinking=True, # Always enable thinking in explore mode
|
||||||
config=self.config # Pass config for model rankings
|
config=self.config, # Pass config for model rankings
|
||||||
)
|
)
|
||||||
|
|
||||||
# Session management
|
# Session management
|
||||||
@ -82,7 +92,7 @@ class CodeExplorer:
|
|||||||
project_path=self.project_path,
|
project_path=self.project_path,
|
||||||
conversation_history=[],
|
conversation_history=[],
|
||||||
session_id=session_id,
|
session_id=session_id,
|
||||||
started_at=time.time()
|
started_at=time.time(),
|
||||||
)
|
)
|
||||||
|
|
||||||
print("🧠 Exploration Mode Started")
|
print("🧠 Exploration Mode Started")
|
||||||
@ -102,7 +112,7 @@ class CodeExplorer:
|
|||||||
top_k=context_limit,
|
top_k=context_limit,
|
||||||
include_context=True,
|
include_context=True,
|
||||||
semantic_weight=0.7,
|
semantic_weight=0.7,
|
||||||
bm25_weight=0.3
|
bm25_weight=0.3,
|
||||||
)
|
)
|
||||||
search_time = time.time() - search_start
|
search_time = time.time() - search_start
|
||||||
|
|
||||||
@ -128,7 +138,6 @@ class CodeExplorer:
|
|||||||
def _build_contextual_prompt(self, question: str, results: List[Any]) -> str:
|
def _build_contextual_prompt(self, question: str, results: List[Any]) -> str:
|
||||||
"""Build a prompt that includes conversation context."""
|
"""Build a prompt that includes conversation context."""
|
||||||
# Get recent conversation context (last 3 exchanges)
|
# Get recent conversation context (last 3 exchanges)
|
||||||
context_summary = ""
|
|
||||||
if self.current_session.conversation_history:
|
if self.current_session.conversation_history:
|
||||||
recent_exchanges = self.current_session.conversation_history[-3:]
|
recent_exchanges = self.current_session.conversation_history[-3:]
|
||||||
context_parts = []
|
context_parts = []
|
||||||
@ -139,28 +148,30 @@ class CodeExplorer:
|
|||||||
context_parts.append(f"Previous Q{i}: {prev_q}")
|
context_parts.append(f"Previous Q{i}: {prev_q}")
|
||||||
context_parts.append(f"Previous A{i}: {prev_summary}")
|
context_parts.append(f"Previous A{i}: {prev_summary}")
|
||||||
|
|
||||||
context_summary = "\n".join(context_parts)
|
# "\n".join(context_parts) # Unused variable removed
|
||||||
|
|
||||||
# Build search results context
|
# Build search results context
|
||||||
results_context = []
|
results_context = []
|
||||||
for i, result in enumerate(results[:8], 1):
|
for i, result in enumerate(results[:8], 1):
|
||||||
file_path = result.file_path if hasattr(result, 'file_path') else 'unknown'
|
# result.file_path if hasattr(result, "file_path") else "unknown" # Unused variable removed
|
||||||
content = result.content if hasattr(result, 'content') else str(result)
|
# result.content if hasattr(result, "content") else str(result) # Unused variable removed
|
||||||
score = result.score if hasattr(result, 'score') else 0.0
|
# result.score if hasattr(result, "score") else 0.0 # Unused variable removed
|
||||||
|
|
||||||
results_context.append(f"""
|
results_context.append(
|
||||||
|
"""
|
||||||
Result {i} (Score: {score:.3f}):
|
Result {i} (Score: {score:.3f}):
|
||||||
File: {file_path}
|
File: {file_path}
|
||||||
Content: {content[:800]}{'...' if len(content) > 800 else ''}
|
Content: {content[:800]}{'...' if len(content) > 800 else ''}
|
||||||
""")
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
results_text = "\n".join(results_context)
|
# "\n".join(results_context) # Unused variable removed
|
||||||
|
|
||||||
# Get system context for better responses
|
# Get system context for better responses
|
||||||
system_context = get_system_context(self.project_path)
|
# get_system_context(self.project_path) # Unused variable removed
|
||||||
|
|
||||||
# Create comprehensive exploration prompt with thinking
|
# Create comprehensive exploration prompt with thinking
|
||||||
prompt = f"""<think>
|
prompt = """<think>
|
||||||
The user asked: "{question}"
|
The user asked: "{question}"
|
||||||
|
|
||||||
System context: {system_context}
|
System context: {system_context}
|
||||||
@ -217,8 +228,14 @@ Guidelines:
|
|||||||
"""Synthesize results with full context and thinking."""
|
"""Synthesize results with full context and thinking."""
|
||||||
try:
|
try:
|
||||||
# Use streaming with thinking visible (don't collapse)
|
# Use streaming with thinking visible (don't collapse)
|
||||||
response = self.synthesizer._call_ollama(prompt, temperature=0.2, disable_thinking=False, use_streaming=True, collapse_thinking=False)
|
response = self.synthesizer._call_ollama(
|
||||||
thinking_stream = ""
|
prompt,
|
||||||
|
temperature=0.2,
|
||||||
|
disable_thinking=False,
|
||||||
|
use_streaming=True,
|
||||||
|
collapse_thinking=False,
|
||||||
|
)
|
||||||
|
# "" # Unused variable removed
|
||||||
|
|
||||||
# Streaming already shows thinking and response
|
# Streaming already shows thinking and response
|
||||||
# No need for additional indicators
|
# No need for additional indicators
|
||||||
@ -229,7 +246,7 @@ Guidelines:
|
|||||||
key_points=[],
|
key_points=[],
|
||||||
code_examples=[],
|
code_examples=[],
|
||||||
suggested_actions=["Check LLM service status"],
|
suggested_actions=["Check LLM service status"],
|
||||||
confidence=0.0
|
confidence=0.0,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Use natural language response directly
|
# Use natural language response directly
|
||||||
@ -238,7 +255,7 @@ Guidelines:
|
|||||||
key_points=[], # Not used with natural language responses
|
key_points=[], # Not used with natural language responses
|
||||||
code_examples=[], # Not used with natural language responses
|
code_examples=[], # Not used with natural language responses
|
||||||
suggested_actions=[], # Not used with natural language responses
|
suggested_actions=[], # Not used with natural language responses
|
||||||
confidence=0.85 # High confidence for natural responses
|
confidence=0.85, # High confidence for natural responses
|
||||||
)
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -248,11 +265,17 @@ Guidelines:
|
|||||||
key_points=[],
|
key_points=[],
|
||||||
code_examples=[],
|
code_examples=[],
|
||||||
suggested_actions=["Check system status and try again"],
|
suggested_actions=["Check system status and try again"],
|
||||||
confidence=0.0
|
confidence=0.0,
|
||||||
)
|
)
|
||||||
|
|
||||||
def _format_exploration_response(self, question: str, synthesis: SynthesisResult,
|
def _format_exploration_response(
|
||||||
result_count: int, search_time: float, synthesis_time: float) -> str:
|
self,
|
||||||
|
question: str,
|
||||||
|
synthesis: SynthesisResult,
|
||||||
|
result_count: int,
|
||||||
|
search_time: float,
|
||||||
|
synthesis_time: float,
|
||||||
|
) -> str:
|
||||||
"""Format exploration response with context indicators."""
|
"""Format exploration response with context indicators."""
|
||||||
|
|
||||||
output = []
|
output = []
|
||||||
@ -262,8 +285,10 @@ Guidelines:
|
|||||||
exchange_count = len(self.current_session.conversation_history)
|
exchange_count = len(self.current_session.conversation_history)
|
||||||
|
|
||||||
output.append(f"🧠 EXPLORATION ANALYSIS (Question #{exchange_count})")
|
output.append(f"🧠 EXPLORATION ANALYSIS (Question #{exchange_count})")
|
||||||
output.append(f"Session: {session_duration/60:.1f}m | Results: {result_count} | "
|
output.append(
|
||||||
f"Time: {search_time+synthesis_time:.1f}s")
|
f"Session: {session_duration/60:.1f}m | Results: {result_count} | "
|
||||||
|
f"Time: {search_time+synthesis_time:.1f}s"
|
||||||
|
)
|
||||||
output.append("=" * 60)
|
output.append("=" * 60)
|
||||||
output.append("")
|
output.append("")
|
||||||
|
|
||||||
@ -274,9 +299,17 @@ Guidelines:
|
|||||||
output.append("")
|
output.append("")
|
||||||
|
|
||||||
# Confidence and context indicator
|
# Confidence and context indicator
|
||||||
confidence_emoji = "🟢" if synthesis.confidence > 0.7 else "🟡" if synthesis.confidence > 0.4 else "🔴"
|
confidence_emoji = (
|
||||||
context_indicator = f" | Context: {exchange_count-1} previous questions" if exchange_count > 1 else ""
|
"🟢"
|
||||||
output.append(f"{confidence_emoji} Confidence: {synthesis.confidence:.1%}{context_indicator}")
|
if synthesis.confidence > 0.7
|
||||||
|
else "🟡" if synthesis.confidence > 0.4 else "🔴"
|
||||||
|
)
|
||||||
|
context_indicator = (
|
||||||
|
f" | Context: {exchange_count-1} previous questions" if exchange_count > 1 else ""
|
||||||
|
)
|
||||||
|
output.append(
|
||||||
|
f"{confidence_emoji} Confidence: {synthesis.confidence:.1%}{context_indicator}"
|
||||||
|
)
|
||||||
|
|
||||||
return "\n".join(output)
|
return "\n".join(output)
|
||||||
|
|
||||||
@ -289,19 +322,23 @@ Guidelines:
|
|||||||
exchange_count = len(self.current_session.conversation_history)
|
exchange_count = len(self.current_session.conversation_history)
|
||||||
|
|
||||||
summary = [
|
summary = [
|
||||||
f"🧠 EXPLORATION SESSION SUMMARY",
|
"🧠 EXPLORATION SESSION SUMMARY",
|
||||||
f"=" * 40,
|
"=" * 40,
|
||||||
f"Project: {self.project_path.name}",
|
f"Project: {self.project_path.name}",
|
||||||
f"Session ID: {self.current_session.session_id}",
|
f"Session ID: {self.current_session.session_id}",
|
||||||
f"Duration: {duration/60:.1f} minutes",
|
f"Duration: {duration/60:.1f} minutes",
|
||||||
f"Questions explored: {exchange_count}",
|
f"Questions explored: {exchange_count}",
|
||||||
f"",
|
"",
|
||||||
]
|
]
|
||||||
|
|
||||||
if exchange_count > 0:
|
if exchange_count > 0:
|
||||||
summary.append("📋 Topics explored:")
|
summary.append("📋 Topics explored:")
|
||||||
for i, exchange in enumerate(self.current_session.conversation_history, 1):
|
for i, exchange in enumerate(self.current_session.conversation_history, 1):
|
||||||
question = exchange["question"][:50] + "..." if len(exchange["question"]) > 50 else exchange["question"]
|
question = (
|
||||||
|
exchange["question"][:50] + "..."
|
||||||
|
if len(exchange["question"]) > 50
|
||||||
|
else exchange["question"]
|
||||||
|
)
|
||||||
confidence = exchange["response"]["confidence"]
|
confidence = exchange["response"]["confidence"]
|
||||||
summary.append(f" {i}. {question} (confidence: {confidence:.1%})")
|
summary.append(f" {i}. {question} (confidence: {confidence:.1%})")
|
||||||
|
|
||||||
@ -325,9 +362,7 @@ Guidelines:
|
|||||||
|
|
||||||
# Test with a simple thinking prompt to see response quality
|
# Test with a simple thinking prompt to see response quality
|
||||||
test_response = self.synthesizer._call_ollama(
|
test_response = self.synthesizer._call_ollama(
|
||||||
"Think briefly: what is 2+2?",
|
"Think briefly: what is 2+2?", temperature=0.1, disable_thinking=False
|
||||||
temperature=0.1,
|
|
||||||
disable_thinking=False
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if test_response:
|
if test_response:
|
||||||
@ -343,24 +378,35 @@ Guidelines:
|
|||||||
def _handle_model_restart(self) -> bool:
|
def _handle_model_restart(self) -> bool:
|
||||||
"""Handle user confirmation and model restart."""
|
"""Handle user confirmation and model restart."""
|
||||||
try:
|
try:
|
||||||
print("\n🤔 To ensure best thinking quality, exploration mode works best with a fresh model.")
|
print(
|
||||||
|
"\n🤔 To ensure best thinking quality, exploration mode works best with a fresh model."
|
||||||
|
)
|
||||||
print(f" Currently running: {self.synthesizer.model}")
|
print(f" Currently running: {self.synthesizer.model}")
|
||||||
print("\n💡 Stop current model and restart for optimal exploration? (y/N): ", end="", flush=True)
|
print(
|
||||||
|
"\n💡 Stop current model and restart for optimal exploration? (y/N): ",
|
||||||
|
end="",
|
||||||
|
flush=True,
|
||||||
|
)
|
||||||
|
|
||||||
response = input().strip().lower()
|
response = input().strip().lower()
|
||||||
|
|
||||||
if response in ['y', 'yes']:
|
if response in ["y", "yes"]:
|
||||||
print("\n🔄 Stopping current model...")
|
print("\n🔄 Stopping current model...")
|
||||||
|
|
||||||
# Use ollama stop command for clean model restart
|
# Use ollama stop command for clean model restart
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
try:
|
try:
|
||||||
subprocess.run([
|
subprocess.run(
|
||||||
"ollama", "stop", self.synthesizer.model
|
["ollama", "stop", self.synthesizer.model],
|
||||||
], timeout=10, capture_output=True)
|
timeout=10,
|
||||||
|
capture_output=True,
|
||||||
|
)
|
||||||
|
|
||||||
print("✅ Model stopped successfully.")
|
print("✅ Model stopped successfully.")
|
||||||
print("🚀 Exploration mode will restart the model with thinking enabled...")
|
print(
|
||||||
|
"🚀 Exploration mode will restart the model with thinking enabled..."
|
||||||
|
)
|
||||||
|
|
||||||
# Reset synthesizer initialization to force fresh start
|
# Reset synthesizer initialization to force fresh start
|
||||||
self.synthesizer._initialized = False
|
self.synthesizer._initialized = False
|
||||||
@ -389,7 +435,6 @@ Guidelines:
|
|||||||
def _call_ollama_with_thinking(self, prompt: str, temperature: float = 0.3) -> tuple:
|
def _call_ollama_with_thinking(self, prompt: str, temperature: float = 0.3) -> tuple:
|
||||||
"""Call Ollama with streaming for fast time-to-first-token."""
|
"""Call Ollama with streaming for fast time-to-first-token."""
|
||||||
import requests
|
import requests
|
||||||
import json
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Use the synthesizer's model and connection
|
# Use the synthesizer's model and connection
|
||||||
@ -405,6 +450,7 @@ Guidelines:
|
|||||||
|
|
||||||
# Get optimal parameters for this model
|
# Get optimal parameters for this model
|
||||||
from .llm_optimization import get_optimal_ollama_parameters
|
from .llm_optimization import get_optimal_ollama_parameters
|
||||||
|
|
||||||
optimal_params = get_optimal_ollama_parameters(model_to_use)
|
optimal_params = get_optimal_ollama_parameters(model_to_use)
|
||||||
|
|
||||||
payload = {
|
payload = {
|
||||||
@ -418,15 +464,15 @@ Guidelines:
|
|||||||
"num_ctx": self.synthesizer._get_optimal_context_size(model_to_use),
|
"num_ctx": self.synthesizer._get_optimal_context_size(model_to_use),
|
||||||
"num_predict": optimal_params.get("num_predict", 2000),
|
"num_predict": optimal_params.get("num_predict", 2000),
|
||||||
"repeat_penalty": optimal_params.get("repeat_penalty", 1.1),
|
"repeat_penalty": optimal_params.get("repeat_penalty", 1.1),
|
||||||
"presence_penalty": optimal_params.get("presence_penalty", 1.0)
|
"presence_penalty": optimal_params.get("presence_penalty", 1.0),
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
response = requests.post(
|
response = requests.post(
|
||||||
f"{self.synthesizer.ollama_url}/api/generate",
|
f"{self.synthesizer.ollama_url}/api/generate",
|
||||||
json=payload,
|
json=payload,
|
||||||
stream=True,
|
stream=True,
|
||||||
timeout=65
|
timeout=65,
|
||||||
)
|
)
|
||||||
|
|
||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
@ -437,14 +483,14 @@ Guidelines:
|
|||||||
for line in response.iter_lines():
|
for line in response.iter_lines():
|
||||||
if line:
|
if line:
|
||||||
try:
|
try:
|
||||||
chunk_data = json.loads(line.decode('utf-8'))
|
chunk_data = json.loads(line.decode("utf-8"))
|
||||||
chunk_text = chunk_data.get('response', '')
|
chunk_text = chunk_data.get("response", "")
|
||||||
|
|
||||||
if chunk_text:
|
if chunk_text:
|
||||||
raw_response += chunk_text
|
raw_response += chunk_text
|
||||||
|
|
||||||
# Display thinking stream as it comes in
|
# Display thinking stream as it comes in
|
||||||
if not thinking_displayed and '<think>' in raw_response:
|
if not thinking_displayed and "<think>" in raw_response:
|
||||||
# Start displaying thinking
|
# Start displaying thinking
|
||||||
self._start_thinking_display()
|
self._start_thinking_display()
|
||||||
thinking_displayed = True
|
thinking_displayed = True
|
||||||
@ -452,7 +498,7 @@ Guidelines:
|
|||||||
if thinking_displayed:
|
if thinking_displayed:
|
||||||
self._stream_thinking_chunk(chunk_text)
|
self._stream_thinking_chunk(chunk_text)
|
||||||
|
|
||||||
if chunk_data.get('done', False):
|
if chunk_data.get("done", False):
|
||||||
break
|
break
|
||||||
|
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
@ -485,7 +531,7 @@ Guidelines:
|
|||||||
end_tag = raw_response.find("</think>") + len("</think>")
|
end_tag = raw_response.find("</think>") + len("</think>")
|
||||||
|
|
||||||
if start_tag != -1 and end_tag != -1:
|
if start_tag != -1 and end_tag != -1:
|
||||||
thinking_content = raw_response[start_tag + 7:end_tag - 8] # Remove tags
|
thinking_content = raw_response[start_tag + 7 : end_tag - 8] # Remove tags
|
||||||
thinking_stream = thinking_content.strip()
|
thinking_stream = thinking_content.strip()
|
||||||
|
|
||||||
# Remove thinking from final response
|
# Remove thinking from final response
|
||||||
@ -494,18 +540,26 @@ Guidelines:
|
|||||||
# Alternative patterns for models that use different thinking formats
|
# Alternative patterns for models that use different thinking formats
|
||||||
elif "Let me think" in raw_response or "I need to analyze" in raw_response:
|
elif "Let me think" in raw_response or "I need to analyze" in raw_response:
|
||||||
# Simple heuristic: first paragraph might be thinking
|
# Simple heuristic: first paragraph might be thinking
|
||||||
lines = raw_response.split('\n')
|
lines = raw_response.split("\n")
|
||||||
potential_thinking = []
|
potential_thinking = []
|
||||||
final_lines = []
|
final_lines = []
|
||||||
|
|
||||||
thinking_indicators = ["Let me think", "I need to", "First, I'll", "Looking at", "Analyzing"]
|
thinking_indicators = [
|
||||||
|
"Let me think",
|
||||||
|
"I need to",
|
||||||
|
"First, I'll",
|
||||||
|
"Looking at",
|
||||||
|
"Analyzing",
|
||||||
|
]
|
||||||
in_thinking = False
|
in_thinking = False
|
||||||
|
|
||||||
for line in lines:
|
for line in lines:
|
||||||
if any(indicator in line for indicator in thinking_indicators):
|
if any(indicator in line for indicator in thinking_indicators):
|
||||||
in_thinking = True
|
in_thinking = True
|
||||||
potential_thinking.append(line)
|
potential_thinking.append(line)
|
||||||
elif in_thinking and (line.startswith('{') or line.startswith('**') or line.startswith('#')):
|
elif in_thinking and (
|
||||||
|
line.startswith("{") or line.startswith("**") or line.startswith("#")
|
||||||
|
):
|
||||||
# Likely end of thinking, start of structured response
|
# Likely end of thinking, start of structured response
|
||||||
in_thinking = False
|
in_thinking = False
|
||||||
final_lines.append(line)
|
final_lines.append(line)
|
||||||
@ -515,8 +569,8 @@ Guidelines:
|
|||||||
final_lines.append(line)
|
final_lines.append(line)
|
||||||
|
|
||||||
if potential_thinking:
|
if potential_thinking:
|
||||||
thinking_stream = '\n'.join(potential_thinking).strip()
|
thinking_stream = "\n".join(potential_thinking).strip()
|
||||||
final_response = '\n'.join(final_lines).strip()
|
final_response = "\n".join(final_lines).strip()
|
||||||
|
|
||||||
return thinking_stream, final_response
|
return thinking_stream, final_response
|
||||||
|
|
||||||
@ -529,28 +583,27 @@ Guidelines:
|
|||||||
|
|
||||||
def _stream_thinking_chunk(self, chunk: str):
|
def _stream_thinking_chunk(self, chunk: str):
|
||||||
"""Stream a chunk of thinking as it arrives."""
|
"""Stream a chunk of thinking as it arrives."""
|
||||||
import sys
|
|
||||||
|
|
||||||
self._thinking_buffer += chunk
|
self._thinking_buffer += chunk
|
||||||
|
|
||||||
# Check if we're in thinking tags
|
# Check if we're in thinking tags
|
||||||
if '<think>' in self._thinking_buffer and not self._in_thinking_tags:
|
if "<think>" in self._thinking_buffer and not self._in_thinking_tags:
|
||||||
self._in_thinking_tags = True
|
self._in_thinking_tags = True
|
||||||
# Display everything after <think>
|
# Display everything after <think>
|
||||||
start_idx = self._thinking_buffer.find('<think>') + 7
|
start_idx = self._thinking_buffer.find("<think>") + 7
|
||||||
thinking_content = self._thinking_buffer[start_idx:]
|
thinking_content = self._thinking_buffer[start_idx:]
|
||||||
if thinking_content:
|
if thinking_content:
|
||||||
print(f"\033[2m\033[3m{thinking_content}\033[0m", end='', flush=True)
|
print(f"\033[2m\033[3m{thinking_content}\033[0m", end="", flush=True)
|
||||||
elif self._in_thinking_tags and '</think>' not in chunk:
|
elif self._in_thinking_tags and "</think>" not in chunk:
|
||||||
# We're in thinking mode, display the chunk
|
# We're in thinking mode, display the chunk
|
||||||
print(f"\033[2m\033[3m{chunk}\033[0m", end='', flush=True)
|
print(f"\033[2m\033[3m{chunk}\033[0m", end="", flush=True)
|
||||||
elif '</think>' in self._thinking_buffer:
|
elif "</think>" in self._thinking_buffer:
|
||||||
# End of thinking
|
# End of thinking
|
||||||
self._in_thinking_tags = False
|
self._in_thinking_tags = False
|
||||||
|
|
||||||
def _end_thinking_display(self):
|
def _end_thinking_display(self):
|
||||||
"""End the thinking stream display."""
|
"""End the thinking stream display."""
|
||||||
print(f"\n\033[2m\033[3m" + "─" * 40 + "\033[0m")
|
print("\n\033[2m\033[3m" + "─" * 40 + "\033[0m")
|
||||||
print()
|
print()
|
||||||
|
|
||||||
def _display_thinking_stream(self, thinking_stream: str):
|
def _display_thinking_stream(self, thinking_stream: str):
|
||||||
@ -562,11 +615,11 @@ Guidelines:
|
|||||||
print("\033[2m\033[3m" + "─" * 40 + "\033[0m")
|
print("\033[2m\033[3m" + "─" * 40 + "\033[0m")
|
||||||
|
|
||||||
# Split into paragraphs and display with proper formatting
|
# Split into paragraphs and display with proper formatting
|
||||||
paragraphs = thinking_stream.split('\n\n')
|
paragraphs = thinking_stream.split("\n\n")
|
||||||
for para in paragraphs:
|
for para in paragraphs:
|
||||||
if para.strip():
|
if para.strip():
|
||||||
# Wrap long lines nicely
|
# Wrap long lines nicely
|
||||||
lines = para.strip().split('\n')
|
lines = para.strip().split("\n")
|
||||||
for line in lines:
|
for line in lines:
|
||||||
if line.strip():
|
if line.strip():
|
||||||
# Light gray and italic
|
# Light gray and italic
|
||||||
@ -576,7 +629,10 @@ Guidelines:
|
|||||||
print("\033[2m\033[3m" + "─" * 40 + "\033[0m")
|
print("\033[2m\033[3m" + "─" * 40 + "\033[0m")
|
||||||
print()
|
print()
|
||||||
|
|
||||||
|
|
||||||
# Quick test function
|
# Quick test function
|
||||||
|
|
||||||
|
|
||||||
def test_explorer():
|
def test_explorer():
|
||||||
"""Test the code explorer."""
|
"""Test the code explorer."""
|
||||||
explorer = CodeExplorer(Path("."))
|
explorer = CodeExplorer(Path("."))
|
||||||
@ -592,5 +648,6 @@ def test_explorer():
|
|||||||
|
|
||||||
print("\n" + explorer.end_session())
|
print("\n" + explorer.end_session())
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
test_explorer()
|
test_explorer()
|
||||||
@ -12,40 +12,47 @@ Drop-in replacement for the original server with:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
import socket
|
import socket
|
||||||
import threading
|
|
||||||
import time
|
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import os
|
import threading
|
||||||
import logging
|
import time
|
||||||
|
from concurrent.futures import Future, ThreadPoolExecutor
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Dict, Any, Optional, Callable
|
from typing import Any, Callable, Dict, Optional
|
||||||
from datetime import datetime
|
|
||||||
from concurrent.futures import ThreadPoolExecutor, Future
|
from rich import print as rprint
|
||||||
import queue
|
|
||||||
|
|
||||||
# Rich console for beautiful output
|
# Rich console for beautiful output
|
||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn, TimeRemainingColumn, MofNCompleteColumn
|
|
||||||
from rich.panel import Panel
|
|
||||||
from rich.table import Table
|
|
||||||
from rich.live import Live
|
from rich.live import Live
|
||||||
from rich import print as rprint
|
from rich.panel import Panel
|
||||||
|
from rich.progress import (
|
||||||
|
BarColumn,
|
||||||
|
MofNCompleteColumn,
|
||||||
|
Progress,
|
||||||
|
SpinnerColumn,
|
||||||
|
TextColumn,
|
||||||
|
TimeRemainingColumn,
|
||||||
|
)
|
||||||
|
from rich.table import Table
|
||||||
|
|
||||||
# Fix Windows console first
|
# Fix Windows console first
|
||||||
if sys.platform == 'win32':
|
if sys.platform == "win32":
|
||||||
os.environ['PYTHONUTF8'] = '1'
|
os.environ["PYTHONUTF8"] = "1"
|
||||||
try:
|
try:
|
||||||
from .windows_console_fix import fix_windows_console
|
from .windows_console_fix import fix_windows_console
|
||||||
|
|
||||||
fix_windows_console()
|
fix_windows_console()
|
||||||
except:
|
except (ImportError, OSError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
from .search import CodeSearcher
|
|
||||||
from .ollama_embeddings import OllamaEmbedder as CodeEmbedder
|
|
||||||
from .indexer import ProjectIndexer
|
from .indexer import ProjectIndexer
|
||||||
|
from .ollama_embeddings import OllamaEmbedder as CodeEmbedder
|
||||||
from .performance import PerformanceMonitor
|
from .performance import PerformanceMonitor
|
||||||
|
from .search import CodeSearcher
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
console = Console()
|
console = Console()
|
||||||
@ -89,14 +96,14 @@ class ServerStatus:
|
|||||||
def get_status(self) -> Dict[str, Any]:
|
def get_status(self) -> Dict[str, Any]:
|
||||||
"""Get complete status as dict"""
|
"""Get complete status as dict"""
|
||||||
return {
|
return {
|
||||||
'phase': self.phase,
|
"phase": self.phase,
|
||||||
'progress': self.progress,
|
"progress": self.progress,
|
||||||
'message': self.message,
|
"message": self.message,
|
||||||
'ready': self.ready,
|
"ready": self.ready,
|
||||||
'error': self.error,
|
"error": self.error,
|
||||||
'uptime': time.time() - self.start_time,
|
"uptime": time.time() - self.start_time,
|
||||||
'health_checks': self.health_checks,
|
"health_checks": self.health_checks,
|
||||||
'details': self.details
|
"details": self.details,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -151,7 +158,7 @@ class FastRAGServer:
|
|||||||
# Quick port check first
|
# Quick port check first
|
||||||
test_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
test_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
test_sock.settimeout(1.0) # Faster timeout
|
test_sock.settimeout(1.0) # Faster timeout
|
||||||
result = test_sock.connect_ex(('localhost', self.port))
|
result = test_sock.connect_ex(("localhost", self.port))
|
||||||
test_sock.close()
|
test_sock.close()
|
||||||
|
|
||||||
if result != 0: # Port is free
|
if result != 0: # Port is free
|
||||||
@ -161,36 +168,43 @@ class FastRAGServer:
|
|||||||
self.status.update("port_cleanup", 10, f"Clearing port {self.port}...")
|
self.status.update("port_cleanup", 10, f"Clearing port {self.port}...")
|
||||||
self._notify_status()
|
self._notify_status()
|
||||||
|
|
||||||
if sys.platform == 'win32':
|
if sys.platform == "win32":
|
||||||
# Windows: Enhanced process killing
|
# Windows: Enhanced process killing
|
||||||
cmd = ['netstat', '-ano']
|
cmd = ["netstat", "-ano"]
|
||||||
result = subprocess.run(cmd, capture_output=True, text=True, timeout=5)
|
result = subprocess.run(cmd, capture_output=True, text=True, timeout=5)
|
||||||
|
|
||||||
for line in result.stdout.split('\n'):
|
for line in result.stdout.split("\n"):
|
||||||
if f':{self.port}' in line and 'LISTENING' in line:
|
if f":{self.port}" in line and "LISTENING" in line:
|
||||||
parts = line.split()
|
parts = line.split()
|
||||||
if len(parts) >= 5:
|
if len(parts) >= 5:
|
||||||
pid = parts[-1]
|
pid = parts[-1]
|
||||||
console.print(f"[dim]Killing process {pid}[/dim]")
|
console.print(f"[dim]Killing process {pid}[/dim]")
|
||||||
subprocess.run(['taskkill', '/PID', pid, '/F'],
|
subprocess.run(
|
||||||
capture_output=True, timeout=3)
|
["taskkill", "/PID", pid, "/F"],
|
||||||
|
capture_output=True,
|
||||||
|
timeout=3,
|
||||||
|
)
|
||||||
time.sleep(0.5) # Reduced wait time
|
time.sleep(0.5) # Reduced wait time
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
# Unix/Linux: Enhanced process killing
|
# Unix/Linux: Enhanced process killing
|
||||||
result = subprocess.run(['lsof', '-ti', f':{self.port}'],
|
result = subprocess.run(
|
||||||
capture_output=True, text=True, timeout=3)
|
["lso", "-ti", f":{self.port}"],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=3,
|
||||||
|
)
|
||||||
if result.stdout.strip():
|
if result.stdout.strip():
|
||||||
pids = result.stdout.strip().split()
|
pids = result.stdout.strip().split()
|
||||||
for pid in pids:
|
for pid in pids:
|
||||||
console.print(f"[dim]Killing process {pid}[/dim]")
|
console.print(f"[dim]Killing process {pid}[/dim]")
|
||||||
subprocess.run(['kill', '-9', pid], capture_output=True)
|
subprocess.run(["kill", "-9", pid], capture_output=True)
|
||||||
time.sleep(0.5)
|
time.sleep(0.5)
|
||||||
|
|
||||||
# Verify port is free
|
# Verify port is free
|
||||||
test_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
test_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
test_sock.settimeout(1.0)
|
test_sock.settimeout(1.0)
|
||||||
result = test_sock.connect_ex(('localhost', self.port))
|
result = test_sock.connect_ex(("localhost", self.port))
|
||||||
test_sock.close()
|
test_sock.close()
|
||||||
|
|
||||||
if result == 0:
|
if result == 0:
|
||||||
@ -206,12 +220,12 @@ class FastRAGServer:
|
|||||||
|
|
||||||
def _check_indexing_needed(self) -> bool:
|
def _check_indexing_needed(self) -> bool:
|
||||||
"""Quick check if indexing is needed"""
|
"""Quick check if indexing is needed"""
|
||||||
rag_dir = self.project_path / '.mini-rag'
|
rag_dir = self.project_path / ".mini-rag"
|
||||||
if not rag_dir.exists():
|
if not rag_dir.exists():
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# Check if database exists and is not empty
|
# Check if database exists and is not empty
|
||||||
db_path = rag_dir / 'code_vectors.lance'
|
db_path = rag_dir / "code_vectors.lance"
|
||||||
if not db_path.exists():
|
if not db_path.exists():
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@ -224,12 +238,12 @@ class FastRAGServer:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
db = lancedb.connect(rag_dir)
|
db = lancedb.connect(rag_dir)
|
||||||
if 'code_vectors' not in db.table_names():
|
if "code_vectors" not in db.table_names():
|
||||||
return True
|
return True
|
||||||
table = db.open_table('code_vectors')
|
table = db.open_table("code_vectors")
|
||||||
count = table.count_rows()
|
count = table.count_rows()
|
||||||
return count == 0
|
return count == 0
|
||||||
except:
|
except (OSError, IOError, ValueError, AttributeError):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _fast_index(self) -> bool:
|
def _fast_index(self) -> bool:
|
||||||
@ -242,7 +256,7 @@ class FastRAGServer:
|
|||||||
self.indexer = ProjectIndexer(
|
self.indexer = ProjectIndexer(
|
||||||
self.project_path,
|
self.project_path,
|
||||||
embedder=self.embedder, # Reuse loaded embedder
|
embedder=self.embedder, # Reuse loaded embedder
|
||||||
max_workers=min(4, os.cpu_count() or 2)
|
max_workers=min(4, os.cpu_count() or 2),
|
||||||
)
|
)
|
||||||
|
|
||||||
console.print("\n[bold cyan]🚀 Fast Indexing Starting...[/bold cyan]")
|
console.print("\n[bold cyan]🚀 Fast Indexing Starting...[/bold cyan]")
|
||||||
@ -267,11 +281,14 @@ class FastRAGServer:
|
|||||||
|
|
||||||
if total_files == 0:
|
if total_files == 0:
|
||||||
self.status.update("indexing", 80, "Index up to date")
|
self.status.update("indexing", 80, "Index up to date")
|
||||||
return {'files_indexed': 0, 'chunks_created': 0, 'time_taken': 0}
|
return {
|
||||||
|
"files_indexed": 0,
|
||||||
|
"chunks_created": 0,
|
||||||
|
"time_taken": 0,
|
||||||
|
}
|
||||||
|
|
||||||
task = progress.add_task(
|
task = progress.add_task(
|
||||||
f"[cyan]Indexing {total_files} files...",
|
f"[cyan]Indexing {total_files} files...", total=total_files
|
||||||
total=total_files
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Track progress by hooking into the processor
|
# Track progress by hooking into the processor
|
||||||
@ -282,8 +299,11 @@ class FastRAGServer:
|
|||||||
while processed_count < total_files and self.running:
|
while processed_count < total_files and self.running:
|
||||||
time.sleep(0.1) # Fast polling
|
time.sleep(0.1) # Fast polling
|
||||||
current_progress = (processed_count / total_files) * 60 + 20
|
current_progress = (processed_count / total_files) * 60 + 20
|
||||||
self.status.update("indexing", current_progress,
|
self.status.update(
|
||||||
f"Indexed {processed_count}/{total_files} files")
|
"indexing",
|
||||||
|
current_progress,
|
||||||
|
f"Indexed {processed_count}/{total_files} files",
|
||||||
|
)
|
||||||
progress.update(task, completed=processed_count)
|
progress.update(task, completed=processed_count)
|
||||||
self._notify_status()
|
self._notify_status()
|
||||||
|
|
||||||
@ -314,13 +334,18 @@ class FastRAGServer:
|
|||||||
# Run indexing
|
# Run indexing
|
||||||
stats = self.indexer.index_project(force_reindex=False)
|
stats = self.indexer.index_project(force_reindex=False)
|
||||||
|
|
||||||
self.status.update("indexing", 80,
|
self.status.update(
|
||||||
|
"indexing",
|
||||||
|
80,
|
||||||
f"Indexed {stats.get('files_indexed', 0)} files, "
|
f"Indexed {stats.get('files_indexed', 0)} files, "
|
||||||
f"created {stats.get('chunks_created', 0)} chunks")
|
f"created {stats.get('chunks_created', 0)} chunks",
|
||||||
|
)
|
||||||
self._notify_status()
|
self._notify_status()
|
||||||
|
|
||||||
console.print(f"\n[green]✅ Indexing complete: {stats.get('files_indexed', 0)} files, "
|
console.print(
|
||||||
f"{stats.get('chunks_created', 0)} chunks in {stats.get('time_taken', 0):.1f}s[/green]")
|
f"\n[green]✅ Indexing complete: {stats.get('files_indexed', 0)} files, "
|
||||||
|
f"{stats.get('chunks_created', 0)} chunks in {stats.get('time_taken', 0):.1f}s[/green]"
|
||||||
|
)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@ -347,7 +372,9 @@ class FastRAGServer:
|
|||||||
) as progress:
|
) as progress:
|
||||||
|
|
||||||
# Task 1: Load embedder (this takes the most time)
|
# Task 1: Load embedder (this takes the most time)
|
||||||
embedder_task = progress.add_task("[cyan]Loading embedding model...", total=100)
|
embedder_task = progress.add_task(
|
||||||
|
"[cyan]Loading embedding model...", total=100
|
||||||
|
)
|
||||||
|
|
||||||
def load_embedder():
|
def load_embedder():
|
||||||
self.status.update("embedder", 25, "Loading embedding model...")
|
self.status.update("embedder", 25, "Loading embedding model...")
|
||||||
@ -401,46 +428,46 @@ class FastRAGServer:
|
|||||||
# Check 1: Embedder functionality
|
# Check 1: Embedder functionality
|
||||||
if self.embedder:
|
if self.embedder:
|
||||||
test_embedding = self.embedder.embed_code("def test(): pass")
|
test_embedding = self.embedder.embed_code("def test(): pass")
|
||||||
checks['embedder'] = {
|
checks["embedder"] = {
|
||||||
'status': 'healthy',
|
"status": "healthy",
|
||||||
'embedding_dim': len(test_embedding),
|
"embedding_dim": len(test_embedding),
|
||||||
'model': getattr(self.embedder, 'model_name', 'unknown')
|
"model": getattr(self.embedder, "model_name", "unknown"),
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
checks['embedder'] = {'status': 'missing'}
|
checks["embedder"] = {"status": "missing"}
|
||||||
|
|
||||||
# Check 2: Database connectivity
|
# Check 2: Database connectivity
|
||||||
if self.searcher:
|
if self.searcher:
|
||||||
stats = self.searcher.get_statistics()
|
stats = self.searcher.get_statistics()
|
||||||
checks['database'] = {
|
checks["database"] = {
|
||||||
'status': 'healthy',
|
"status": "healthy",
|
||||||
'chunks': stats.get('total_chunks', 0),
|
"chunks": stats.get("total_chunks", 0),
|
||||||
'languages': len(stats.get('languages', {}))
|
"languages": len(stats.get("languages", {})),
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
checks['database'] = {'status': 'missing'}
|
checks["database"] = {"status": "missing"}
|
||||||
|
|
||||||
# Check 3: Search functionality
|
# Check 3: Search functionality
|
||||||
if self.searcher:
|
if self.searcher:
|
||||||
test_results = self.searcher.search("test query", top_k=1)
|
test_results = self.searcher.search("test query", top_k=1)
|
||||||
checks['search'] = {
|
checks["search"] = {
|
||||||
'status': 'healthy',
|
"status": "healthy",
|
||||||
'test_results': len(test_results)
|
"test_results": len(test_results),
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
checks['search'] = {'status': 'unavailable'}
|
checks["search"] = {"status": "unavailable"}
|
||||||
|
|
||||||
# Check 4: Port availability
|
# Check 4: Port availability
|
||||||
try:
|
try:
|
||||||
test_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
test_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
test_sock.bind(('localhost', self.port))
|
test_sock.bind(("localhost", self.port))
|
||||||
test_sock.close()
|
test_sock.close()
|
||||||
checks['port'] = {'status': 'available'}
|
checks["port"] = {"status": "available"}
|
||||||
except:
|
except (ConnectionError, OSError, TypeError, ValueError, socket.error):
|
||||||
checks['port'] = {'status': 'occupied'}
|
checks["port"] = {"status": "occupied"}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
checks['health_check_error'] = str(e)
|
checks["health_check_error"] = str(e)
|
||||||
|
|
||||||
self.status.health_checks = checks
|
self.status.health_checks = checks
|
||||||
self.last_health_check = time.time()
|
self.last_health_check = time.time()
|
||||||
@ -452,10 +479,10 @@ class FastRAGServer:
|
|||||||
table.add_column("Details", style="dim")
|
table.add_column("Details", style="dim")
|
||||||
|
|
||||||
for component, info in checks.items():
|
for component, info in checks.items():
|
||||||
status = info.get('status', 'unknown')
|
status = info.get("status", "unknown")
|
||||||
details = ', '.join([f"{k}={v}" for k, v in info.items() if k != 'status'])
|
details = ", ".join([f"{k}={v}" for k, v in info.items() if k != "status"])
|
||||||
|
|
||||||
color = "green" if status in ['healthy', 'available'] else "yellow"
|
color = "green" if status in ["healthy", "available"] else "yellow"
|
||||||
table.add_row(component, f"[{color}]{status}[/{color}]", details)
|
table.add_row(component, f"[{color}]{status}[/{color}]", details)
|
||||||
|
|
||||||
console.print(table)
|
console.print(table)
|
||||||
@ -479,7 +506,7 @@ class FastRAGServer:
|
|||||||
|
|
||||||
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||||
self.socket.bind(('localhost', self.port))
|
self.socket.bind(("localhost", self.port))
|
||||||
self.socket.listen(10) # Increased backlog
|
self.socket.listen(10) # Increased backlog
|
||||||
|
|
||||||
self.running = True
|
self.running = True
|
||||||
@ -491,15 +518,15 @@ class FastRAGServer:
|
|||||||
|
|
||||||
# Display ready status
|
# Display ready status
|
||||||
panel = Panel(
|
panel = Panel(
|
||||||
f"[bold green]🎉 RAG Server Ready![/bold green]\n\n"
|
"[bold green]🎉 RAG Server Ready![/bold green]\n\n"
|
||||||
f"🌐 Address: localhost:{self.port}\n"
|
f"🌐 Address: localhost:{self.port}\n"
|
||||||
f"⚡ Startup Time: {total_time:.2f}s\n"
|
f"⚡ Startup Time: {total_time:.2f}s\n"
|
||||||
f"📁 Project: {self.project_path.name}\n"
|
f"📁 Project: {self.project_path.name}\n"
|
||||||
f"🧠 Model: {getattr(self.embedder, 'model_name', 'default')}\n"
|
f"🧠 Model: {getattr(self.embedder, 'model_name', 'default')}\n"
|
||||||
f"📊 Chunks Indexed: {self.status.health_checks.get('database', {}).get('chunks', 0)}\n\n"
|
f"📊 Chunks Indexed: {self.status.health_checks.get('database', {}).get('chunks', 0)}\n\n"
|
||||||
f"[dim]Ready to serve the development environment queries...[/dim]",
|
"[dim]Ready to serve the development environment queries...[/dim]",
|
||||||
title="🚀 Server Status",
|
title="🚀 Server Status",
|
||||||
border_style="green"
|
border_style="green",
|
||||||
)
|
)
|
||||||
console.print(panel)
|
console.print(panel)
|
||||||
|
|
||||||
@ -547,24 +574,21 @@ class FastRAGServer:
|
|||||||
request = json.loads(data)
|
request = json.loads(data)
|
||||||
|
|
||||||
# Handle different request types
|
# Handle different request types
|
||||||
if request.get('command') == 'shutdown':
|
if request.get("command") == "shutdown":
|
||||||
console.print("\n[yellow]🛑 Shutdown requested[/yellow]")
|
console.print("\n[yellow]🛑 Shutdown requested[/yellow]")
|
||||||
response = {'success': True, 'message': 'Server shutting down'}
|
response = {"success": True, "message": "Server shutting down"}
|
||||||
self._send_json(client, response)
|
self._send_json(client, response)
|
||||||
self.stop()
|
self.stop()
|
||||||
return
|
return
|
||||||
|
|
||||||
if request.get('command') == 'status':
|
if request.get("command") == "status":
|
||||||
response = {
|
response = {"success": True, "status": self.status.get_status()}
|
||||||
'success': True,
|
|
||||||
'status': self.status.get_status()
|
|
||||||
}
|
|
||||||
self._send_json(client, response)
|
self._send_json(client, response)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Handle search requests
|
# Handle search requests
|
||||||
query = request.get('query', '')
|
query = request.get("query", "")
|
||||||
top_k = request.get('top_k', 10)
|
top_k = request.get("top_k", 10)
|
||||||
|
|
||||||
if not query:
|
if not query:
|
||||||
raise ValueError("Empty query")
|
raise ValueError("Empty query")
|
||||||
@ -572,7 +596,9 @@ class FastRAGServer:
|
|||||||
self.query_count += 1
|
self.query_count += 1
|
||||||
|
|
||||||
# Enhanced query logging
|
# Enhanced query logging
|
||||||
console.print(f"[blue]🔍 Query #{self.query_count}:[/blue] [dim]{query[:50]}{'...' if len(query) > 50 else ''}[/dim]")
|
console.print(
|
||||||
|
f"[blue]🔍 Query #{self.query_count}:[/blue] [dim]{query[:50]}{'...' if len(query) > 50 else ''}[/dim]"
|
||||||
|
)
|
||||||
|
|
||||||
# Perform search with timing
|
# Perform search with timing
|
||||||
start = time.time()
|
start = time.time()
|
||||||
@ -581,79 +607,81 @@ class FastRAGServer:
|
|||||||
|
|
||||||
# Enhanced response
|
# Enhanced response
|
||||||
response = {
|
response = {
|
||||||
'success': True,
|
"success": True,
|
||||||
'query': query,
|
"query": query,
|
||||||
'count': len(results),
|
"count": len(results),
|
||||||
'search_time_ms': int(search_time * 1000),
|
"search_time_ms": int(search_time * 1000),
|
||||||
'results': [r.to_dict() for r in results],
|
"results": [r.to_dict() for r in results],
|
||||||
'server_uptime': int(time.time() - self.status.start_time),
|
"server_uptime": int(time.time() - self.status.start_time),
|
||||||
'total_queries': self.query_count,
|
"total_queries": self.query_count,
|
||||||
'server_status': 'ready'
|
"server_status": "ready",
|
||||||
}
|
}
|
||||||
|
|
||||||
self._send_json(client, response)
|
self._send_json(client, response)
|
||||||
|
|
||||||
# Enhanced result logging
|
# Enhanced result logging
|
||||||
console.print(f"[green]✅ {len(results)} results in {search_time*1000:.0f}ms[/green]")
|
console.print(
|
||||||
|
f"[green]✅ {len(results)} results in {search_time*1000:.0f}ms[/green]"
|
||||||
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error_msg = str(e)
|
error_msg = str(e)
|
||||||
logger.error(f"Client handler error: {error_msg}")
|
logger.error(f"Client handler error: {error_msg}")
|
||||||
|
|
||||||
error_response = {
|
error_response = {
|
||||||
'success': False,
|
"success": False,
|
||||||
'error': error_msg,
|
"error": error_msg,
|
||||||
'error_type': type(e).__name__,
|
"error_type": type(e).__name__,
|
||||||
'server_status': self.status.phase
|
"server_status": self.status.phase,
|
||||||
}
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self._send_json(client, error_response)
|
self._send_json(client, error_response)
|
||||||
except:
|
except (TypeError, ValueError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
console.print(f"[red]❌ Query failed: {error_msg}[/red]")
|
console.print(f"[red]❌ Query failed: {error_msg}[/red]")
|
||||||
finally:
|
finally:
|
||||||
try:
|
try:
|
||||||
client.close()
|
client.close()
|
||||||
except:
|
except (ConnectionError, OSError, TypeError, ValueError, socket.error):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def _receive_json(self, sock: socket.socket) -> str:
|
def _receive_json(self, sock: socket.socket) -> str:
|
||||||
"""Receive JSON with length prefix and timeout handling"""
|
"""Receive JSON with length prefix and timeout handling"""
|
||||||
try:
|
try:
|
||||||
# Receive length (4 bytes)
|
# Receive length (4 bytes)
|
||||||
length_data = b''
|
length_data = b""
|
||||||
while len(length_data) < 4:
|
while len(length_data) < 4:
|
||||||
chunk = sock.recv(4 - len(length_data))
|
chunk = sock.recv(4 - len(length_data))
|
||||||
if not chunk:
|
if not chunk:
|
||||||
raise ConnectionError("Connection closed while receiving length")
|
raise ConnectionError("Connection closed while receiving length")
|
||||||
length_data += chunk
|
length_data += chunk
|
||||||
|
|
||||||
length = int.from_bytes(length_data, 'big')
|
length = int.from_bytes(length_data, "big")
|
||||||
if length > 10_000_000: # 10MB limit
|
if length > 10_000_000: # 10MB limit
|
||||||
raise ValueError(f"Message too large: {length} bytes")
|
raise ValueError(f"Message too large: {length} bytes")
|
||||||
|
|
||||||
# Receive data
|
# Receive data
|
||||||
data = b''
|
data = b""
|
||||||
while len(data) < length:
|
while len(data) < length:
|
||||||
chunk = sock.recv(min(65536, length - len(data)))
|
chunk = sock.recv(min(65536, length - len(data)))
|
||||||
if not chunk:
|
if not chunk:
|
||||||
raise ConnectionError("Connection closed while receiving data")
|
raise ConnectionError("Connection closed while receiving data")
|
||||||
data += chunk
|
data += chunk
|
||||||
|
|
||||||
return data.decode('utf-8')
|
return data.decode("utf-8")
|
||||||
except socket.timeout:
|
except socket.timeout:
|
||||||
raise ConnectionError("Timeout while receiving data")
|
raise ConnectionError("Timeout while receiving data")
|
||||||
|
|
||||||
def _send_json(self, sock: socket.socket, data: dict):
|
def _send_json(self, sock: socket.socket, data: dict):
|
||||||
"""Send JSON with length prefix"""
|
"""Send JSON with length prefix"""
|
||||||
json_str = json.dumps(data, ensure_ascii=False, separators=(',', ':'))
|
json_str = json.dumps(data, ensure_ascii=False, separators=(",", ":"))
|
||||||
json_bytes = json_str.encode('utf-8')
|
json_bytes = json_str.encode("utf-8")
|
||||||
|
|
||||||
# Send length prefix
|
# Send length prefix
|
||||||
length = len(json_bytes)
|
length = len(json_bytes)
|
||||||
sock.send(length.to_bytes(4, 'big'))
|
sock.send(length.to_bytes(4, "big"))
|
||||||
|
|
||||||
# Send data
|
# Send data
|
||||||
sock.sendall(json_bytes)
|
sock.sendall(json_bytes)
|
||||||
@ -667,7 +695,7 @@ class FastRAGServer:
|
|||||||
if self.socket:
|
if self.socket:
|
||||||
try:
|
try:
|
||||||
self.socket.close()
|
self.socket.close()
|
||||||
except:
|
except (ConnectionError, OSError, TypeError, ValueError, socket.error):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Shutdown executor
|
# Shutdown executor
|
||||||
@ -677,6 +705,8 @@ class FastRAGServer:
|
|||||||
|
|
||||||
|
|
||||||
# Enhanced client with status monitoring
|
# Enhanced client with status monitoring
|
||||||
|
|
||||||
|
|
||||||
class FastRAGClient:
|
class FastRAGClient:
|
||||||
"""Enhanced client with better error handling and status monitoring"""
|
"""Enhanced client with better error handling and status monitoring"""
|
||||||
|
|
||||||
@ -689,9 +719,9 @@ class FastRAGClient:
|
|||||||
try:
|
try:
|
||||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
sock.settimeout(self.timeout)
|
sock.settimeout(self.timeout)
|
||||||
sock.connect(('localhost', self.port))
|
sock.connect(("localhost", self.port))
|
||||||
|
|
||||||
request = {'query': query, 'top_k': top_k}
|
request = {"query": query, "top_k": top_k}
|
||||||
self._send_json(sock, request)
|
self._send_json(sock, request)
|
||||||
|
|
||||||
data = self._receive_json(sock)
|
data = self._receive_json(sock)
|
||||||
@ -702,31 +732,27 @@ class FastRAGClient:
|
|||||||
|
|
||||||
except ConnectionRefusedError:
|
except ConnectionRefusedError:
|
||||||
return {
|
return {
|
||||||
'success': False,
|
"success": False,
|
||||||
'error': 'RAG server not running. Start with: python -m mini_rag server',
|
"error": "RAG server not running. Start with: python -m mini_rag server",
|
||||||
'error_type': 'connection_refused'
|
"error_type": "connection_refused",
|
||||||
}
|
}
|
||||||
except socket.timeout:
|
except socket.timeout:
|
||||||
return {
|
return {
|
||||||
'success': False,
|
"success": False,
|
||||||
'error': f'Request timed out after {self.timeout}s',
|
"error": f"Request timed out after {self.timeout}s",
|
||||||
'error_type': 'timeout'
|
"error_type": "timeout",
|
||||||
}
|
}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return {
|
return {"success": False, "error": str(e), "error_type": type(e).__name__}
|
||||||
'success': False,
|
|
||||||
'error': str(e),
|
|
||||||
'error_type': type(e).__name__
|
|
||||||
}
|
|
||||||
|
|
||||||
def get_status(self) -> Dict[str, Any]:
|
def get_status(self) -> Dict[str, Any]:
|
||||||
"""Get detailed server status"""
|
"""Get detailed server status"""
|
||||||
try:
|
try:
|
||||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
sock.settimeout(5.0)
|
sock.settimeout(5.0)
|
||||||
sock.connect(('localhost', self.port))
|
sock.connect(("localhost", self.port))
|
||||||
|
|
||||||
request = {'command': 'status'}
|
request = {"command": "status"}
|
||||||
self._send_json(sock, request)
|
self._send_json(sock, request)
|
||||||
|
|
||||||
data = self._receive_json(sock)
|
data = self._receive_json(sock)
|
||||||
@ -736,18 +762,14 @@ class FastRAGClient:
|
|||||||
return response
|
return response
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return {
|
return {"success": False, "error": str(e), "server_running": False}
|
||||||
'success': False,
|
|
||||||
'error': str(e),
|
|
||||||
'server_running': False
|
|
||||||
}
|
|
||||||
|
|
||||||
def is_running(self) -> bool:
|
def is_running(self) -> bool:
|
||||||
"""Enhanced server detection"""
|
"""Enhanced server detection"""
|
||||||
try:
|
try:
|
||||||
status = self.get_status()
|
status = self.get_status()
|
||||||
return status.get('success', False)
|
return status.get("success", False)
|
||||||
except:
|
except (TypeError, ValueError):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def shutdown(self) -> Dict[str, Any]:
|
def shutdown(self) -> Dict[str, Any]:
|
||||||
@ -755,9 +777,9 @@ class FastRAGClient:
|
|||||||
try:
|
try:
|
||||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
sock.settimeout(10.0)
|
sock.settimeout(10.0)
|
||||||
sock.connect(('localhost', self.port))
|
sock.connect(("localhost", self.port))
|
||||||
|
|
||||||
request = {'command': 'shutdown'}
|
request = {"command": "shutdown"}
|
||||||
self._send_json(sock, request)
|
self._send_json(sock, request)
|
||||||
|
|
||||||
data = self._receive_json(sock)
|
data = self._receive_json(sock)
|
||||||
@ -767,41 +789,38 @@ class FastRAGClient:
|
|||||||
return response
|
return response
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return {
|
return {"success": False, "error": str(e)}
|
||||||
'success': False,
|
|
||||||
'error': str(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
def _send_json(self, sock: socket.socket, data: dict):
|
def _send_json(self, sock: socket.socket, data: dict):
|
||||||
"""Send JSON with length prefix"""
|
"""Send JSON with length prefix"""
|
||||||
json_str = json.dumps(data, ensure_ascii=False, separators=(',', ':'))
|
json_str = json.dumps(data, ensure_ascii=False, separators=(",", ":"))
|
||||||
json_bytes = json_str.encode('utf-8')
|
json_bytes = json_str.encode("utf-8")
|
||||||
|
|
||||||
length = len(json_bytes)
|
length = len(json_bytes)
|
||||||
sock.send(length.to_bytes(4, 'big'))
|
sock.send(length.to_bytes(4, "big"))
|
||||||
sock.sendall(json_bytes)
|
sock.sendall(json_bytes)
|
||||||
|
|
||||||
def _receive_json(self, sock: socket.socket) -> str:
|
def _receive_json(self, sock: socket.socket) -> str:
|
||||||
"""Receive JSON with length prefix"""
|
"""Receive JSON with length prefix"""
|
||||||
# Receive length
|
# Receive length
|
||||||
length_data = b''
|
length_data = b""
|
||||||
while len(length_data) < 4:
|
while len(length_data) < 4:
|
||||||
chunk = sock.recv(4 - len(length_data))
|
chunk = sock.recv(4 - len(length_data))
|
||||||
if not chunk:
|
if not chunk:
|
||||||
raise ConnectionError("Connection closed")
|
raise ConnectionError("Connection closed")
|
||||||
length_data += chunk
|
length_data += chunk
|
||||||
|
|
||||||
length = int.from_bytes(length_data, 'big')
|
length = int.from_bytes(length_data, "big")
|
||||||
|
|
||||||
# Receive data
|
# Receive data
|
||||||
data = b''
|
data = b""
|
||||||
while len(data) < length:
|
while len(data) < length:
|
||||||
chunk = sock.recv(min(65536, length - len(data)))
|
chunk = sock.recv(min(65536, length - len(data)))
|
||||||
if not chunk:
|
if not chunk:
|
||||||
raise ConnectionError("Connection closed")
|
raise ConnectionError("Connection closed")
|
||||||
data += chunk
|
data += chunk
|
||||||
|
|
||||||
return data.decode('utf-8')
|
return data.decode("utf-8")
|
||||||
|
|
||||||
|
|
||||||
def start_fast_server(project_path: Path, port: int = 7777, auto_index: bool = True):
|
def start_fast_server(project_path: Path, port: int = 7777, auto_index: bool = True):
|
||||||
|
|||||||
@ -3,31 +3,39 @@ Parallel indexing engine for efficient codebase processing.
|
|||||||
Handles file discovery, chunking, embedding, and storage.
|
Handles file discovery, chunking, embedding, and storage.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
|
||||||
import json
|
|
||||||
import hashlib
|
import hashlib
|
||||||
|
import json
|
||||||
import logging
|
import logging
|
||||||
from pathlib import Path
|
import os
|
||||||
from typing import List, Dict, Any, Optional, Set, Tuple
|
|
||||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn, TimeRemainingColumn
|
|
||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
|
from rich.progress import (
|
||||||
|
BarColumn,
|
||||||
|
Progress,
|
||||||
|
SpinnerColumn,
|
||||||
|
TextColumn,
|
||||||
|
TimeRemainingColumn,
|
||||||
|
)
|
||||||
|
|
||||||
# Optional LanceDB import
|
# Optional LanceDB import
|
||||||
try:
|
try:
|
||||||
import lancedb
|
import lancedb
|
||||||
import pyarrow as pa
|
import pyarrow as pa
|
||||||
|
|
||||||
LANCEDB_AVAILABLE = True
|
LANCEDB_AVAILABLE = True
|
||||||
except ImportError:
|
except ImportError:
|
||||||
lancedb = None
|
lancedb = None
|
||||||
pa = None
|
pa = None
|
||||||
LANCEDB_AVAILABLE = False
|
LANCEDB_AVAILABLE = False
|
||||||
|
|
||||||
|
from .chunker import CodeChunker
|
||||||
from .ollama_embeddings import OllamaEmbedder as CodeEmbedder
|
from .ollama_embeddings import OllamaEmbedder as CodeEmbedder
|
||||||
from .chunker import CodeChunker, CodeChunk
|
|
||||||
from .path_handler import normalize_path, normalize_relative_path
|
from .path_handler import normalize_path, normalize_relative_path
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@ -37,11 +45,13 @@ console = Console()
|
|||||||
class ProjectIndexer:
|
class ProjectIndexer:
|
||||||
"""Indexes a project directory for semantic search."""
|
"""Indexes a project directory for semantic search."""
|
||||||
|
|
||||||
def __init__(self,
|
def __init__(
|
||||||
|
self,
|
||||||
project_path: Path,
|
project_path: Path,
|
||||||
embedder: Optional[CodeEmbedder] = None,
|
embedder: Optional[CodeEmbedder] = None,
|
||||||
chunker: Optional[CodeChunker] = None,
|
chunker: Optional[CodeChunker] = None,
|
||||||
max_workers: int = 4):
|
max_workers: int = 4,
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
Initialize the indexer.
|
Initialize the indexer.
|
||||||
|
|
||||||
@ -52,9 +62,9 @@ class ProjectIndexer:
|
|||||||
max_workers: Number of parallel workers for indexing
|
max_workers: Number of parallel workers for indexing
|
||||||
"""
|
"""
|
||||||
self.project_path = Path(project_path).resolve()
|
self.project_path = Path(project_path).resolve()
|
||||||
self.rag_dir = self.project_path / '.mini-rag'
|
self.rag_dir = self.project_path / ".mini-rag"
|
||||||
self.manifest_path = self.rag_dir / 'manifest.json'
|
self.manifest_path = self.rag_dir / "manifest.json"
|
||||||
self.config_path = self.rag_dir / 'config.json'
|
self.config_path = self.rag_dir / "config.json"
|
||||||
|
|
||||||
# Create RAG directory if it doesn't exist
|
# Create RAG directory if it doesn't exist
|
||||||
self.rag_dir.mkdir(exist_ok=True)
|
self.rag_dir.mkdir(exist_ok=True)
|
||||||
@ -71,26 +81,75 @@ class ProjectIndexer:
|
|||||||
# File patterns to include/exclude
|
# File patterns to include/exclude
|
||||||
self.include_patterns = [
|
self.include_patterns = [
|
||||||
# Code files
|
# Code files
|
||||||
'*.py', '*.js', '*.jsx', '*.ts', '*.tsx',
|
"*.py",
|
||||||
'*.go', '*.java', '*.cpp', '*.c', '*.cs',
|
"*.js",
|
||||||
'*.rs', '*.rb', '*.php', '*.swift', '*.kt',
|
"*.jsx",
|
||||||
'*.scala', '*.r', '*.m', '*.h', '*.hpp',
|
"*.ts",
|
||||||
|
"*.tsx",
|
||||||
|
"*.go",
|
||||||
|
"*.java",
|
||||||
|
"*.cpp",
|
||||||
|
"*.c",
|
||||||
|
"*.cs",
|
||||||
|
"*.rs",
|
||||||
|
"*.rb",
|
||||||
|
"*.php",
|
||||||
|
"*.swift",
|
||||||
|
"*.kt",
|
||||||
|
"*.scala",
|
||||||
|
"*.r",
|
||||||
|
"*.m",
|
||||||
|
"*.h",
|
||||||
|
"*.hpp",
|
||||||
# Documentation files
|
# Documentation files
|
||||||
'*.md', '*.markdown', '*.rst', '*.txt',
|
"*.md",
|
||||||
'*.adoc', '*.asciidoc',
|
"*.markdown",
|
||||||
|
"*.rst",
|
||||||
|
"*.txt",
|
||||||
|
"*.adoc",
|
||||||
|
"*.asciidoc",
|
||||||
# Config files
|
# Config files
|
||||||
'*.json', '*.yaml', '*.yml', '*.toml', '*.ini',
|
"*.json",
|
||||||
'*.xml', '*.conf', '*.config',
|
"*.yaml",
|
||||||
|
"*.yml",
|
||||||
|
"*.toml",
|
||||||
|
"*.ini",
|
||||||
|
"*.xml",
|
||||||
|
"*.con",
|
||||||
|
"*.config",
|
||||||
# Other text files
|
# Other text files
|
||||||
'README', 'LICENSE', 'CHANGELOG', 'AUTHORS',
|
"README",
|
||||||
'CONTRIBUTING', 'TODO', 'NOTES'
|
"LICENSE",
|
||||||
|
"CHANGELOG",
|
||||||
|
"AUTHORS",
|
||||||
|
"CONTRIBUTING",
|
||||||
|
"TODO",
|
||||||
|
"NOTES",
|
||||||
]
|
]
|
||||||
|
|
||||||
self.exclude_patterns = [
|
self.exclude_patterns = [
|
||||||
'__pycache__', '.git', 'node_modules', '.venv', 'venv',
|
"__pycache__",
|
||||||
'env', 'dist', 'build', 'target', '.idea', '.vscode',
|
".git",
|
||||||
'*.pyc', '*.pyo', '*.pyd', '.DS_Store', '*.so', '*.dll',
|
"node_modules",
|
||||||
'*.dylib', '*.exe', '*.bin', '*.log', '*.lock'
|
".venv",
|
||||||
|
"venv",
|
||||||
|
"env",
|
||||||
|
"dist",
|
||||||
|
"build",
|
||||||
|
"target",
|
||||||
|
".idea",
|
||||||
|
".vscode",
|
||||||
|
"*.pyc",
|
||||||
|
"*.pyo",
|
||||||
|
"*.pyd",
|
||||||
|
".DS_Store",
|
||||||
|
"*.so",
|
||||||
|
"*.dll",
|
||||||
|
"*.dylib",
|
||||||
|
"*.exe",
|
||||||
|
"*.bin",
|
||||||
|
"*.log",
|
||||||
|
"*.lock",
|
||||||
]
|
]
|
||||||
|
|
||||||
# Load existing manifest if it exists
|
# Load existing manifest if it exists
|
||||||
@ -100,23 +159,23 @@ class ProjectIndexer:
|
|||||||
"""Load existing manifest or create new one."""
|
"""Load existing manifest or create new one."""
|
||||||
if self.manifest_path.exists():
|
if self.manifest_path.exists():
|
||||||
try:
|
try:
|
||||||
with open(self.manifest_path, 'r') as f:
|
with open(self.manifest_path, "r") as f:
|
||||||
return json.load(f)
|
return json.load(f)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"Failed to load manifest: {e}")
|
logger.warning(f"Failed to load manifest: {e}")
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'version': '1.0',
|
"version": "1.0",
|
||||||
'indexed_at': None,
|
"indexed_at": None,
|
||||||
'file_count': 0,
|
"file_count": 0,
|
||||||
'chunk_count': 0,
|
"chunk_count": 0,
|
||||||
'files': {}
|
"files": {},
|
||||||
}
|
}
|
||||||
|
|
||||||
def _save_manifest(self):
|
def _save_manifest(self):
|
||||||
"""Save manifest to disk."""
|
"""Save manifest to disk."""
|
||||||
try:
|
try:
|
||||||
with open(self.manifest_path, 'w') as f:
|
with open(self.manifest_path, "w") as f:
|
||||||
json.dump(self.manifest, f, indent=2)
|
json.dump(self.manifest, f, indent=2)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to save manifest: {e}")
|
logger.error(f"Failed to save manifest: {e}")
|
||||||
@ -125,7 +184,7 @@ class ProjectIndexer:
|
|||||||
"""Load or create comprehensive configuration."""
|
"""Load or create comprehensive configuration."""
|
||||||
if self.config_path.exists():
|
if self.config_path.exists():
|
||||||
try:
|
try:
|
||||||
with open(self.config_path, 'r') as f:
|
with open(self.config_path, "r") as f:
|
||||||
config = json.load(f)
|
config = json.load(f)
|
||||||
# Apply any loaded settings
|
# Apply any loaded settings
|
||||||
self._apply_config(config)
|
self._apply_config(config)
|
||||||
@ -138,49 +197,57 @@ class ProjectIndexer:
|
|||||||
"project": {
|
"project": {
|
||||||
"name": self.project_path.name,
|
"name": self.project_path.name,
|
||||||
"description": f"RAG index for {self.project_path.name}",
|
"description": f"RAG index for {self.project_path.name}",
|
||||||
"created_at": datetime.now().isoformat()
|
"created_at": datetime.now().isoformat(),
|
||||||
},
|
},
|
||||||
"embedding": {
|
"embedding": {
|
||||||
"provider": "ollama",
|
"provider": "ollama",
|
||||||
"model": self.embedder.model_name if hasattr(self.embedder, 'model_name') else 'nomic-embed-text:latest',
|
"model": (
|
||||||
|
self.embedder.model_name
|
||||||
|
if hasattr(self.embedder, "model_name")
|
||||||
|
else "nomic-embed-text:latest"
|
||||||
|
),
|
||||||
"base_url": "http://localhost:11434",
|
"base_url": "http://localhost:11434",
|
||||||
"batch_size": 4,
|
"batch_size": 4,
|
||||||
"max_workers": 4
|
"max_workers": 4,
|
||||||
},
|
},
|
||||||
"chunking": {
|
"chunking": {
|
||||||
"max_size": self.chunker.max_chunk_size if hasattr(self.chunker, 'max_chunk_size') else 2500,
|
"max_size": (
|
||||||
"min_size": self.chunker.min_chunk_size if hasattr(self.chunker, 'min_chunk_size') else 100,
|
self.chunker.max_chunk_size
|
||||||
|
if hasattr(self.chunker, "max_chunk_size")
|
||||||
|
else 2500
|
||||||
|
),
|
||||||
|
"min_size": (
|
||||||
|
self.chunker.min_chunk_size
|
||||||
|
if hasattr(self.chunker, "min_chunk_size")
|
||||||
|
else 100
|
||||||
|
),
|
||||||
"overlap": 100,
|
"overlap": 100,
|
||||||
"strategy": "semantic"
|
"strategy": "semantic",
|
||||||
},
|
|
||||||
"streaming": {
|
|
||||||
"enabled": True,
|
|
||||||
"threshold_mb": 1,
|
|
||||||
"chunk_size_kb": 64
|
|
||||||
},
|
},
|
||||||
|
"streaming": {"enabled": True, "threshold_mb": 1, "chunk_size_kb": 64},
|
||||||
"files": {
|
"files": {
|
||||||
"include_patterns": self.include_patterns,
|
"include_patterns": self.include_patterns,
|
||||||
"exclude_patterns": self.exclude_patterns,
|
"exclude_patterns": self.exclude_patterns,
|
||||||
"max_file_size_mb": 50,
|
"max_file_size_mb": 50,
|
||||||
"encoding_fallbacks": ["utf-8", "latin-1", "cp1252", "utf-8-sig"]
|
"encoding_fallbacks": ["utf-8", "latin-1", "cp1252", "utf-8-sig"],
|
||||||
},
|
},
|
||||||
"indexing": {
|
"indexing": {
|
||||||
"parallel_workers": self.max_workers,
|
"parallel_workers": self.max_workers,
|
||||||
"incremental": True,
|
"incremental": True,
|
||||||
"track_changes": True,
|
"track_changes": True,
|
||||||
"skip_binary": True
|
"skip_binary": True,
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"default_top_k": 10,
|
"default_top_k": 10,
|
||||||
"similarity_threshold": 0.7,
|
"similarity_threshold": 0.7,
|
||||||
"hybrid_search": True,
|
"hybrid_search": True,
|
||||||
"bm25_weight": 0.3
|
"bm25_weight": 0.3,
|
||||||
},
|
},
|
||||||
"storage": {
|
"storage": {
|
||||||
"compress_vectors": False,
|
"compress_vectors": False,
|
||||||
"index_type": "ivf_pq",
|
"index_type": "ivf_pq",
|
||||||
"cleanup_old_chunks": True
|
"cleanup_old_chunks": True,
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
# Save comprehensive config with nice formatting
|
# Save comprehensive config with nice formatting
|
||||||
@ -191,31 +258,41 @@ class ProjectIndexer:
|
|||||||
"""Apply configuration settings to the indexer."""
|
"""Apply configuration settings to the indexer."""
|
||||||
try:
|
try:
|
||||||
# Apply embedding settings
|
# Apply embedding settings
|
||||||
if 'embedding' in config:
|
if "embedding" in config:
|
||||||
emb_config = config['embedding']
|
emb_config = config["embedding"]
|
||||||
if hasattr(self.embedder, 'model_name'):
|
if hasattr(self.embedder, "model_name"):
|
||||||
self.embedder.model_name = emb_config.get('model', self.embedder.model_name)
|
self.embedder.model_name = emb_config.get(
|
||||||
if hasattr(self.embedder, 'base_url'):
|
"model", self.embedder.model_name
|
||||||
self.embedder.base_url = emb_config.get('base_url', self.embedder.base_url)
|
)
|
||||||
|
if hasattr(self.embedder, "base_url"):
|
||||||
|
self.embedder.base_url = emb_config.get("base_url", self.embedder.base_url)
|
||||||
|
|
||||||
# Apply chunking settings
|
# Apply chunking settings
|
||||||
if 'chunking' in config:
|
if "chunking" in config:
|
||||||
chunk_config = config['chunking']
|
chunk_config = config["chunking"]
|
||||||
if hasattr(self.chunker, 'max_chunk_size'):
|
if hasattr(self.chunker, "max_chunk_size"):
|
||||||
self.chunker.max_chunk_size = chunk_config.get('max_size', self.chunker.max_chunk_size)
|
self.chunker.max_chunk_size = chunk_config.get(
|
||||||
if hasattr(self.chunker, 'min_chunk_size'):
|
"max_size", self.chunker.max_chunk_size
|
||||||
self.chunker.min_chunk_size = chunk_config.get('min_size', self.chunker.min_chunk_size)
|
)
|
||||||
|
if hasattr(self.chunker, "min_chunk_size"):
|
||||||
|
self.chunker.min_chunk_size = chunk_config.get(
|
||||||
|
"min_size", self.chunker.min_chunk_size
|
||||||
|
)
|
||||||
|
|
||||||
# Apply file patterns
|
# Apply file patterns
|
||||||
if 'files' in config:
|
if "files" in config:
|
||||||
file_config = config['files']
|
file_config = config["files"]
|
||||||
self.include_patterns = file_config.get('include_patterns', self.include_patterns)
|
self.include_patterns = file_config.get(
|
||||||
self.exclude_patterns = file_config.get('exclude_patterns', self.exclude_patterns)
|
"include_patterns", self.include_patterns
|
||||||
|
)
|
||||||
|
self.exclude_patterns = file_config.get(
|
||||||
|
"exclude_patterns", self.exclude_patterns
|
||||||
|
)
|
||||||
|
|
||||||
# Apply indexing settings
|
# Apply indexing settings
|
||||||
if 'indexing' in config:
|
if "indexing" in config:
|
||||||
idx_config = config['indexing']
|
idx_config = config["indexing"]
|
||||||
self.max_workers = idx_config.get('parallel_workers', self.max_workers)
|
self.max_workers = idx_config.get("parallel_workers", self.max_workers)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"Failed to apply some config settings: {e}")
|
logger.warning(f"Failed to apply some config settings: {e}")
|
||||||
@ -228,10 +305,10 @@ class ProjectIndexer:
|
|||||||
"_comment": "RAG System Configuration - Edit this file to customize indexing behavior",
|
"_comment": "RAG System Configuration - Edit this file to customize indexing behavior",
|
||||||
"_version": "2.0",
|
"_version": "2.0",
|
||||||
"_docs": "See README.md for detailed configuration options",
|
"_docs": "See README.md for detailed configuration options",
|
||||||
**config
|
**config,
|
||||||
}
|
}
|
||||||
|
|
||||||
with open(self.config_path, 'w') as f:
|
with open(self.config_path, "w") as f:
|
||||||
json.dump(config_with_comments, f, indent=2, sort_keys=True)
|
json.dump(config_with_comments, f, indent=2, sort_keys=True)
|
||||||
|
|
||||||
logger.info(f"Configuration saved to {self.config_path}")
|
logger.info(f"Configuration saved to {self.config_path}")
|
||||||
@ -257,7 +334,7 @@ class ProjectIndexer:
|
|||||||
try:
|
try:
|
||||||
if file_path.stat().st_size > 1_000_000:
|
if file_path.stat().st_size > 1_000_000:
|
||||||
return False
|
return False
|
||||||
except:
|
except (OSError, IOError):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Check exclude patterns first
|
# Check exclude patterns first
|
||||||
@ -281,21 +358,33 @@ class ProjectIndexer:
|
|||||||
"""Check if an extensionless file should be indexed based on content."""
|
"""Check if an extensionless file should be indexed based on content."""
|
||||||
try:
|
try:
|
||||||
# Read first 1KB to check content
|
# Read first 1KB to check content
|
||||||
with open(file_path, 'rb') as f:
|
with open(file_path, "rb") as f:
|
||||||
first_chunk = f.read(1024)
|
first_chunk = f.read(1024)
|
||||||
|
|
||||||
# Check if it's a text file (not binary)
|
# Check if it's a text file (not binary)
|
||||||
try:
|
try:
|
||||||
text_content = first_chunk.decode('utf-8')
|
text_content = first_chunk.decode("utf-8")
|
||||||
except UnicodeDecodeError:
|
except UnicodeDecodeError:
|
||||||
return False # Binary file, skip
|
return False # Binary file, skip
|
||||||
|
|
||||||
# Check for code indicators
|
# Check for code indicators
|
||||||
code_indicators = [
|
code_indicators = [
|
||||||
'#!/usr/bin/env python', '#!/usr/bin/python', '#!.*python',
|
"#!/usr/bin/env python",
|
||||||
'import ', 'from ', 'def ', 'class ', 'if __name__',
|
"#!/usr/bin/python",
|
||||||
'function ', 'var ', 'const ', 'let ', 'package main',
|
"#!.*python",
|
||||||
'public class', 'private class', 'public static void'
|
"import ",
|
||||||
|
"from ",
|
||||||
|
"def ",
|
||||||
|
"class ",
|
||||||
|
"if __name__",
|
||||||
|
"function ",
|
||||||
|
"var ",
|
||||||
|
"const ",
|
||||||
|
"let ",
|
||||||
|
"package main",
|
||||||
|
"public class",
|
||||||
|
"private class",
|
||||||
|
"public static void",
|
||||||
]
|
]
|
||||||
|
|
||||||
text_lower = text_content.lower()
|
text_lower = text_content.lower()
|
||||||
@ -305,8 +394,15 @@ class ProjectIndexer:
|
|||||||
|
|
||||||
# Check for configuration files
|
# Check for configuration files
|
||||||
config_indicators = [
|
config_indicators = [
|
||||||
'#!/bin/bash', '#!/bin/sh', '[', 'version =', 'name =',
|
"#!/bin/bash",
|
||||||
'description =', 'author =', '<configuration>', '<?xml'
|
"#!/bin/sh",
|
||||||
|
"[",
|
||||||
|
"version =",
|
||||||
|
"name =",
|
||||||
|
"description =",
|
||||||
|
"author =",
|
||||||
|
"<configuration>",
|
||||||
|
"<?xml",
|
||||||
]
|
]
|
||||||
|
|
||||||
for indicator in config_indicators:
|
for indicator in config_indicators:
|
||||||
@ -323,17 +419,17 @@ class ProjectIndexer:
|
|||||||
file_str = normalize_relative_path(file_path, self.project_path)
|
file_str = normalize_relative_path(file_path, self.project_path)
|
||||||
|
|
||||||
# Not in manifest - needs indexing
|
# Not in manifest - needs indexing
|
||||||
if file_str not in self.manifest['files']:
|
if file_str not in self.manifest["files"]:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
file_info = self.manifest['files'][file_str]
|
file_info = self.manifest["files"][file_str]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
stat = file_path.stat()
|
stat = file_path.stat()
|
||||||
|
|
||||||
# Quick checks first (no I/O) - check size and modification time
|
# Quick checks first (no I/O) - check size and modification time
|
||||||
stored_size = file_info.get('size', 0)
|
stored_size = file_info.get("size", 0)
|
||||||
stored_mtime = file_info.get('mtime', 0)
|
stored_mtime = file_info.get("mtime", 0)
|
||||||
|
|
||||||
current_size = stat.st_size
|
current_size = stat.st_size
|
||||||
current_mtime = stat.st_mtime
|
current_mtime = stat.st_mtime
|
||||||
@ -345,7 +441,7 @@ class ProjectIndexer:
|
|||||||
# Size and mtime same - check hash only if needed (for paranoia)
|
# Size and mtime same - check hash only if needed (for paranoia)
|
||||||
# This catches cases where content changed but mtime didn't (rare but possible)
|
# This catches cases where content changed but mtime didn't (rare but possible)
|
||||||
current_hash = self._get_file_hash(file_path)
|
current_hash = self._get_file_hash(file_path)
|
||||||
stored_hash = file_info.get('hash', '')
|
stored_hash = file_info.get("hash", "")
|
||||||
|
|
||||||
return current_hash != stored_hash
|
return current_hash != stored_hash
|
||||||
|
|
||||||
@ -356,11 +452,11 @@ class ProjectIndexer:
|
|||||||
|
|
||||||
def _cleanup_removed_files(self):
|
def _cleanup_removed_files(self):
|
||||||
"""Remove entries for files that no longer exist from manifest and database."""
|
"""Remove entries for files that no longer exist from manifest and database."""
|
||||||
if 'files' not in self.manifest:
|
if "files" not in self.manifest:
|
||||||
return
|
return
|
||||||
|
|
||||||
removed_files = []
|
removed_files = []
|
||||||
for file_str in list(self.manifest['files'].keys()):
|
for file_str in list(self.manifest["files"].keys()):
|
||||||
file_path = self.project_path / file_str
|
file_path = self.project_path / file_str
|
||||||
if not file_path.exists():
|
if not file_path.exists():
|
||||||
removed_files.append(file_str)
|
removed_files.append(file_str)
|
||||||
@ -371,14 +467,14 @@ class ProjectIndexer:
|
|||||||
for file_str in removed_files:
|
for file_str in removed_files:
|
||||||
# Remove from database
|
# Remove from database
|
||||||
try:
|
try:
|
||||||
if hasattr(self, 'table') and self.table:
|
if hasattr(self, "table") and self.table:
|
||||||
self.table.delete(f"file_path = '{file_str}'")
|
self.table.delete(f"file_path = '{file_str}'")
|
||||||
logger.debug(f"Removed chunks for deleted file: {file_str}")
|
logger.debug(f"Removed chunks for deleted file: {file_str}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"Could not remove chunks for {file_str}: {e}")
|
logger.warning(f"Could not remove chunks for {file_str}: {e}")
|
||||||
|
|
||||||
# Remove from manifest
|
# Remove from manifest
|
||||||
del self.manifest['files'][file_str]
|
del self.manifest["files"][file_str]
|
||||||
|
|
||||||
# Save updated manifest
|
# Save updated manifest
|
||||||
self._save_manifest()
|
self._save_manifest()
|
||||||
@ -391,7 +487,9 @@ class ProjectIndexer:
|
|||||||
# Walk through project directory
|
# Walk through project directory
|
||||||
for root, dirs, files in os.walk(self.project_path):
|
for root, dirs, files in os.walk(self.project_path):
|
||||||
# Skip excluded directories
|
# Skip excluded directories
|
||||||
dirs[:] = [d for d in dirs if not any(pattern in d for pattern in self.exclude_patterns)]
|
dirs[:] = [
|
||||||
|
d for d in dirs if not any(pattern in d for pattern in self.exclude_patterns)
|
||||||
|
]
|
||||||
|
|
||||||
root_path = Path(root)
|
root_path = Path(root)
|
||||||
for file in files:
|
for file in files:
|
||||||
@ -402,7 +500,9 @@ class ProjectIndexer:
|
|||||||
|
|
||||||
return files_to_index
|
return files_to_index
|
||||||
|
|
||||||
def _process_file(self, file_path: Path, stream_threshold: int = 1024 * 1024) -> Optional[List[Dict[str, Any]]]:
|
def _process_file(
|
||||||
|
self, file_path: Path, stream_threshold: int = 1024 * 1024
|
||||||
|
) -> Optional[List[Dict[str, Any]]]:
|
||||||
"""Process a single file: read, chunk, embed.
|
"""Process a single file: read, chunk, embed.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -418,7 +518,7 @@ class ProjectIndexer:
|
|||||||
content = self._read_file_streaming(file_path)
|
content = self._read_file_streaming(file_path)
|
||||||
else:
|
else:
|
||||||
# Read file content normally for small files
|
# Read file content normally for small files
|
||||||
content = file_path.read_text(encoding='utf-8')
|
content = file_path.read_text(encoding="utf-8")
|
||||||
|
|
||||||
# Chunk the file
|
# Chunk the file
|
||||||
chunks = self.chunker.chunk_file(file_path, content)
|
chunks = self.chunker.chunk_file(file_path, content)
|
||||||
@ -446,39 +546,43 @@ class ProjectIndexer:
|
|||||||
)
|
)
|
||||||
|
|
||||||
record = {
|
record = {
|
||||||
'file_path': normalize_relative_path(file_path, self.project_path),
|
"file_path": normalize_relative_path(file_path, self.project_path),
|
||||||
'absolute_path': normalize_path(file_path),
|
"absolute_path": normalize_path(file_path),
|
||||||
'chunk_id': f"{file_path.stem}_{i}",
|
"chunk_id": f"{file_path.stem}_{i}",
|
||||||
'content': chunk.content,
|
"content": chunk.content,
|
||||||
'start_line': int(chunk.start_line),
|
"start_line": int(chunk.start_line),
|
||||||
'end_line': int(chunk.end_line),
|
"end_line": int(chunk.end_line),
|
||||||
'chunk_type': chunk.chunk_type,
|
"chunk_type": chunk.chunk_type,
|
||||||
'name': chunk.name or f"chunk_{i}",
|
"name": chunk.name or f"chunk_{i}",
|
||||||
'language': chunk.language,
|
"language": chunk.language,
|
||||||
'embedding': embedding, # Keep as numpy array
|
"embedding": embedding, # Keep as numpy array
|
||||||
'indexed_at': datetime.now().isoformat(),
|
"indexed_at": datetime.now().isoformat(),
|
||||||
# Add new metadata fields
|
# Add new metadata fields
|
||||||
'file_lines': int(chunk.file_lines) if chunk.file_lines else 0,
|
"file_lines": int(chunk.file_lines) if chunk.file_lines else 0,
|
||||||
'chunk_index': int(chunk.chunk_index) if chunk.chunk_index is not None else i,
|
"chunk_index": (
|
||||||
'total_chunks': int(chunk.total_chunks) if chunk.total_chunks else len(chunks),
|
int(chunk.chunk_index) if chunk.chunk_index is not None else i
|
||||||
'parent_class': chunk.parent_class or '',
|
),
|
||||||
'parent_function': chunk.parent_function or '',
|
"total_chunks": (
|
||||||
'prev_chunk_id': chunk.prev_chunk_id or '',
|
int(chunk.total_chunks) if chunk.total_chunks else len(chunks)
|
||||||
'next_chunk_id': chunk.next_chunk_id or '',
|
),
|
||||||
|
"parent_class": chunk.parent_class or "",
|
||||||
|
"parent_function": chunk.parent_function or "",
|
||||||
|
"prev_chunk_id": chunk.prev_chunk_id or "",
|
||||||
|
"next_chunk_id": chunk.next_chunk_id or "",
|
||||||
}
|
}
|
||||||
records.append(record)
|
records.append(record)
|
||||||
|
|
||||||
# Update manifest with enhanced tracking
|
# Update manifest with enhanced tracking
|
||||||
file_str = normalize_relative_path(file_path, self.project_path)
|
file_str = normalize_relative_path(file_path, self.project_path)
|
||||||
stat = file_path.stat()
|
stat = file_path.stat()
|
||||||
self.manifest['files'][file_str] = {
|
self.manifest["files"][file_str] = {
|
||||||
'hash': self._get_file_hash(file_path),
|
"hash": self._get_file_hash(file_path),
|
||||||
'size': stat.st_size,
|
"size": stat.st_size,
|
||||||
'mtime': stat.st_mtime,
|
"mtime": stat.st_mtime,
|
||||||
'chunks': len(chunks),
|
"chunks": len(chunks),
|
||||||
'indexed_at': datetime.now().isoformat(),
|
"indexed_at": datetime.now().isoformat(),
|
||||||
'language': chunks[0].language if chunks else 'unknown',
|
"language": chunks[0].language if chunks else "unknown",
|
||||||
'encoding': 'utf-8' # Track encoding used
|
"encoding": "utf-8", # Track encoding used
|
||||||
}
|
}
|
||||||
|
|
||||||
return records
|
return records
|
||||||
@ -501,7 +605,7 @@ class ProjectIndexer:
|
|||||||
content_parts = []
|
content_parts = []
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with open(file_path, 'r', encoding='utf-8') as f:
|
with open(file_path, "r", encoding="utf-8") as f:
|
||||||
while True:
|
while True:
|
||||||
chunk = f.read(chunk_size)
|
chunk = f.read(chunk_size)
|
||||||
if not chunk:
|
if not chunk:
|
||||||
@ -509,13 +613,13 @@ class ProjectIndexer:
|
|||||||
content_parts.append(chunk)
|
content_parts.append(chunk)
|
||||||
|
|
||||||
logger.debug(f"Streamed {len(content_parts)} chunks from {file_path}")
|
logger.debug(f"Streamed {len(content_parts)} chunks from {file_path}")
|
||||||
return ''.join(content_parts)
|
return "".join(content_parts)
|
||||||
|
|
||||||
except UnicodeDecodeError:
|
except UnicodeDecodeError:
|
||||||
# Try with different encodings for problematic files
|
# Try with different encodings for problematic files
|
||||||
for encoding in ['latin-1', 'cp1252', 'utf-8-sig']:
|
for encoding in ["latin-1", "cp1252", "utf-8-sig"]:
|
||||||
try:
|
try:
|
||||||
with open(file_path, 'r', encoding=encoding) as f:
|
with open(file_path, "r", encoding=encoding) as f:
|
||||||
content_parts = []
|
content_parts = []
|
||||||
while True:
|
while True:
|
||||||
chunk = f.read(chunk_size)
|
chunk = f.read(chunk_size)
|
||||||
@ -523,8 +627,10 @@ class ProjectIndexer:
|
|||||||
break
|
break
|
||||||
content_parts.append(chunk)
|
content_parts.append(chunk)
|
||||||
|
|
||||||
logger.debug(f"Streamed {len(content_parts)} chunks from {file_path} using {encoding}")
|
logger.debug(
|
||||||
return ''.join(content_parts)
|
f"Streamed {len(content_parts)} chunks from {file_path} using {encoding}"
|
||||||
|
)
|
||||||
|
return "".join(content_parts)
|
||||||
except UnicodeDecodeError:
|
except UnicodeDecodeError:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@ -535,16 +641,21 @@ class ProjectIndexer:
|
|||||||
def _init_database(self):
|
def _init_database(self):
|
||||||
"""Initialize LanceDB connection and table."""
|
"""Initialize LanceDB connection and table."""
|
||||||
if not LANCEDB_AVAILABLE:
|
if not LANCEDB_AVAILABLE:
|
||||||
logger.error("LanceDB is not available. Please install LanceDB for full indexing functionality.")
|
logger.error(
|
||||||
|
"LanceDB is not available. Please install LanceDB for full indexing functionality."
|
||||||
|
)
|
||||||
logger.info("For Ollama-only mode, consider using hash-based embeddings instead.")
|
logger.info("For Ollama-only mode, consider using hash-based embeddings instead.")
|
||||||
raise ImportError("LanceDB dependency is required for indexing. Install with: pip install lancedb pyarrow")
|
raise ImportError(
|
||||||
|
"LanceDB dependency is required for indexing. Install with: pip install lancedb pyarrow"
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.db = lancedb.connect(self.rag_dir)
|
self.db = lancedb.connect(self.rag_dir)
|
||||||
|
|
||||||
# Define schema with fixed-size vector
|
# Define schema with fixed-size vector
|
||||||
embedding_dim = self.embedder.get_embedding_dim()
|
embedding_dim = self.embedder.get_embedding_dim()
|
||||||
schema = pa.schema([
|
schema = pa.schema(
|
||||||
|
[
|
||||||
pa.field("file_path", pa.string()),
|
pa.field("file_path", pa.string()),
|
||||||
pa.field("absolute_path", pa.string()),
|
pa.field("absolute_path", pa.string()),
|
||||||
pa.field("chunk_id", pa.string()),
|
pa.field("chunk_id", pa.string()),
|
||||||
@ -554,7 +665,9 @@ class ProjectIndexer:
|
|||||||
pa.field("chunk_type", pa.string()),
|
pa.field("chunk_type", pa.string()),
|
||||||
pa.field("name", pa.string()),
|
pa.field("name", pa.string()),
|
||||||
pa.field("language", pa.string()),
|
pa.field("language", pa.string()),
|
||||||
pa.field("embedding", pa.list_(pa.float32(), embedding_dim)), # Fixed-size list
|
pa.field(
|
||||||
|
"embedding", pa.list_(pa.float32(), embedding_dim)
|
||||||
|
), # Fixed-size list
|
||||||
pa.field("indexed_at", pa.string()),
|
pa.field("indexed_at", pa.string()),
|
||||||
# New metadata fields
|
# New metadata fields
|
||||||
pa.field("file_lines", pa.int32()),
|
pa.field("file_lines", pa.int32()),
|
||||||
@ -564,7 +677,8 @@ class ProjectIndexer:
|
|||||||
pa.field("parent_function", pa.string(), nullable=True),
|
pa.field("parent_function", pa.string(), nullable=True),
|
||||||
pa.field("prev_chunk_id", pa.string(), nullable=True),
|
pa.field("prev_chunk_id", pa.string(), nullable=True),
|
||||||
pa.field("next_chunk_id", pa.string(), nullable=True),
|
pa.field("next_chunk_id", pa.string(), nullable=True),
|
||||||
])
|
]
|
||||||
|
)
|
||||||
|
|
||||||
# Create or open table
|
# Create or open table
|
||||||
if "code_vectors" in self.db.table_names():
|
if "code_vectors" in self.db.table_names():
|
||||||
@ -581,7 +695,9 @@ class ProjectIndexer:
|
|||||||
|
|
||||||
if not required_fields.issubset(existing_fields):
|
if not required_fields.issubset(existing_fields):
|
||||||
# Schema mismatch - drop and recreate table
|
# Schema mismatch - drop and recreate table
|
||||||
logger.warning("Schema mismatch detected. Dropping and recreating table.")
|
logger.warning(
|
||||||
|
"Schema mismatch detected. Dropping and recreating table."
|
||||||
|
)
|
||||||
self.db.drop_table("code_vectors")
|
self.db.drop_table("code_vectors")
|
||||||
self.table = self.db.create_table("code_vectors", schema=schema)
|
self.table = self.db.create_table("code_vectors", schema=schema)
|
||||||
logger.info("Recreated code_vectors table with updated schema")
|
logger.info("Recreated code_vectors table with updated schema")
|
||||||
@ -596,7 +712,9 @@ class ProjectIndexer:
|
|||||||
else:
|
else:
|
||||||
# Create empty table with schema
|
# Create empty table with schema
|
||||||
self.table = self.db.create_table("code_vectors", schema=schema)
|
self.table = self.db.create_table("code_vectors", schema=schema)
|
||||||
logger.info(f"Created new code_vectors table with embedding dimension {embedding_dim}")
|
logger.info(
|
||||||
|
f"Created new code_vectors table with embedding dimension {embedding_dim}"
|
||||||
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to initialize database: {e}")
|
logger.error(f"Failed to initialize database: {e}")
|
||||||
@ -624,11 +742,11 @@ class ProjectIndexer:
|
|||||||
# Clear manifest if force reindex
|
# Clear manifest if force reindex
|
||||||
if force_reindex:
|
if force_reindex:
|
||||||
self.manifest = {
|
self.manifest = {
|
||||||
'version': '1.0',
|
"version": "1.0",
|
||||||
'indexed_at': None,
|
"indexed_at": None,
|
||||||
'file_count': 0,
|
"file_count": 0,
|
||||||
'chunk_count': 0,
|
"chunk_count": 0,
|
||||||
'files': {}
|
"files": {},
|
||||||
}
|
}
|
||||||
# Clear existing table
|
# Clear existing table
|
||||||
if "code_vectors" in self.db.table_names():
|
if "code_vectors" in self.db.table_names():
|
||||||
@ -643,9 +761,9 @@ class ProjectIndexer:
|
|||||||
if not files_to_index:
|
if not files_to_index:
|
||||||
console.print("[green][/green] All files are up to date!")
|
console.print("[green][/green] All files are up to date!")
|
||||||
return {
|
return {
|
||||||
'files_indexed': 0,
|
"files_indexed": 0,
|
||||||
'chunks_created': 0,
|
"chunks_created": 0,
|
||||||
'time_taken': 0,
|
"time_taken": 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
console.print(f"[cyan]Found {len(files_to_index)} files to index[/cyan]")
|
console.print(f"[cyan]Found {len(files_to_index)} files to index[/cyan]")
|
||||||
@ -663,10 +781,7 @@ class ProjectIndexer:
|
|||||||
console=console,
|
console=console,
|
||||||
) as progress:
|
) as progress:
|
||||||
|
|
||||||
task = progress.add_task(
|
task = progress.add_task("[cyan]Indexing files...", total=len(files_to_index))
|
||||||
"[cyan]Indexing files...",
|
|
||||||
total=len(files_to_index)
|
|
||||||
)
|
|
||||||
|
|
||||||
with ThreadPoolExecutor(max_workers=self.max_workers) as executor:
|
with ThreadPoolExecutor(max_workers=self.max_workers) as executor:
|
||||||
# Submit all files for processing
|
# Submit all files for processing
|
||||||
@ -712,10 +827,10 @@ class ProjectIndexer:
|
|||||||
raise
|
raise
|
||||||
|
|
||||||
# Update manifest
|
# Update manifest
|
||||||
self.manifest['indexed_at'] = datetime.now().isoformat()
|
self.manifest["indexed_at"] = datetime.now().isoformat()
|
||||||
self.manifest['file_count'] = len(self.manifest['files'])
|
self.manifest["file_count"] = len(self.manifest["files"])
|
||||||
self.manifest['chunk_count'] = sum(
|
self.manifest["chunk_count"] = sum(
|
||||||
f['chunks'] for f in self.manifest['files'].values()
|
f["chunks"] for f in self.manifest["files"].values()
|
||||||
)
|
)
|
||||||
self._save_manifest()
|
self._save_manifest()
|
||||||
|
|
||||||
@ -724,11 +839,11 @@ class ProjectIndexer:
|
|||||||
time_taken = (end_time - start_time).total_seconds()
|
time_taken = (end_time - start_time).total_seconds()
|
||||||
|
|
||||||
stats = {
|
stats = {
|
||||||
'files_indexed': len(files_to_index) - len(failed_files),
|
"files_indexed": len(files_to_index) - len(failed_files),
|
||||||
'files_failed': len(failed_files),
|
"files_failed": len(failed_files),
|
||||||
'chunks_created': len(all_records),
|
"chunks_created": len(all_records),
|
||||||
'time_taken': time_taken,
|
"time_taken": time_taken,
|
||||||
'files_per_second': len(files_to_index) / time_taken if time_taken > 0 else 0,
|
"files_per_second": (len(files_to_index) / time_taken if time_taken > 0 else 0),
|
||||||
}
|
}
|
||||||
|
|
||||||
# Print summary
|
# Print summary
|
||||||
@ -739,7 +854,9 @@ class ProjectIndexer:
|
|||||||
console.print(f"Speed: {stats['files_per_second']:.1f} files/second")
|
console.print(f"Speed: {stats['files_per_second']:.1f} files/second")
|
||||||
|
|
||||||
if failed_files:
|
if failed_files:
|
||||||
console.print(f"\n[yellow]Warning:[/yellow] {len(failed_files)} files failed to index")
|
console.print(
|
||||||
|
f"\n[yellow]Warning:[/yellow] {len(failed_files)} files failed to index"
|
||||||
|
)
|
||||||
|
|
||||||
return stats
|
return stats
|
||||||
|
|
||||||
@ -774,14 +891,16 @@ class ProjectIndexer:
|
|||||||
df["total_chunks"] = df["total_chunks"].astype("int32")
|
df["total_chunks"] = df["total_chunks"].astype("int32")
|
||||||
|
|
||||||
# Use vector store's update method (multiply out old, multiply in new)
|
# Use vector store's update method (multiply out old, multiply in new)
|
||||||
if hasattr(self, '_vector_store') and self._vector_store:
|
if hasattr(self, "_vector_store") and self._vector_store:
|
||||||
success = self._vector_store.update_file_vectors(file_str, df)
|
success = self._vector_store.update_file_vectors(file_str, df)
|
||||||
else:
|
else:
|
||||||
# Fallback: delete by file path and add new data
|
# Fallback: delete by file path and add new data
|
||||||
try:
|
try:
|
||||||
self.table.delete(f"file = '{file_str}'")
|
self.table.delete(f"file = '{file_str}'")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.debug(f"Could not delete existing chunks (might not exist): {e}")
|
logger.debug(
|
||||||
|
f"Could not delete existing chunks (might not exist): {e}"
|
||||||
|
)
|
||||||
self.table.add(df)
|
self.table.add(df)
|
||||||
success = True
|
success = True
|
||||||
|
|
||||||
@ -789,23 +908,25 @@ class ProjectIndexer:
|
|||||||
# Update manifest with enhanced file tracking
|
# Update manifest with enhanced file tracking
|
||||||
file_hash = self._get_file_hash(file_path)
|
file_hash = self._get_file_hash(file_path)
|
||||||
stat = file_path.stat()
|
stat = file_path.stat()
|
||||||
if 'files' not in self.manifest:
|
if "files" not in self.manifest:
|
||||||
self.manifest['files'] = {}
|
self.manifest["files"] = {}
|
||||||
self.manifest['files'][file_str] = {
|
self.manifest["files"][file_str] = {
|
||||||
'hash': file_hash,
|
"hash": file_hash,
|
||||||
'size': stat.st_size,
|
"size": stat.st_size,
|
||||||
'mtime': stat.st_mtime,
|
"mtime": stat.st_mtime,
|
||||||
'chunks': len(records),
|
"chunks": len(records),
|
||||||
'last_updated': datetime.now().isoformat(),
|
"last_updated": datetime.now().isoformat(),
|
||||||
'language': records[0].get('language', 'unknown') if records else 'unknown',
|
"language": (
|
||||||
'encoding': 'utf-8'
|
records[0].get("language", "unknown") if records else "unknown"
|
||||||
|
),
|
||||||
|
"encoding": "utf-8",
|
||||||
}
|
}
|
||||||
self._save_manifest()
|
self._save_manifest()
|
||||||
logger.debug(f"Successfully updated {len(records)} chunks for {file_str}")
|
logger.debug(f"Successfully updated {len(records)} chunks for {file_str}")
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
# File exists but has no processable content - remove existing chunks
|
# File exists but has no processable content - remove existing chunks
|
||||||
if hasattr(self, '_vector_store') and self._vector_store:
|
if hasattr(self, "_vector_store") and self._vector_store:
|
||||||
self._vector_store.delete_by_file(file_str)
|
self._vector_store.delete_by_file(file_str)
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
@ -838,7 +959,7 @@ class ProjectIndexer:
|
|||||||
file_str = normalize_relative_path(file_path, self.project_path)
|
file_str = normalize_relative_path(file_path, self.project_path)
|
||||||
|
|
||||||
# Delete from vector store
|
# Delete from vector store
|
||||||
if hasattr(self, '_vector_store') and self._vector_store:
|
if hasattr(self, "_vector_store") and self._vector_store:
|
||||||
success = self._vector_store.delete_by_file(file_str)
|
success = self._vector_store.delete_by_file(file_str)
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
@ -849,8 +970,8 @@ class ProjectIndexer:
|
|||||||
success = False
|
success = False
|
||||||
|
|
||||||
# Update manifest
|
# Update manifest
|
||||||
if success and 'files' in self.manifest and file_str in self.manifest['files']:
|
if success and "files" in self.manifest and file_str in self.manifest["files"]:
|
||||||
del self.manifest['files'][file_str]
|
del self.manifest["files"][file_str]
|
||||||
self._save_manifest()
|
self._save_manifest()
|
||||||
logger.debug(f"Deleted chunks for file: {file_str}")
|
logger.debug(f"Deleted chunks for file: {file_str}")
|
||||||
|
|
||||||
@ -863,20 +984,20 @@ class ProjectIndexer:
|
|||||||
def get_statistics(self) -> Dict[str, Any]:
|
def get_statistics(self) -> Dict[str, Any]:
|
||||||
"""Get indexing statistics."""
|
"""Get indexing statistics."""
|
||||||
stats = {
|
stats = {
|
||||||
'project_path': str(self.project_path),
|
"project_path": str(self.project_path),
|
||||||
'indexed_at': self.manifest.get('indexed_at', 'Never'),
|
"indexed_at": self.manifest.get("indexed_at", "Never"),
|
||||||
'file_count': self.manifest.get('file_count', 0),
|
"file_count": self.manifest.get("file_count", 0),
|
||||||
'chunk_count': self.manifest.get('chunk_count', 0),
|
"chunk_count": self.manifest.get("chunk_count", 0),
|
||||||
'index_size_mb': 0,
|
"index_size_mb": 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Calculate index size
|
# Calculate index size
|
||||||
try:
|
try:
|
||||||
db_path = self.rag_dir / 'code_vectors.lance'
|
db_path = self.rag_dir / "code_vectors.lance"
|
||||||
if db_path.exists():
|
if db_path.exists():
|
||||||
size_bytes = sum(f.stat().st_size for f in db_path.rglob('*') if f.is_file())
|
size_bytes = sum(f.stat().st_size for f in db_path.rglob("*") if f.is_file())
|
||||||
stats['index_size_mb'] = size_bytes / (1024 * 1024)
|
stats["index_size_mb"] = size_bytes / (1024 * 1024)
|
||||||
except:
|
except (OSError, IOError, PermissionError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
return stats
|
return stats
|
||||||
@ -6,17 +6,19 @@ Provides runaway prevention, context management, and intelligent detection
|
|||||||
of problematic model behaviors to ensure reliable user experience.
|
of problematic model behaviors to ensure reliable user experience.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
import re
|
import re
|
||||||
import time
|
import time
|
||||||
import logging
|
|
||||||
from typing import Optional, Dict, List, Tuple
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
from typing import Dict, List, Optional, Tuple
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class SafeguardConfig:
|
class SafeguardConfig:
|
||||||
"""Configuration for LLM safeguards - gentle and educational."""
|
"""Configuration for LLM safeguards - gentle and educational."""
|
||||||
|
|
||||||
max_output_tokens: int = 4000 # Allow longer responses for learning
|
max_output_tokens: int = 4000 # Allow longer responses for learning
|
||||||
max_repetition_ratio: float = 0.7 # Be very permissive - only catch extreme repetition
|
max_repetition_ratio: float = 0.7 # Be very permissive - only catch extreme repetition
|
||||||
max_response_time: int = 120 # Allow 2 minutes for complex thinking
|
max_response_time: int = 120 # Allow 2 minutes for complex thinking
|
||||||
@ -24,6 +26,7 @@ class SafeguardConfig:
|
|||||||
context_window: int = 32000 # Match Qwen3 context length (32K token limit)
|
context_window: int = 32000 # Match Qwen3 context length (32K token limit)
|
||||||
enable_thinking_detection: bool = True # Detect thinking patterns
|
enable_thinking_detection: bool = True # Detect thinking patterns
|
||||||
|
|
||||||
|
|
||||||
class ModelRunawayDetector:
|
class ModelRunawayDetector:
|
||||||
"""Detects and prevents model runaway behaviors."""
|
"""Detects and prevents model runaway behaviors."""
|
||||||
|
|
||||||
@ -35,21 +38,28 @@ class ModelRunawayDetector:
|
|||||||
"""Compile regex patterns for runaway detection."""
|
"""Compile regex patterns for runaway detection."""
|
||||||
return {
|
return {
|
||||||
# Excessive repetition patterns
|
# Excessive repetition patterns
|
||||||
'word_repetition': re.compile(r'\b(\w+)\b(?:\s+\1\b){3,}', re.IGNORECASE),
|
"word_repetition": re.compile(r"\b(\w+)\b(?:\s+\1\b){3,}", re.IGNORECASE),
|
||||||
'phrase_repetition': re.compile(r'(.{10,50}?)\1{2,}', re.DOTALL),
|
"phrase_repetition": re.compile(r"(.{10,50}?)\1{2,}", re.DOTALL),
|
||||||
|
|
||||||
# Thinking loop patterns (small models get stuck)
|
# Thinking loop patterns (small models get stuck)
|
||||||
'thinking_loop': re.compile(r'(let me think|i think|thinking|consider|actually|wait|hmm|well)\s*[.,:]*\s*\1', re.IGNORECASE),
|
"thinking_loop": re.compile(
|
||||||
|
r"(let me think|i think|thinking|consider|actually|wait|hmm|well)\s*[.,:]*\s*\1",
|
||||||
|
re.IGNORECASE,
|
||||||
|
),
|
||||||
# Rambling patterns
|
# Rambling patterns
|
||||||
'excessive_filler': re.compile(r'\b(um|uh|well|you know|like|basically|actually|so|then|and|but|however)\b(?:\s+[^.!?]*){5,}', re.IGNORECASE),
|
"excessive_filler": re.compile(
|
||||||
|
r"\b(um|uh|well|you know|like|basically|actually|so|then|and|but|however)\b(?:\s+[^.!?]*){5,}",
|
||||||
|
re.IGNORECASE,
|
||||||
|
),
|
||||||
# JSON corruption patterns
|
# JSON corruption patterns
|
||||||
'broken_json': re.compile(r'\{[^}]*\{[^}]*\{'), # Nested broken JSON
|
"broken_json": re.compile(r"\{[^}]*\{[^}]*\{"), # Nested broken JSON
|
||||||
'json_repetition': re.compile(r'("[\w_]+"\s*:\s*"[^"]*",?\s*){4,}'), # Repeated JSON fields
|
"json_repetition": re.compile(
|
||||||
|
r'("[\w_]+"\s*:\s*"[^"]*",?\s*){4,}'
|
||||||
|
), # Repeated JSON fields
|
||||||
}
|
}
|
||||||
|
|
||||||
def check_response_quality(self, response: str, query: str, start_time: float) -> Tuple[bool, Optional[str], Optional[str]]:
|
def check_response_quality(
|
||||||
|
self, response: str, query: str, start_time: float
|
||||||
|
) -> Tuple[bool, Optional[str], Optional[str]]:
|
||||||
"""
|
"""
|
||||||
Check response quality and detect runaway behaviors.
|
Check response quality and detect runaway behaviors.
|
||||||
|
|
||||||
@ -81,7 +91,7 @@ class ModelRunawayDetector:
|
|||||||
return False, rambling_issue, self._explain_rambling()
|
return False, rambling_issue, self._explain_rambling()
|
||||||
|
|
||||||
# Check JSON corruption (for structured responses)
|
# Check JSON corruption (for structured responses)
|
||||||
if '{' in response and '}' in response:
|
if "{" in response and "}" in response:
|
||||||
json_issue = self._check_json_corruption(response)
|
json_issue = self._check_json_corruption(response)
|
||||||
if json_issue:
|
if json_issue:
|
||||||
return False, json_issue, self._explain_json_corruption()
|
return False, json_issue, self._explain_json_corruption()
|
||||||
@ -91,11 +101,11 @@ class ModelRunawayDetector:
|
|||||||
def _check_repetition(self, response: str) -> Optional[str]:
|
def _check_repetition(self, response: str) -> Optional[str]:
|
||||||
"""Check for excessive repetition."""
|
"""Check for excessive repetition."""
|
||||||
# Word repetition
|
# Word repetition
|
||||||
if self.response_patterns['word_repetition'].search(response):
|
if self.response_patterns["word_repetition"].search(response):
|
||||||
return "word_repetition"
|
return "word_repetition"
|
||||||
|
|
||||||
# Phrase repetition
|
# Phrase repetition
|
||||||
if self.response_patterns['phrase_repetition'].search(response):
|
if self.response_patterns["phrase_repetition"].search(response):
|
||||||
return "phrase_repetition"
|
return "phrase_repetition"
|
||||||
|
|
||||||
# Calculate repetition ratio (excluding Qwen3 thinking blocks)
|
# Calculate repetition ratio (excluding Qwen3 thinking blocks)
|
||||||
@ -104,7 +114,7 @@ class ModelRunawayDetector:
|
|||||||
# Extract only the actual response (after thinking) for repetition analysis
|
# Extract only the actual response (after thinking) for repetition analysis
|
||||||
thinking_end = response.find("</think>")
|
thinking_end = response.find("</think>")
|
||||||
if thinking_end != -1:
|
if thinking_end != -1:
|
||||||
analysis_text = response[thinking_end + 8:].strip()
|
analysis_text = response[thinking_end + 8 :].strip()
|
||||||
|
|
||||||
# If the actual response (excluding thinking) is short, don't penalize
|
# If the actual response (excluding thinking) is short, don't penalize
|
||||||
if len(analysis_text.split()) < 20:
|
if len(analysis_text.split()) < 20:
|
||||||
@ -121,11 +131,11 @@ class ModelRunawayDetector:
|
|||||||
|
|
||||||
def _check_thinking_loops(self, response: str) -> Optional[str]:
|
def _check_thinking_loops(self, response: str) -> Optional[str]:
|
||||||
"""Check for thinking loops (common in small models)."""
|
"""Check for thinking loops (common in small models)."""
|
||||||
if self.response_patterns['thinking_loop'].search(response):
|
if self.response_patterns["thinking_loop"].search(response):
|
||||||
return "thinking_loop"
|
return "thinking_loop"
|
||||||
|
|
||||||
# Check for excessive meta-commentary
|
# Check for excessive meta-commentary
|
||||||
thinking_words = ['think', 'considering', 'actually', 'wait', 'hmm', 'let me']
|
thinking_words = ["think", "considering", "actually", "wait", "hmm", "let me"]
|
||||||
thinking_count = sum(response.lower().count(word) for word in thinking_words)
|
thinking_count = sum(response.lower().count(word) for word in thinking_words)
|
||||||
|
|
||||||
if thinking_count > 5 and len(response.split()) < 200:
|
if thinking_count > 5 and len(response.split()) < 200:
|
||||||
@ -135,11 +145,11 @@ class ModelRunawayDetector:
|
|||||||
|
|
||||||
def _check_rambling(self, response: str) -> Optional[str]:
|
def _check_rambling(self, response: str) -> Optional[str]:
|
||||||
"""Check for rambling or excessive filler."""
|
"""Check for rambling or excessive filler."""
|
||||||
if self.response_patterns['excessive_filler'].search(response):
|
if self.response_patterns["excessive_filler"].search(response):
|
||||||
return "excessive_filler"
|
return "excessive_filler"
|
||||||
|
|
||||||
# Check for extremely long sentences (sign of rambling)
|
# Check for extremely long sentences (sign of rambling)
|
||||||
sentences = re.split(r'[.!?]+', response)
|
sentences = re.split(r"[.!?]+", response)
|
||||||
long_sentences = [s for s in sentences if len(s.split()) > 50]
|
long_sentences = [s for s in sentences if len(s.split()) > 50]
|
||||||
|
|
||||||
if len(long_sentences) > 2:
|
if len(long_sentences) > 2:
|
||||||
@ -149,10 +159,10 @@ class ModelRunawayDetector:
|
|||||||
|
|
||||||
def _check_json_corruption(self, response: str) -> Optional[str]:
|
def _check_json_corruption(self, response: str) -> Optional[str]:
|
||||||
"""Check for JSON corruption in structured responses."""
|
"""Check for JSON corruption in structured responses."""
|
||||||
if self.response_patterns['broken_json'].search(response):
|
if self.response_patterns["broken_json"].search(response):
|
||||||
return "broken_json"
|
return "broken_json"
|
||||||
|
|
||||||
if self.response_patterns['json_repetition'].search(response):
|
if self.response_patterns["json_repetition"].search(response):
|
||||||
return "json_repetition"
|
return "json_repetition"
|
||||||
|
|
||||||
return None
|
return None
|
||||||
@ -184,7 +194,7 @@ class ModelRunawayDetector:
|
|||||||
• Consider using a larger model if available"""
|
• Consider using a larger model if available"""
|
||||||
|
|
||||||
def _explain_repetition(self, issue_type: str) -> str:
|
def _explain_repetition(self, issue_type: str) -> str:
|
||||||
return f"""🔄 The AI got stuck in repetition loops ({issue_type}).
|
return """🔄 The AI got stuck in repetition loops ({issue_type}).
|
||||||
|
|
||||||
**Why this happens:**
|
**Why this happens:**
|
||||||
• Small models sometimes repeat when uncertain
|
• Small models sometimes repeat when uncertain
|
||||||
@ -243,35 +253,48 @@ class ModelRunawayDetector:
|
|||||||
"""Get specific recovery suggestions based on the issue."""
|
"""Get specific recovery suggestions based on the issue."""
|
||||||
suggestions = []
|
suggestions = []
|
||||||
|
|
||||||
if issue_type in ['thinking_loop', 'excessive_thinking']:
|
if issue_type in ["thinking_loop", "excessive_thinking"]:
|
||||||
suggestions.extend([
|
suggestions.extend(
|
||||||
f"Try synthesis mode: `rag-mini search . \"{query}\" --synthesize`",
|
[
|
||||||
|
f'Try synthesis mode: `rag-mini search . "{query}" --synthesize`',
|
||||||
"Ask more direct questions without 'why' or 'how'",
|
"Ask more direct questions without 'why' or 'how'",
|
||||||
"Break complex questions into smaller parts"
|
"Break complex questions into smaller parts",
|
||||||
])
|
]
|
||||||
|
)
|
||||||
|
|
||||||
elif issue_type in ['word_repetition', 'phrase_repetition', 'high_repetition_ratio']:
|
elif issue_type in [
|
||||||
suggestions.extend([
|
"word_repetition",
|
||||||
|
"phrase_repetition",
|
||||||
|
"high_repetition_ratio",
|
||||||
|
]:
|
||||||
|
suggestions.extend(
|
||||||
|
[
|
||||||
"Try rephrasing your question completely",
|
"Try rephrasing your question completely",
|
||||||
"Use more specific technical terms",
|
"Use more specific technical terms",
|
||||||
f"Try exploration mode: `rag-mini explore .`"
|
"Try exploration mode: `rag-mini explore .`",
|
||||||
])
|
]
|
||||||
|
)
|
||||||
|
|
||||||
elif issue_type == 'timeout':
|
elif issue_type == "timeout":
|
||||||
suggestions.extend([
|
suggestions.extend(
|
||||||
|
[
|
||||||
"Try a simpler version of your question",
|
"Try a simpler version of your question",
|
||||||
"Use synthesis mode for faster responses",
|
"Use synthesis mode for faster responses",
|
||||||
"Check if Ollama is under heavy load"
|
"Check if Ollama is under heavy load",
|
||||||
])
|
]
|
||||||
|
)
|
||||||
|
|
||||||
# Universal suggestions
|
# Universal suggestions
|
||||||
suggestions.extend([
|
suggestions.extend(
|
||||||
|
[
|
||||||
"Consider using a larger model if available (qwen3:1.7b or qwen3:4b)",
|
"Consider using a larger model if available (qwen3:1.7b or qwen3:4b)",
|
||||||
"Check model status: `ollama list`"
|
"Check model status: `ollama list`",
|
||||||
])
|
]
|
||||||
|
)
|
||||||
|
|
||||||
return suggestions
|
return suggestions
|
||||||
|
|
||||||
|
|
||||||
def get_optimal_ollama_parameters(model_name: str) -> Dict[str, any]:
|
def get_optimal_ollama_parameters(model_name: str) -> Dict[str, any]:
|
||||||
"""Get optimal parameters for different Ollama models."""
|
"""Get optimal parameters for different Ollama models."""
|
||||||
|
|
||||||
@ -313,7 +336,10 @@ def get_optimal_ollama_parameters(model_name: str) -> Dict[str, any]:
|
|||||||
|
|
||||||
return base_params
|
return base_params
|
||||||
|
|
||||||
|
|
||||||
# Quick test
|
# Quick test
|
||||||
|
|
||||||
|
|
||||||
def test_safeguards():
|
def test_safeguards():
|
||||||
"""Test the safeguard system."""
|
"""Test the safeguard system."""
|
||||||
detector = ModelRunawayDetector()
|
detector = ModelRunawayDetector()
|
||||||
@ -321,11 +347,14 @@ def test_safeguards():
|
|||||||
# Test repetition detection
|
# Test repetition detection
|
||||||
bad_response = "The user authentication system works by checking user credentials. The user authentication system works by checking user credentials. The user authentication system works by checking user credentials."
|
bad_response = "The user authentication system works by checking user credentials. The user authentication system works by checking user credentials. The user authentication system works by checking user credentials."
|
||||||
|
|
||||||
is_valid, issue, explanation = detector.check_response_quality(bad_response, "auth", time.time())
|
is_valid, issue, explanation = detector.check_response_quality(
|
||||||
|
bad_response, "auth", time.time()
|
||||||
|
)
|
||||||
|
|
||||||
print(f"Repetition test: Valid={is_valid}, Issue={issue}")
|
print(f"Repetition test: Valid={is_valid}, Issue={issue}")
|
||||||
if explanation:
|
if explanation:
|
||||||
print(explanation)
|
print(explanation)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
test_safeguards()
|
test_safeguards()
|
||||||
@ -9,37 +9,56 @@ Takes raw search results and generates coherent, contextual summaries.
|
|||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import time
|
import time
|
||||||
from typing import List, Dict, Any, Optional
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
import requests
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import Any, List, Optional
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from .llm_safeguards import ModelRunawayDetector, SafeguardConfig, get_optimal_ollama_parameters
|
from .llm_safeguards import (
|
||||||
|
ModelRunawayDetector,
|
||||||
|
SafeguardConfig,
|
||||||
|
get_optimal_ollama_parameters,
|
||||||
|
)
|
||||||
from .system_context import get_system_context
|
from .system_context import get_system_context
|
||||||
except ImportError:
|
except ImportError:
|
||||||
# Graceful fallback if safeguards not available
|
# Graceful fallback if safeguards not available
|
||||||
ModelRunawayDetector = None
|
ModelRunawayDetector = None
|
||||||
SafeguardConfig = None
|
SafeguardConfig = None
|
||||||
get_optimal_ollama_parameters = lambda x: {}
|
|
||||||
get_system_context = lambda x=None: ""
|
def get_optimal_ollama_parameters(x):
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def get_system_context(x=None):
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class SynthesisResult:
|
class SynthesisResult:
|
||||||
"""Result of LLM synthesis."""
|
"""Result of LLM synthesis."""
|
||||||
|
|
||||||
summary: str
|
summary: str
|
||||||
key_points: List[str]
|
key_points: List[str]
|
||||||
code_examples: List[str]
|
code_examples: List[str]
|
||||||
suggested_actions: List[str]
|
suggested_actions: List[str]
|
||||||
confidence: float
|
confidence: float
|
||||||
|
|
||||||
|
|
||||||
class LLMSynthesizer:
|
class LLMSynthesizer:
|
||||||
"""Synthesizes RAG search results using Ollama LLMs."""
|
"""Synthesizes RAG search results using Ollama LLMs."""
|
||||||
|
|
||||||
def __init__(self, ollama_url: str = "http://localhost:11434", model: str = None, enable_thinking: bool = False, config=None):
|
def __init__(
|
||||||
self.ollama_url = ollama_url.rstrip('/')
|
self,
|
||||||
|
ollama_url: str = "http://localhost:11434",
|
||||||
|
model: str = None,
|
||||||
|
enable_thinking: bool = False,
|
||||||
|
config=None,
|
||||||
|
):
|
||||||
|
self.ollama_url = ollama_url.rstrip("/")
|
||||||
self.available_models = []
|
self.available_models = []
|
||||||
self.model = model
|
self.model = model
|
||||||
self.enable_thinking = enable_thinking # Default False for synthesis mode
|
self.enable_thinking = enable_thinking # Default False for synthesis mode
|
||||||
@ -58,7 +77,7 @@ class LLMSynthesizer:
|
|||||||
response = requests.get(f"{self.ollama_url}/api/tags", timeout=5)
|
response = requests.get(f"{self.ollama_url}/api/tags", timeout=5)
|
||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
data = response.json()
|
data = response.json()
|
||||||
return [model['name'] for model in data.get('models', [])]
|
return [model["name"] for model in data.get("models", [])]
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"Could not fetch Ollama models: {e}")
|
logger.warning(f"Could not fetch Ollama models: {e}")
|
||||||
return []
|
return []
|
||||||
@ -67,18 +86,31 @@ class LLMSynthesizer:
|
|||||||
"""Select the best available model based on configuration rankings."""
|
"""Select the best available model based on configuration rankings."""
|
||||||
if not self.available_models:
|
if not self.available_models:
|
||||||
# Use config fallback if available, otherwise use default
|
# Use config fallback if available, otherwise use default
|
||||||
if self.config and hasattr(self.config, 'llm') and hasattr(self.config.llm, 'model_rankings') and self.config.llm.model_rankings:
|
if (
|
||||||
|
self.config
|
||||||
|
and hasattr(self.config, "llm")
|
||||||
|
and hasattr(self.config.llm, "model_rankings")
|
||||||
|
and self.config.llm.model_rankings
|
||||||
|
):
|
||||||
return self.config.llm.model_rankings[0] # First preferred model
|
return self.config.llm.model_rankings[0] # First preferred model
|
||||||
return "qwen2.5:1.5b" # System fallback only if no config
|
return "qwen2.5:1.5b" # System fallback only if no config
|
||||||
|
|
||||||
# Get model rankings from config or use defaults
|
# Get model rankings from config or use defaults
|
||||||
if self.config and hasattr(self.config, 'llm') and hasattr(self.config.llm, 'model_rankings'):
|
if (
|
||||||
|
self.config
|
||||||
|
and hasattr(self.config, "llm")
|
||||||
|
and hasattr(self.config.llm, "model_rankings")
|
||||||
|
):
|
||||||
model_rankings = self.config.llm.model_rankings
|
model_rankings = self.config.llm.model_rankings
|
||||||
else:
|
else:
|
||||||
# Fallback rankings if no config
|
# Fallback rankings if no config
|
||||||
model_rankings = [
|
model_rankings = [
|
||||||
"qwen3:1.7b", "qwen3:0.6b", "qwen3:4b", "qwen2.5:3b",
|
"qwen3:1.7b",
|
||||||
"qwen2.5:1.5b", "qwen2.5-coder:1.5b"
|
"qwen3:0.6b",
|
||||||
|
"qwen3:4b",
|
||||||
|
"qwen2.5:3b",
|
||||||
|
"qwen2.5:1.5b",
|
||||||
|
"qwen2.5-coder:1.5b",
|
||||||
]
|
]
|
||||||
|
|
||||||
# Find first available model from our ranked list (exact matches first)
|
# Find first available model from our ranked list (exact matches first)
|
||||||
@ -90,12 +122,14 @@ class LLMSynthesizer:
|
|||||||
return available_model
|
return available_model
|
||||||
|
|
||||||
# Partial match with version handling (e.g., "qwen3:1.7b" matches "qwen3:1.7b-q8_0")
|
# Partial match with version handling (e.g., "qwen3:1.7b" matches "qwen3:1.7b-q8_0")
|
||||||
preferred_parts = preferred_model.lower().split(':')
|
preferred_parts = preferred_model.lower().split(":")
|
||||||
available_parts = available_model.lower().split(':')
|
available_parts = available_model.lower().split(":")
|
||||||
|
|
||||||
if len(preferred_parts) >= 2 and len(available_parts) >= 2:
|
if len(preferred_parts) >= 2 and len(available_parts) >= 2:
|
||||||
if (preferred_parts[0] == available_parts[0] and
|
if (
|
||||||
preferred_parts[1] in available_parts[1]):
|
preferred_parts[0] == available_parts[0]
|
||||||
|
and preferred_parts[1] in available_parts[1]
|
||||||
|
):
|
||||||
logger.info(f"Selected version match model: {available_model}")
|
logger.info(f"Selected version match model: {available_model}")
|
||||||
return available_model
|
return available_model
|
||||||
|
|
||||||
@ -122,9 +156,9 @@ class LLMSynthesizer:
|
|||||||
def _get_optimal_context_size(self, model_name: str) -> int:
|
def _get_optimal_context_size(self, model_name: str) -> int:
|
||||||
"""Get optimal context size based on model capabilities and configuration."""
|
"""Get optimal context size based on model capabilities and configuration."""
|
||||||
# Get configured context window
|
# Get configured context window
|
||||||
if self.config and hasattr(self.config, 'llm'):
|
if self.config and hasattr(self.config, "llm"):
|
||||||
configured_context = self.config.llm.context_window
|
configured_context = self.config.llm.context_window
|
||||||
auto_context = getattr(self.config.llm, 'auto_context', True)
|
auto_context = getattr(self.config.llm, "auto_context", True)
|
||||||
else:
|
else:
|
||||||
configured_context = 16384 # Default to 16K
|
configured_context = 16384 # Default to 16K
|
||||||
auto_context = True
|
auto_context = True
|
||||||
@ -132,23 +166,21 @@ class LLMSynthesizer:
|
|||||||
# Model-specific maximum context windows (based on research)
|
# Model-specific maximum context windows (based on research)
|
||||||
model_limits = {
|
model_limits = {
|
||||||
# Qwen3 models with native context support
|
# Qwen3 models with native context support
|
||||||
'qwen3:0.6b': 32768, # 32K native
|
"qwen3:0.6b": 32768, # 32K native
|
||||||
'qwen3:1.7b': 32768, # 32K native
|
"qwen3:1.7b": 32768, # 32K native
|
||||||
'qwen3:4b': 131072, # 131K with YaRN extension
|
"qwen3:4b": 131072, # 131K with YaRN extension
|
||||||
|
|
||||||
# Qwen2.5 models
|
# Qwen2.5 models
|
||||||
'qwen2.5:1.5b': 32768, # 32K native
|
"qwen2.5:1.5b": 32768, # 32K native
|
||||||
'qwen2.5:3b': 32768, # 32K native
|
"qwen2.5:3b": 32768, # 32K native
|
||||||
'qwen2.5-coder:1.5b': 32768, # 32K native
|
"qwen2.5-coder:1.5b": 32768, # 32K native
|
||||||
|
|
||||||
# Fallback for unknown models
|
# Fallback for unknown models
|
||||||
'default': 8192
|
"default": 8192,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Find model limit (check for partial matches)
|
# Find model limit (check for partial matches)
|
||||||
model_limit = model_limits.get('default', 8192)
|
model_limit = model_limits.get("default", 8192)
|
||||||
for model_pattern, limit in model_limits.items():
|
for model_pattern, limit in model_limits.items():
|
||||||
if model_pattern != 'default' and model_pattern.lower() in model_name.lower():
|
if model_pattern != "default" and model_pattern.lower() in model_name.lower():
|
||||||
model_limit = limit
|
model_limit = limit
|
||||||
break
|
break
|
||||||
|
|
||||||
@ -161,7 +193,9 @@ class LLMSynthesizer:
|
|||||||
# Ensure minimum usable context for RAG
|
# Ensure minimum usable context for RAG
|
||||||
optimal_context = max(optimal_context, 4096) # Minimum 4K for basic RAG
|
optimal_context = max(optimal_context, 4096) # Minimum 4K for basic RAG
|
||||||
|
|
||||||
logger.debug(f"Context for {model_name}: {optimal_context} tokens (configured: {configured_context}, limit: {model_limit})")
|
logger.debug(
|
||||||
|
f"Context for {model_name}: {optimal_context} tokens (configured: {configured_context}, limit: {model_limit})"
|
||||||
|
)
|
||||||
return optimal_context
|
return optimal_context
|
||||||
|
|
||||||
def is_available(self) -> bool:
|
def is_available(self) -> bool:
|
||||||
@ -169,7 +203,14 @@ class LLMSynthesizer:
|
|||||||
self._ensure_initialized()
|
self._ensure_initialized()
|
||||||
return len(self.available_models) > 0
|
return len(self.available_models) > 0
|
||||||
|
|
||||||
def _call_ollama(self, prompt: str, temperature: float = 0.3, disable_thinking: bool = False, use_streaming: bool = True, collapse_thinking: bool = True) -> Optional[str]:
|
def _call_ollama(
|
||||||
|
self,
|
||||||
|
prompt: str,
|
||||||
|
temperature: float = 0.3,
|
||||||
|
disable_thinking: bool = False,
|
||||||
|
use_streaming: bool = True,
|
||||||
|
collapse_thinking: bool = True,
|
||||||
|
) -> Optional[str]:
|
||||||
"""Make a call to Ollama API with safeguards."""
|
"""Make a call to Ollama API with safeguards."""
|
||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
|
|
||||||
@ -181,7 +222,9 @@ class LLMSynthesizer:
|
|||||||
model_to_use = self.model
|
model_to_use = self.model
|
||||||
if self.model not in self.available_models:
|
if self.model not in self.available_models:
|
||||||
# Refresh model list in case of race condition
|
# Refresh model list in case of race condition
|
||||||
logger.warning(f"Configured model {self.model} not in available list, refreshing...")
|
logger.warning(
|
||||||
|
f"Configured model {self.model} not in available list, refreshing..."
|
||||||
|
)
|
||||||
self.available_models = self._get_available_models()
|
self.available_models = self._get_available_models()
|
||||||
|
|
||||||
if self.model in self.available_models:
|
if self.model in self.available_models:
|
||||||
@ -235,21 +278,25 @@ class LLMSynthesizer:
|
|||||||
"temperature": qwen3_temp,
|
"temperature": qwen3_temp,
|
||||||
"top_p": qwen3_top_p,
|
"top_p": qwen3_top_p,
|
||||||
"top_k": qwen3_top_k,
|
"top_k": qwen3_top_k,
|
||||||
"num_ctx": self._get_optimal_context_size(model_to_use), # Dynamic context based on model and config
|
"num_ctx": self._get_optimal_context_size(
|
||||||
|
model_to_use
|
||||||
|
), # Dynamic context based on model and config
|
||||||
"num_predict": optimal_params.get("num_predict", 2000),
|
"num_predict": optimal_params.get("num_predict", 2000),
|
||||||
"repeat_penalty": optimal_params.get("repeat_penalty", 1.1),
|
"repeat_penalty": optimal_params.get("repeat_penalty", 1.1),
|
||||||
"presence_penalty": qwen3_presence
|
"presence_penalty": qwen3_presence,
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
# Handle streaming with thinking display
|
# Handle streaming with thinking display
|
||||||
if use_streaming:
|
if use_streaming:
|
||||||
return self._handle_streaming_with_thinking_display(payload, model_to_use, use_thinking, start_time, collapse_thinking)
|
return self._handle_streaming_with_thinking_display(
|
||||||
|
payload, model_to_use, use_thinking, start_time, collapse_thinking
|
||||||
|
)
|
||||||
|
|
||||||
response = requests.post(
|
response = requests.post(
|
||||||
f"{self.ollama_url}/api/generate",
|
f"{self.ollama_url}/api/generate",
|
||||||
json=payload,
|
json=payload,
|
||||||
timeout=65 # Slightly longer than safeguard timeout
|
timeout=65, # Slightly longer than safeguard timeout
|
||||||
)
|
)
|
||||||
|
|
||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
@ -257,39 +304,51 @@ class LLMSynthesizer:
|
|||||||
|
|
||||||
# All models use standard response format
|
# All models use standard response format
|
||||||
# Qwen3 thinking tokens are embedded in the response content itself as <think>...</think>
|
# Qwen3 thinking tokens are embedded in the response content itself as <think>...</think>
|
||||||
raw_response = result.get('response', '').strip()
|
raw_response = result.get("response", "").strip()
|
||||||
|
|
||||||
# Log thinking content for Qwen3 debugging
|
# Log thinking content for Qwen3 debugging
|
||||||
if "qwen3" in model_to_use.lower() and use_thinking and "<think>" in raw_response:
|
if (
|
||||||
|
"qwen3" in model_to_use.lower()
|
||||||
|
and use_thinking
|
||||||
|
and "<think>" in raw_response
|
||||||
|
):
|
||||||
thinking_start = raw_response.find("<think>")
|
thinking_start = raw_response.find("<think>")
|
||||||
thinking_end = raw_response.find("</think>")
|
thinking_end = raw_response.find("</think>")
|
||||||
if thinking_start != -1 and thinking_end != -1:
|
if thinking_start != -1 and thinking_end != -1:
|
||||||
thinking_content = raw_response[thinking_start+7:thinking_end]
|
thinking_content = raw_response[thinking_start + 7 : thinking_end]
|
||||||
logger.info(f"Qwen3 thinking: {thinking_content[:100]}...")
|
logger.info(f"Qwen3 thinking: {thinking_content[:100]}...")
|
||||||
|
|
||||||
# Apply safeguards to check response quality
|
# Apply safeguards to check response quality
|
||||||
if self.safeguard_detector and raw_response:
|
if self.safeguard_detector and raw_response:
|
||||||
is_valid, issue_type, explanation = self.safeguard_detector.check_response_quality(
|
is_valid, issue_type, explanation = (
|
||||||
raw_response, prompt[:100], start_time # First 100 chars of prompt for context
|
self.safeguard_detector.check_response_quality(
|
||||||
|
raw_response,
|
||||||
|
prompt[:100],
|
||||||
|
start_time, # First 100 chars of prompt for context
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
if not is_valid:
|
if not is_valid:
|
||||||
logger.warning(f"Safeguard triggered: {issue_type}")
|
logger.warning(f"Safeguard triggered: {issue_type}")
|
||||||
# Preserve original response but add safeguard warning
|
# Preserve original response but add safeguard warning
|
||||||
return self._create_safeguard_response_with_content(issue_type, explanation, raw_response)
|
return self._create_safeguard_response_with_content(
|
||||||
|
issue_type, explanation, raw_response
|
||||||
|
)
|
||||||
|
|
||||||
# Clean up thinking tags from final response
|
# Clean up thinking tags from final response
|
||||||
cleaned_response = raw_response
|
cleaned_response = raw_response
|
||||||
if '<think>' in cleaned_response or '</think>' in cleaned_response:
|
if "<think>" in cleaned_response or "</think>" in cleaned_response:
|
||||||
# Remove thinking content but preserve the rest
|
# Remove thinking content but preserve the rest
|
||||||
cleaned_response = cleaned_response.replace('<think>', '').replace('</think>', '')
|
cleaned_response = cleaned_response.replace("<think>", "").replace(
|
||||||
|
"</think>", ""
|
||||||
|
)
|
||||||
# Clean up extra whitespace that might be left
|
# Clean up extra whitespace that might be left
|
||||||
lines = cleaned_response.split('\n')
|
lines = cleaned_response.split("\n")
|
||||||
cleaned_lines = []
|
cleaned_lines = []
|
||||||
for line in lines:
|
for line in lines:
|
||||||
if line.strip(): # Only keep non-empty lines
|
if line.strip(): # Only keep non-empty lines
|
||||||
cleaned_lines.append(line)
|
cleaned_lines.append(line)
|
||||||
cleaned_response = '\n'.join(cleaned_lines)
|
cleaned_response = "\n".join(cleaned_lines)
|
||||||
|
|
||||||
return cleaned_response.strip()
|
return cleaned_response.strip()
|
||||||
else:
|
else:
|
||||||
@ -300,9 +359,11 @@ class LLMSynthesizer:
|
|||||||
logger.error(f"Ollama call failed: {e}")
|
logger.error(f"Ollama call failed: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _create_safeguard_response(self, issue_type: str, explanation: str, original_prompt: str) -> str:
|
def _create_safeguard_response(
|
||||||
|
self, issue_type: str, explanation: str, original_prompt: str
|
||||||
|
) -> str:
|
||||||
"""Create a helpful response when safeguards are triggered."""
|
"""Create a helpful response when safeguards are triggered."""
|
||||||
return f"""⚠️ Model Response Issue Detected
|
return """⚠️ Model Response Issue Detected
|
||||||
|
|
||||||
{explanation}
|
{explanation}
|
||||||
|
|
||||||
@ -318,7 +379,9 @@ class LLMSynthesizer:
|
|||||||
|
|
||||||
This is normal with smaller AI models and helps ensure you get quality responses."""
|
This is normal with smaller AI models and helps ensure you get quality responses."""
|
||||||
|
|
||||||
def _create_safeguard_response_with_content(self, issue_type: str, explanation: str, original_response: str) -> str:
|
def _create_safeguard_response_with_content(
|
||||||
|
self, issue_type: str, explanation: str, original_response: str
|
||||||
|
) -> str:
|
||||||
"""Create a response that preserves the original content but adds a safeguard warning."""
|
"""Create a response that preserves the original content but adds a safeguard warning."""
|
||||||
|
|
||||||
# For Qwen3, extract the actual response (after thinking)
|
# For Qwen3, extract the actual response (after thinking)
|
||||||
@ -326,11 +389,11 @@ This is normal with smaller AI models and helps ensure you get quality responses
|
|||||||
if "<think>" in original_response and "</think>" in original_response:
|
if "<think>" in original_response and "</think>" in original_response:
|
||||||
thinking_end = original_response.find("</think>")
|
thinking_end = original_response.find("</think>")
|
||||||
if thinking_end != -1:
|
if thinking_end != -1:
|
||||||
actual_response = original_response[thinking_end + 8:].strip()
|
actual_response = original_response[thinking_end + 8 :].strip()
|
||||||
|
|
||||||
# If we have useful content, preserve it with a warning
|
# If we have useful content, preserve it with a warning
|
||||||
if len(actual_response.strip()) > 20:
|
if len(actual_response.strip()) > 20:
|
||||||
return f"""⚠️ **Response Quality Warning** ({issue_type})
|
return """⚠️ **Response Quality Warning** ({issue_type})
|
||||||
|
|
||||||
{explanation}
|
{explanation}
|
||||||
|
|
||||||
@ -345,7 +408,7 @@ This is normal with smaller AI models and helps ensure you get quality responses
|
|||||||
💡 **Note**: This response may have quality issues. Consider rephrasing your question or trying exploration mode for better results."""
|
💡 **Note**: This response may have quality issues. Consider rephrasing your question or trying exploration mode for better results."""
|
||||||
else:
|
else:
|
||||||
# If content is too short or problematic, use the original safeguard response
|
# If content is too short or problematic, use the original safeguard response
|
||||||
return f"""⚠️ Model Response Issue Detected
|
return """⚠️ Model Response Issue Detected
|
||||||
|
|
||||||
{explanation}
|
{explanation}
|
||||||
|
|
||||||
@ -358,17 +421,20 @@ This is normal with smaller AI models and helps ensure you get quality responses
|
|||||||
|
|
||||||
This is normal with smaller AI models and helps ensure you get quality responses."""
|
This is normal with smaller AI models and helps ensure you get quality responses."""
|
||||||
|
|
||||||
def _handle_streaming_with_thinking_display(self, payload: dict, model_name: str, use_thinking: bool, start_time: float, collapse_thinking: bool = True) -> Optional[str]:
|
def _handle_streaming_with_thinking_display(
|
||||||
|
self,
|
||||||
|
payload: dict,
|
||||||
|
model_name: str,
|
||||||
|
use_thinking: bool,
|
||||||
|
start_time: float,
|
||||||
|
collapse_thinking: bool = True,
|
||||||
|
) -> Optional[str]:
|
||||||
"""Handle streaming response with real-time thinking token display."""
|
"""Handle streaming response with real-time thinking token display."""
|
||||||
import json
|
import json
|
||||||
import sys
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = requests.post(
|
response = requests.post(
|
||||||
f"{self.ollama_url}/api/generate",
|
f"{self.ollama_url}/api/generate", json=payload, stream=True, timeout=65
|
||||||
json=payload,
|
|
||||||
stream=True,
|
|
||||||
timeout=65
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if response.status_code != 200:
|
if response.status_code != 200:
|
||||||
@ -382,44 +448,54 @@ This is normal with smaller AI models and helps ensure you get quality responses
|
|||||||
thinking_lines_printed = 0
|
thinking_lines_printed = 0
|
||||||
|
|
||||||
# ANSI escape codes for colors and cursor control
|
# ANSI escape codes for colors and cursor control
|
||||||
GRAY = '\033[90m' # Dark gray for thinking
|
GRAY = "\033[90m" # Dark gray for thinking
|
||||||
LIGHT_GRAY = '\033[37m' # Light gray alternative
|
# "\033[37m" # Light gray alternative # Unused variable removed
|
||||||
RESET = '\033[0m' # Reset color
|
RESET = "\033[0m" # Reset color
|
||||||
CLEAR_LINE = '\033[2K' # Clear entire line
|
CLEAR_LINE = "\033[2K" # Clear entire line
|
||||||
CURSOR_UP = '\033[A' # Move cursor up one line
|
CURSOR_UP = "\033[A" # Move cursor up one line
|
||||||
|
|
||||||
print(f"\n💭 {GRAY}Thinking...{RESET}", flush=True)
|
print(f"\n💭 {GRAY}Thinking...{RESET}", flush=True)
|
||||||
|
|
||||||
for line in response.iter_lines():
|
for line in response.iter_lines():
|
||||||
if line:
|
if line:
|
||||||
try:
|
try:
|
||||||
chunk_data = json.loads(line.decode('utf-8'))
|
chunk_data = json.loads(line.decode("utf-8"))
|
||||||
chunk_text = chunk_data.get('response', '')
|
chunk_text = chunk_data.get("response", "")
|
||||||
|
|
||||||
if chunk_text:
|
if chunk_text:
|
||||||
full_response += chunk_text
|
full_response += chunk_text
|
||||||
|
|
||||||
# Handle thinking tokens
|
# Handle thinking tokens
|
||||||
if use_thinking and '<think>' in chunk_text:
|
if use_thinking and "<think>" in chunk_text:
|
||||||
is_in_thinking = True
|
is_in_thinking = True
|
||||||
chunk_text = chunk_text.replace('<think>', '')
|
chunk_text = chunk_text.replace("<think>", "")
|
||||||
|
|
||||||
if is_in_thinking and '</think>' in chunk_text:
|
if is_in_thinking and "</think>" in chunk_text:
|
||||||
is_in_thinking = False
|
is_in_thinking = False
|
||||||
is_thinking_complete = True
|
is_thinking_complete = True
|
||||||
chunk_text = chunk_text.replace('</think>', '')
|
chunk_text = chunk_text.replace("</think>", "")
|
||||||
|
|
||||||
if collapse_thinking:
|
if collapse_thinking:
|
||||||
# Clear thinking content and show completion
|
# Clear thinking content and show completion
|
||||||
# Move cursor up to clear thinking lines
|
# Move cursor up to clear thinking lines
|
||||||
for _ in range(thinking_lines_printed + 1):
|
for _ in range(thinking_lines_printed + 1):
|
||||||
print(f"{CURSOR_UP}{CLEAR_LINE}", end='', flush=True)
|
print(
|
||||||
|
f"{CURSOR_UP}{CLEAR_LINE}",
|
||||||
|
end="",
|
||||||
|
flush=True,
|
||||||
|
)
|
||||||
|
|
||||||
print(f"💭 {GRAY}Thinking complete ✓{RESET}", flush=True)
|
print(
|
||||||
|
f"💭 {GRAY}Thinking complete ✓{RESET}",
|
||||||
|
flush=True,
|
||||||
|
)
|
||||||
thinking_lines_printed = 0
|
thinking_lines_printed = 0
|
||||||
else:
|
else:
|
||||||
# Keep thinking visible, just show completion
|
# Keep thinking visible, just show completion
|
||||||
print(f"\n💭 {GRAY}Thinking complete ✓{RESET}", flush=True)
|
print(
|
||||||
|
f"\n💭 {GRAY}Thinking complete ✓{RESET}",
|
||||||
|
flush=True,
|
||||||
|
)
|
||||||
|
|
||||||
print("🤖 AI Response:", flush=True)
|
print("🤖 AI Response:", flush=True)
|
||||||
continue
|
continue
|
||||||
@ -429,11 +505,17 @@ This is normal with smaller AI models and helps ensure you get quality responses
|
|||||||
thinking_content += chunk_text
|
thinking_content += chunk_text
|
||||||
|
|
||||||
# Handle line breaks and word wrapping properly
|
# Handle line breaks and word wrapping properly
|
||||||
if ' ' in chunk_text or '\n' in chunk_text or len(thinking_content) > 100:
|
if (
|
||||||
|
" " in chunk_text
|
||||||
|
or "\n" in chunk_text
|
||||||
|
or len(thinking_content) > 100
|
||||||
|
):
|
||||||
# Split by sentences for better readability
|
# Split by sentences for better readability
|
||||||
sentences = thinking_content.replace('\n', ' ').split('. ')
|
sentences = thinking_content.replace("\n", " ").split(". ")
|
||||||
|
|
||||||
for sentence in sentences[:-1]: # Process complete sentences
|
for sentence in sentences[
|
||||||
|
:-1
|
||||||
|
]: # Process complete sentences
|
||||||
sentence = sentence.strip()
|
sentence = sentence.strip()
|
||||||
if sentence:
|
if sentence:
|
||||||
# Word wrap long sentences
|
# Word wrap long sentences
|
||||||
@ -442,32 +524,44 @@ This is normal with smaller AI models and helps ensure you get quality responses
|
|||||||
for word in words:
|
for word in words:
|
||||||
if len(line + " " + word) > 70:
|
if len(line + " " + word) > 70:
|
||||||
if line:
|
if line:
|
||||||
print(f"{GRAY} {line.strip()}{RESET}", flush=True)
|
print(
|
||||||
|
f"{GRAY} {line.strip()}{RESET}",
|
||||||
|
flush=True,
|
||||||
|
)
|
||||||
thinking_lines_printed += 1
|
thinking_lines_printed += 1
|
||||||
line = word
|
line = word
|
||||||
else:
|
else:
|
||||||
line += " " + word if line else word
|
line += " " + word if line else word
|
||||||
|
|
||||||
if line.strip():
|
if line.strip():
|
||||||
print(f"{GRAY} {line.strip()}.{RESET}", flush=True)
|
print(
|
||||||
|
f"{GRAY} {line.strip()}.{RESET}",
|
||||||
|
flush=True,
|
||||||
|
)
|
||||||
thinking_lines_printed += 1
|
thinking_lines_printed += 1
|
||||||
|
|
||||||
# Keep the last incomplete sentence for next iteration
|
# Keep the last incomplete sentence for next iteration
|
||||||
thinking_content = sentences[-1] if sentences else ""
|
thinking_content = sentences[-1] if sentences else ""
|
||||||
|
|
||||||
# Display regular response content (skip any leftover thinking)
|
# Display regular response content (skip any leftover thinking)
|
||||||
elif not is_in_thinking and is_thinking_complete and chunk_text.strip():
|
elif (
|
||||||
|
not is_in_thinking
|
||||||
|
and is_thinking_complete
|
||||||
|
and chunk_text.strip()
|
||||||
|
):
|
||||||
# Filter out any remaining thinking tags that might leak through
|
# Filter out any remaining thinking tags that might leak through
|
||||||
clean_text = chunk_text
|
clean_text = chunk_text
|
||||||
if '<think>' in clean_text or '</think>' in clean_text:
|
if "<think>" in clean_text or "</think>" in clean_text:
|
||||||
clean_text = clean_text.replace('<think>', '').replace('</think>', '')
|
clean_text = clean_text.replace("<think>", "").replace(
|
||||||
|
"</think>", ""
|
||||||
|
)
|
||||||
|
|
||||||
if clean_text: # Remove .strip() here to preserve whitespace
|
if clean_text: # Remove .strip() here to preserve whitespace
|
||||||
# Preserve all formatting including newlines and spaces
|
# Preserve all formatting including newlines and spaces
|
||||||
print(clean_text, end='', flush=True)
|
print(clean_text, end="", flush=True)
|
||||||
|
|
||||||
# Check if response is done
|
# Check if response is done
|
||||||
if chunk_data.get('done', False):
|
if chunk_data.get("done", False):
|
||||||
print() # Final newline
|
print() # Final newline
|
||||||
break
|
break
|
||||||
|
|
||||||
@ -483,16 +577,15 @@ This is normal with smaller AI models and helps ensure you get quality responses
|
|||||||
logger.error(f"Streaming failed: {e}")
|
logger.error(f"Streaming failed: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _handle_streaming_with_early_stop(self, payload: dict, model_name: str, use_thinking: bool, start_time: float) -> Optional[str]:
|
def _handle_streaming_with_early_stop(
|
||||||
|
self, payload: dict, model_name: str, use_thinking: bool, start_time: float
|
||||||
|
) -> Optional[str]:
|
||||||
"""Handle streaming response with intelligent early stopping."""
|
"""Handle streaming response with intelligent early stopping."""
|
||||||
import json
|
import json
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = requests.post(
|
response = requests.post(
|
||||||
f"{self.ollama_url}/api/generate",
|
f"{self.ollama_url}/api/generate", json=payload, stream=True, timeout=65
|
||||||
json=payload,
|
|
||||||
stream=True,
|
|
||||||
timeout=65
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if response.status_code != 200:
|
if response.status_code != 200:
|
||||||
@ -502,14 +595,16 @@ This is normal with smaller AI models and helps ensure you get quality responses
|
|||||||
full_response = ""
|
full_response = ""
|
||||||
word_buffer = []
|
word_buffer = []
|
||||||
repetition_window = 30 # Check last 30 words for repetition (more context)
|
repetition_window = 30 # Check last 30 words for repetition (more context)
|
||||||
stop_threshold = 0.8 # Stop only if 80% of recent words are repetitive (very permissive)
|
stop_threshold = (
|
||||||
|
0.8 # Stop only if 80% of recent words are repetitive (very permissive)
|
||||||
|
)
|
||||||
min_response_length = 100 # Don't early stop until we have at least 100 chars
|
min_response_length = 100 # Don't early stop until we have at least 100 chars
|
||||||
|
|
||||||
for line in response.iter_lines():
|
for line in response.iter_lines():
|
||||||
if line:
|
if line:
|
||||||
try:
|
try:
|
||||||
chunk_data = json.loads(line.decode('utf-8'))
|
chunk_data = json.loads(line.decode("utf-8"))
|
||||||
chunk_text = chunk_data.get('response', '')
|
chunk_text = chunk_data.get("response", "")
|
||||||
|
|
||||||
if chunk_text:
|
if chunk_text:
|
||||||
full_response += chunk_text
|
full_response += chunk_text
|
||||||
@ -523,28 +618,47 @@ This is normal with smaller AI models and helps ensure you get quality responses
|
|||||||
word_buffer = word_buffer[-repetition_window:]
|
word_buffer = word_buffer[-repetition_window:]
|
||||||
|
|
||||||
# Check for repetition patterns after we have enough words AND content
|
# Check for repetition patterns after we have enough words AND content
|
||||||
if len(word_buffer) >= repetition_window and len(full_response) >= min_response_length:
|
if (
|
||||||
|
len(word_buffer) >= repetition_window
|
||||||
|
and len(full_response) >= min_response_length
|
||||||
|
):
|
||||||
unique_words = set(word_buffer)
|
unique_words = set(word_buffer)
|
||||||
repetition_ratio = 1 - (len(unique_words) / len(word_buffer))
|
repetition_ratio = 1 - (len(unique_words) / len(word_buffer))
|
||||||
|
|
||||||
# Early stop only if repetition is EXTREMELY high (80%+)
|
# Early stop only if repetition is EXTREMELY high (80%+)
|
||||||
if repetition_ratio > stop_threshold:
|
if repetition_ratio > stop_threshold:
|
||||||
logger.info(f"Early stopping due to repetition: {repetition_ratio:.2f}")
|
logger.info(
|
||||||
|
f"Early stopping due to repetition: {repetition_ratio:.2f}"
|
||||||
|
)
|
||||||
|
|
||||||
# Add a gentle completion to the response
|
# Add a gentle completion to the response
|
||||||
if not full_response.strip().endswith(('.', '!', '?')):
|
if not full_response.strip().endswith((".", "!", "?")):
|
||||||
full_response += "..."
|
full_response += "..."
|
||||||
|
|
||||||
# Send stop signal to model (attempt to gracefully stop)
|
# Send stop signal to model (attempt to gracefully stop)
|
||||||
try:
|
try:
|
||||||
stop_payload = {"model": model_name, "stop": True}
|
stop_payload = {
|
||||||
requests.post(f"{self.ollama_url}/api/generate", json=stop_payload, timeout=2)
|
"model": model_name,
|
||||||
except:
|
"stop": True,
|
||||||
|
}
|
||||||
|
requests.post(
|
||||||
|
f"{self.ollama_url}/api/generate",
|
||||||
|
json=stop_payload,
|
||||||
|
timeout=2,
|
||||||
|
)
|
||||||
|
except (
|
||||||
|
ConnectionError,
|
||||||
|
FileNotFoundError,
|
||||||
|
IOError,
|
||||||
|
OSError,
|
||||||
|
TimeoutError,
|
||||||
|
requests.RequestException,
|
||||||
|
):
|
||||||
pass # If stop fails, we already have partial response
|
pass # If stop fails, we already have partial response
|
||||||
|
|
||||||
break
|
break
|
||||||
|
|
||||||
if chunk_data.get('done', False):
|
if chunk_data.get("done", False):
|
||||||
break
|
break
|
||||||
|
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
@ -552,16 +666,18 @@ This is normal with smaller AI models and helps ensure you get quality responses
|
|||||||
|
|
||||||
# Clean up thinking tags from final response
|
# Clean up thinking tags from final response
|
||||||
cleaned_response = full_response
|
cleaned_response = full_response
|
||||||
if '<think>' in cleaned_response or '</think>' in cleaned_response:
|
if "<think>" in cleaned_response or "</think>" in cleaned_response:
|
||||||
# Remove thinking content but preserve the rest
|
# Remove thinking content but preserve the rest
|
||||||
cleaned_response = cleaned_response.replace('<think>', '').replace('</think>', '')
|
cleaned_response = cleaned_response.replace("<think>", "").replace(
|
||||||
|
"</think>", ""
|
||||||
|
)
|
||||||
# Clean up extra whitespace that might be left
|
# Clean up extra whitespace that might be left
|
||||||
lines = cleaned_response.split('\n')
|
lines = cleaned_response.split("\n")
|
||||||
cleaned_lines = []
|
cleaned_lines = []
|
||||||
for line in lines:
|
for line in lines:
|
||||||
if line.strip(): # Only keep non-empty lines
|
if line.strip(): # Only keep non-empty lines
|
||||||
cleaned_lines.append(line)
|
cleaned_lines.append(line)
|
||||||
cleaned_response = '\n'.join(cleaned_lines)
|
cleaned_response = "\n".join(cleaned_lines)
|
||||||
|
|
||||||
return cleaned_response.strip()
|
return cleaned_response.strip()
|
||||||
|
|
||||||
@ -569,7 +685,9 @@ This is normal with smaller AI models and helps ensure you get quality responses
|
|||||||
logger.error(f"Streaming with early stop failed: {e}")
|
logger.error(f"Streaming with early stop failed: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def synthesize_search_results(self, query: str, results: List[Any], project_path: Path) -> SynthesisResult:
|
def synthesize_search_results(
|
||||||
|
self, query: str, results: List[Any], project_path: Path
|
||||||
|
) -> SynthesisResult:
|
||||||
"""Synthesize search results into a coherent summary."""
|
"""Synthesize search results into a coherent summary."""
|
||||||
|
|
||||||
self._ensure_initialized()
|
self._ensure_initialized()
|
||||||
@ -579,29 +697,31 @@ This is normal with smaller AI models and helps ensure you get quality responses
|
|||||||
key_points=[],
|
key_points=[],
|
||||||
code_examples=[],
|
code_examples=[],
|
||||||
suggested_actions=["Install and run Ollama with a model"],
|
suggested_actions=["Install and run Ollama with a model"],
|
||||||
confidence=0.0
|
confidence=0.0,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Prepare context from search results
|
# Prepare context from search results
|
||||||
context_parts = []
|
context_parts = []
|
||||||
for i, result in enumerate(results[:8], 1): # Limit to top 8 results
|
for i, result in enumerate(results[:8], 1): # Limit to top 8 results
|
||||||
file_path = result.file_path if hasattr(result, 'file_path') else 'unknown'
|
# result.file_path if hasattr(result, "file_path") else "unknown" # Unused variable removed
|
||||||
content = result.content if hasattr(result, 'content') else str(result)
|
# result.content if hasattr(result, "content") else str(result) # Unused variable removed
|
||||||
score = result.score if hasattr(result, 'score') else 0.0
|
# result.score if hasattr(result, "score") else 0.0 # Unused variable removed
|
||||||
|
|
||||||
context_parts.append(f"""
|
context_parts.append(
|
||||||
|
"""
|
||||||
Result {i} (Score: {score:.3f}):
|
Result {i} (Score: {score:.3f}):
|
||||||
File: {file_path}
|
File: {file_path}
|
||||||
Content: {content[:500]}{'...' if len(content) > 500 else ''}
|
Content: {content[:500]}{'...' if len(content) > 500 else ''}
|
||||||
""")
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
context = "\n".join(context_parts)
|
# "\n".join(context_parts) # Unused variable removed
|
||||||
|
|
||||||
# Get system context for better responses
|
# Get system context for better responses
|
||||||
system_context = get_system_context(project_path)
|
# get_system_context(project_path) # Unused variable removed
|
||||||
|
|
||||||
# Create synthesis prompt with system context
|
# Create synthesis prompt with system context
|
||||||
prompt = f"""You are a senior software engineer analyzing code search results. Your task is to synthesize the search results into a helpful, actionable summary.
|
prompt = """You are a senior software engineer analyzing code search results. Your task is to synthesize the search results into a helpful, actionable summary.
|
||||||
|
|
||||||
SYSTEM CONTEXT: {system_context}
|
SYSTEM CONTEXT: {system_context}
|
||||||
SEARCH QUERY: "{query}"
|
SEARCH QUERY: "{query}"
|
||||||
@ -646,33 +766,33 @@ Respond with ONLY the JSON, no other text."""
|
|||||||
key_points=[],
|
key_points=[],
|
||||||
code_examples=[],
|
code_examples=[],
|
||||||
suggested_actions=["Check Ollama status and try again"],
|
suggested_actions=["Check Ollama status and try again"],
|
||||||
confidence=0.0
|
confidence=0.0,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Parse JSON response
|
# Parse JSON response
|
||||||
try:
|
try:
|
||||||
# Extract JSON from response (in case there's extra text)
|
# Extract JSON from response (in case there's extra text)
|
||||||
start_idx = response.find('{')
|
start_idx = response.find("{")
|
||||||
end_idx = response.rfind('}') + 1
|
end_idx = response.rfind("}") + 1
|
||||||
if start_idx >= 0 and end_idx > start_idx:
|
if start_idx >= 0 and end_idx > start_idx:
|
||||||
json_str = response[start_idx:end_idx]
|
json_str = response[start_idx:end_idx]
|
||||||
data = json.loads(json_str)
|
data = json.loads(json_str)
|
||||||
|
|
||||||
return SynthesisResult(
|
return SynthesisResult(
|
||||||
summary=data.get('summary', 'No summary generated'),
|
summary=data.get("summary", "No summary generated"),
|
||||||
key_points=data.get('key_points', []),
|
key_points=data.get("key_points", []),
|
||||||
code_examples=data.get('code_examples', []),
|
code_examples=data.get("code_examples", []),
|
||||||
suggested_actions=data.get('suggested_actions', []),
|
suggested_actions=data.get("suggested_actions", []),
|
||||||
confidence=float(data.get('confidence', 0.5))
|
confidence=float(data.get("confidence", 0.5)),
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# Fallback: use the raw response as summary
|
# Fallback: use the raw response as summary
|
||||||
return SynthesisResult(
|
return SynthesisResult(
|
||||||
summary=response[:300] + '...' if len(response) > 300 else response,
|
summary=response[:300] + "..." if len(response) > 300 else response,
|
||||||
key_points=[],
|
key_points=[],
|
||||||
code_examples=[],
|
code_examples=[],
|
||||||
suggested_actions=[],
|
suggested_actions=[],
|
||||||
confidence=0.3
|
confidence=0.3,
|
||||||
)
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -682,7 +802,7 @@ Respond with ONLY the JSON, no other text."""
|
|||||||
key_points=[],
|
key_points=[],
|
||||||
code_examples=[],
|
code_examples=[],
|
||||||
suggested_actions=["Try the search again or check LLM output"],
|
suggested_actions=["Try the search again or check LLM output"],
|
||||||
confidence=0.0
|
confidence=0.0,
|
||||||
)
|
)
|
||||||
|
|
||||||
def format_synthesis_output(self, synthesis: SynthesisResult, query: str) -> str:
|
def format_synthesis_output(self, synthesis: SynthesisResult, query: str) -> str:
|
||||||
@ -693,7 +813,7 @@ Respond with ONLY the JSON, no other text."""
|
|||||||
output.append("=" * 50)
|
output.append("=" * 50)
|
||||||
output.append("")
|
output.append("")
|
||||||
|
|
||||||
output.append(f"📝 Summary:")
|
output.append("📝 Summary:")
|
||||||
output.append(f" {synthesis.summary}")
|
output.append(f" {synthesis.summary}")
|
||||||
output.append("")
|
output.append("")
|
||||||
|
|
||||||
@ -715,13 +835,20 @@ Respond with ONLY the JSON, no other text."""
|
|||||||
output.append(f" • {action}")
|
output.append(f" • {action}")
|
||||||
output.append("")
|
output.append("")
|
||||||
|
|
||||||
confidence_emoji = "🟢" if synthesis.confidence > 0.7 else "🟡" if synthesis.confidence > 0.4 else "🔴"
|
confidence_emoji = (
|
||||||
|
"🟢"
|
||||||
|
if synthesis.confidence > 0.7
|
||||||
|
else "🟡" if synthesis.confidence > 0.4 else "🔴"
|
||||||
|
)
|
||||||
output.append(f"{confidence_emoji} Confidence: {synthesis.confidence:.1%}")
|
output.append(f"{confidence_emoji} Confidence: {synthesis.confidence:.1%}")
|
||||||
output.append("")
|
output.append("")
|
||||||
|
|
||||||
return "\n".join(output)
|
return "\n".join(output)
|
||||||
|
|
||||||
|
|
||||||
# Quick test function
|
# Quick test function
|
||||||
|
|
||||||
|
|
||||||
def test_synthesizer():
|
def test_synthesizer():
|
||||||
"""Test the synthesizer with sample data."""
|
"""Test the synthesizer with sample data."""
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
@ -740,17 +867,24 @@ def test_synthesizer():
|
|||||||
|
|
||||||
# Mock search results
|
# Mock search results
|
||||||
results = [
|
results = [
|
||||||
MockResult("auth.py", "def authenticate_user(username, password):\n return verify_credentials(username, password)", 0.95),
|
MockResult(
|
||||||
MockResult("models.py", "class User:\n def login(self):\n return authenticate_user(self.username, self.password)", 0.87)
|
"auth.py",
|
||||||
|
"def authenticate_user(username, password):\n return verify_credentials(username, password)",
|
||||||
|
0.95,
|
||||||
|
),
|
||||||
|
MockResult(
|
||||||
|
"models.py",
|
||||||
|
"class User:\n def login(self):\n return authenticate_user(self.username, self.password)",
|
||||||
|
0.87,
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
synthesis = synthesizer.synthesize_search_results(
|
synthesis = synthesizer.synthesize_search_results(
|
||||||
"user authentication",
|
"user authentication", results, Path("/test/project")
|
||||||
results,
|
|
||||||
Path("/test/project")
|
|
||||||
)
|
)
|
||||||
|
|
||||||
print(synthesizer.format_synthesis_output(synthesis, "user authentication"))
|
print(synthesizer.format_synthesis_output(synthesis, "user authentication"))
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
test_synthesizer()
|
test_synthesizer()
|
||||||
@ -3,16 +3,16 @@ Non-invasive file watcher designed to not interfere with development workflows.
|
|||||||
Uses minimal resources and gracefully handles high-load scenarios.
|
Uses minimal resources and gracefully handles high-load scenarios.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
|
||||||
import time
|
|
||||||
import logging
|
import logging
|
||||||
import threading
|
|
||||||
import queue
|
import queue
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional, Set
|
from typing import Optional, Set
|
||||||
from datetime import datetime
|
|
||||||
|
from watchdog.events import DirModifiedEvent, FileSystemEventHandler
|
||||||
from watchdog.observers import Observer
|
from watchdog.observers import Observer
|
||||||
from watchdog.events import FileSystemEventHandler, DirModifiedEvent
|
|
||||||
|
|
||||||
from .indexer import ProjectIndexer
|
from .indexer import ProjectIndexer
|
||||||
|
|
||||||
@ -74,10 +74,12 @@ class NonInvasiveQueue:
|
|||||||
class MinimalEventHandler(FileSystemEventHandler):
|
class MinimalEventHandler(FileSystemEventHandler):
|
||||||
"""Minimal event handler that only watches for meaningful changes."""
|
"""Minimal event handler that only watches for meaningful changes."""
|
||||||
|
|
||||||
def __init__(self,
|
def __init__(
|
||||||
|
self,
|
||||||
update_queue: NonInvasiveQueue,
|
update_queue: NonInvasiveQueue,
|
||||||
include_patterns: Set[str],
|
include_patterns: Set[str],
|
||||||
exclude_patterns: Set[str]):
|
exclude_patterns: Set[str],
|
||||||
|
):
|
||||||
self.update_queue = update_queue
|
self.update_queue = update_queue
|
||||||
self.include_patterns = include_patterns
|
self.include_patterns = include_patterns
|
||||||
self.exclude_patterns = exclude_patterns
|
self.exclude_patterns = exclude_patterns
|
||||||
@ -100,11 +102,13 @@ class MinimalEventHandler(FileSystemEventHandler):
|
|||||||
|
|
||||||
# Skip temporary and system files
|
# Skip temporary and system files
|
||||||
name = path.name
|
name = path.name
|
||||||
if (name.startswith('.') or
|
if (
|
||||||
name.startswith('~') or
|
name.startswith(".")
|
||||||
name.endswith('.tmp') or
|
or name.startswith("~")
|
||||||
name.endswith('.swp') or
|
or name.endswith(".tmp")
|
||||||
name.endswith('.lock')):
|
or name.endswith(".swp")
|
||||||
|
or name.endswith(".lock")
|
||||||
|
):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Check exclude patterns first (faster)
|
# Check exclude patterns first (faster)
|
||||||
@ -124,7 +128,9 @@ class MinimalEventHandler(FileSystemEventHandler):
|
|||||||
"""Rate limit events per file."""
|
"""Rate limit events per file."""
|
||||||
current_time = time.time()
|
current_time = time.time()
|
||||||
if file_path in self.last_event_time:
|
if file_path in self.last_event_time:
|
||||||
if current_time - self.last_event_time[file_path] < 2.0: # 2 second cooldown per file
|
if (
|
||||||
|
current_time - self.last_event_time[file_path] < 2.0
|
||||||
|
): # 2 second cooldown per file
|
||||||
return False
|
return False
|
||||||
|
|
||||||
self.last_event_time[file_path] = current_time
|
self.last_event_time[file_path] = current_time
|
||||||
@ -132,16 +138,20 @@ class MinimalEventHandler(FileSystemEventHandler):
|
|||||||
|
|
||||||
def on_modified(self, event):
|
def on_modified(self, event):
|
||||||
"""Handle file modifications with minimal overhead."""
|
"""Handle file modifications with minimal overhead."""
|
||||||
if (not event.is_directory and
|
if (
|
||||||
self._should_process(event.src_path) and
|
not event.is_directory
|
||||||
self._rate_limit_event(event.src_path)):
|
and self._should_process(event.src_path)
|
||||||
|
and self._rate_limit_event(event.src_path)
|
||||||
|
):
|
||||||
self.update_queue.add(Path(event.src_path))
|
self.update_queue.add(Path(event.src_path))
|
||||||
|
|
||||||
def on_created(self, event):
|
def on_created(self, event):
|
||||||
"""Handle file creation."""
|
"""Handle file creation."""
|
||||||
if (not event.is_directory and
|
if (
|
||||||
self._should_process(event.src_path) and
|
not event.is_directory
|
||||||
self._rate_limit_event(event.src_path)):
|
and self._should_process(event.src_path)
|
||||||
|
and self._rate_limit_event(event.src_path)
|
||||||
|
):
|
||||||
self.update_queue.add(Path(event.src_path))
|
self.update_queue.add(Path(event.src_path))
|
||||||
|
|
||||||
def on_deleted(self, event):
|
def on_deleted(self, event):
|
||||||
@ -158,11 +168,13 @@ class MinimalEventHandler(FileSystemEventHandler):
|
|||||||
class NonInvasiveFileWatcher:
|
class NonInvasiveFileWatcher:
|
||||||
"""Non-invasive file watcher that prioritizes system stability."""
|
"""Non-invasive file watcher that prioritizes system stability."""
|
||||||
|
|
||||||
def __init__(self,
|
def __init__(
|
||||||
|
self,
|
||||||
project_path: Path,
|
project_path: Path,
|
||||||
indexer: Optional[ProjectIndexer] = None,
|
indexer: Optional[ProjectIndexer] = None,
|
||||||
cpu_limit: float = 0.1, # Max 10% CPU usage
|
cpu_limit: float = 0.1, # Max 10% CPU usage
|
||||||
max_memory_mb: int = 50): # Max 50MB memory
|
max_memory_mb: int = 50,
|
||||||
|
): # Max 50MB memory
|
||||||
"""
|
"""
|
||||||
Initialize non-invasive watcher.
|
Initialize non-invasive watcher.
|
||||||
|
|
||||||
@ -178,7 +190,9 @@ class NonInvasiveFileWatcher:
|
|||||||
self.max_memory_mb = max_memory_mb
|
self.max_memory_mb = max_memory_mb
|
||||||
|
|
||||||
# Initialize components with conservative settings
|
# Initialize components with conservative settings
|
||||||
self.update_queue = NonInvasiveQueue(delay=10.0, max_queue_size=50) # Very conservative
|
self.update_queue = NonInvasiveQueue(
|
||||||
|
delay=10.0, max_queue_size=50
|
||||||
|
) # Very conservative
|
||||||
self.observer = Observer()
|
self.observer = Observer()
|
||||||
self.worker_thread = None
|
self.worker_thread = None
|
||||||
self.running = False
|
self.running = False
|
||||||
@ -188,19 +202,38 @@ class NonInvasiveFileWatcher:
|
|||||||
self.exclude_patterns = set(self.indexer.exclude_patterns)
|
self.exclude_patterns = set(self.indexer.exclude_patterns)
|
||||||
|
|
||||||
# Add more aggressive exclusions
|
# Add more aggressive exclusions
|
||||||
self.exclude_patterns.update({
|
self.exclude_patterns.update(
|
||||||
'__pycache__', '.git', 'node_modules', '.venv', 'venv',
|
{
|
||||||
'dist', 'build', 'target', '.idea', '.vscode', '.pytest_cache',
|
"__pycache__",
|
||||||
'coverage', 'htmlcov', '.coverage', '.mypy_cache', '.tox',
|
".git",
|
||||||
'logs', 'log', 'tmp', 'temp', '.DS_Store'
|
"node_modules",
|
||||||
})
|
".venv",
|
||||||
|
"venv",
|
||||||
|
"dist",
|
||||||
|
"build",
|
||||||
|
"target",
|
||||||
|
".idea",
|
||||||
|
".vscode",
|
||||||
|
".pytest_cache",
|
||||||
|
"coverage",
|
||||||
|
"htmlcov",
|
||||||
|
".coverage",
|
||||||
|
".mypy_cache",
|
||||||
|
".tox",
|
||||||
|
"logs",
|
||||||
|
"log",
|
||||||
|
"tmp",
|
||||||
|
"temp",
|
||||||
|
".DS_Store",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
# Stats
|
# Stats
|
||||||
self.stats = {
|
self.stats = {
|
||||||
'files_processed': 0,
|
"files_processed": 0,
|
||||||
'files_dropped': 0,
|
"files_dropped": 0,
|
||||||
'cpu_throttle_count': 0,
|
"cpu_throttle_count": 0,
|
||||||
'started_at': None,
|
"started_at": None,
|
||||||
}
|
}
|
||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
@ -212,24 +245,16 @@ class NonInvasiveFileWatcher:
|
|||||||
|
|
||||||
# Set up minimal event handler
|
# Set up minimal event handler
|
||||||
event_handler = MinimalEventHandler(
|
event_handler = MinimalEventHandler(
|
||||||
self.update_queue,
|
self.update_queue, self.include_patterns, self.exclude_patterns
|
||||||
self.include_patterns,
|
|
||||||
self.exclude_patterns
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Schedule with recursive watching
|
# Schedule with recursive watching
|
||||||
self.observer.schedule(
|
self.observer.schedule(event_handler, str(self.project_path), recursive=True)
|
||||||
event_handler,
|
|
||||||
str(self.project_path),
|
|
||||||
recursive=True
|
|
||||||
)
|
|
||||||
|
|
||||||
# Start low-priority worker thread
|
# Start low-priority worker thread
|
||||||
self.running = True
|
self.running = True
|
||||||
self.worker_thread = threading.Thread(
|
self.worker_thread = threading.Thread(
|
||||||
target=self._process_updates_gently,
|
target=self._process_updates_gently, daemon=True, name="RAG-FileWatcher"
|
||||||
daemon=True,
|
|
||||||
name="RAG-FileWatcher"
|
|
||||||
)
|
)
|
||||||
# Set lowest priority
|
# Set lowest priority
|
||||||
self.worker_thread.start()
|
self.worker_thread.start()
|
||||||
@ -237,7 +262,7 @@ class NonInvasiveFileWatcher:
|
|||||||
# Start observer
|
# Start observer
|
||||||
self.observer.start()
|
self.observer.start()
|
||||||
|
|
||||||
self.stats['started_at'] = datetime.now()
|
self.stats["started_at"] = datetime.now()
|
||||||
logger.info("Non-invasive file watcher started")
|
logger.info("Non-invasive file watcher started")
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
@ -282,7 +307,7 @@ class NonInvasiveFileWatcher:
|
|||||||
# If we're consuming too much time, throttle aggressively
|
# If we're consuming too much time, throttle aggressively
|
||||||
work_ratio = 0.1 # Assume we use 10% of time in this check
|
work_ratio = 0.1 # Assume we use 10% of time in this check
|
||||||
if work_ratio > self.cpu_limit:
|
if work_ratio > self.cpu_limit:
|
||||||
self.stats['cpu_throttle_count'] += 1
|
self.stats["cpu_throttle_count"] += 1
|
||||||
time.sleep(2.0) # Back off significantly
|
time.sleep(2.0) # Back off significantly
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@ -294,18 +319,20 @@ class NonInvasiveFileWatcher:
|
|||||||
success = self.indexer.delete_file(file_path)
|
success = self.indexer.delete_file(file_path)
|
||||||
|
|
||||||
if success:
|
if success:
|
||||||
self.stats['files_processed'] += 1
|
self.stats["files_processed"] += 1
|
||||||
|
|
||||||
# Always yield CPU after processing
|
# Always yield CPU after processing
|
||||||
time.sleep(0.1)
|
time.sleep(0.1)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.debug(f"Non-invasive watcher: failed to process {file_path}: {e}")
|
logger.debug(
|
||||||
|
f"Non-invasive watcher: failed to process {file_path}: {e}"
|
||||||
|
)
|
||||||
# Don't let errors propagate - just continue
|
# Don't let errors propagate - just continue
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Update dropped count from queue
|
# Update dropped count from queue
|
||||||
self.stats['files_dropped'] = self.update_queue.dropped_count
|
self.stats["files_dropped"] = self.update_queue.dropped_count
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.debug(f"Non-invasive watcher error: {e}")
|
logger.debug(f"Non-invasive watcher error: {e}")
|
||||||
@ -316,12 +343,12 @@ class NonInvasiveFileWatcher:
|
|||||||
def get_statistics(self) -> dict:
|
def get_statistics(self) -> dict:
|
||||||
"""Get non-invasive watcher statistics."""
|
"""Get non-invasive watcher statistics."""
|
||||||
stats = self.stats.copy()
|
stats = self.stats.copy()
|
||||||
stats['queue_size'] = self.update_queue.queue.qsize()
|
stats["queue_size"] = self.update_queue.queue.qsize()
|
||||||
stats['running'] = self.running
|
stats["running"] = self.running
|
||||||
|
|
||||||
if stats['started_at']:
|
if stats["started_at"]:
|
||||||
uptime = datetime.now() - stats['started_at']
|
uptime = datetime.now() - stats["started_at"]
|
||||||
stats['uptime_seconds'] = uptime.total_seconds()
|
stats["uptime_seconds"] = uptime.total_seconds()
|
||||||
|
|
||||||
return stats
|
return stats
|
||||||
|
|
||||||
|
|||||||
@ -3,15 +3,14 @@ Hybrid code embedding module - Ollama primary with ML fallback.
|
|||||||
Tries Ollama first, falls back to local ML stack if needed.
|
Tries Ollama first, falls back to local ML stack if needed.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import requests
|
|
||||||
import numpy as np
|
|
||||||
from typing import List, Union, Optional, Dict, Any
|
|
||||||
import logging
|
import logging
|
||||||
from functools import lru_cache
|
|
||||||
import time
|
import time
|
||||||
import json
|
|
||||||
from concurrent.futures import ThreadPoolExecutor
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
import threading
|
from functools import lru_cache
|
||||||
|
from typing import Any, Dict, List, Optional, Union
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import requests
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -19,8 +18,9 @@ logger = logging.getLogger(__name__)
|
|||||||
FALLBACK_AVAILABLE = False
|
FALLBACK_AVAILABLE = False
|
||||||
try:
|
try:
|
||||||
import torch
|
import torch
|
||||||
from transformers import AutoTokenizer, AutoModel
|
|
||||||
from sentence_transformers import SentenceTransformer
|
from sentence_transformers import SentenceTransformer
|
||||||
|
from transformers import AutoModel, AutoTokenizer
|
||||||
|
|
||||||
FALLBACK_AVAILABLE = True
|
FALLBACK_AVAILABLE = True
|
||||||
logger.debug("ML fallback dependencies available")
|
logger.debug("ML fallback dependencies available")
|
||||||
except ImportError:
|
except ImportError:
|
||||||
@ -30,8 +30,12 @@ except ImportError:
|
|||||||
class OllamaEmbedder:
|
class OllamaEmbedder:
|
||||||
"""Hybrid embeddings: Ollama primary with ML fallback."""
|
"""Hybrid embeddings: Ollama primary with ML fallback."""
|
||||||
|
|
||||||
def __init__(self, model_name: str = "nomic-embed-text:latest", base_url: str = "http://localhost:11434",
|
def __init__(
|
||||||
enable_fallback: bool = True):
|
self,
|
||||||
|
model_name: str = "nomic-embed-text:latest",
|
||||||
|
base_url: str = "http://localhost:11434",
|
||||||
|
enable_fallback: bool = True,
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
Initialize the hybrid embedder.
|
Initialize the hybrid embedder.
|
||||||
|
|
||||||
@ -70,7 +74,9 @@ class OllamaEmbedder:
|
|||||||
try:
|
try:
|
||||||
self._initialize_fallback_embedder()
|
self._initialize_fallback_embedder()
|
||||||
self.mode = "fallback"
|
self.mode = "fallback"
|
||||||
logger.info(f"✅ ML fallback active: {self.fallback_embedder.model_type if hasattr(self.fallback_embedder, 'model_type') else 'transformer'}")
|
logger.info(
|
||||||
|
f"✅ ML fallback active: {self.fallback_embedder.model_type if hasattr(self.fallback_embedder, 'model_type') else 'transformer'}"
|
||||||
|
)
|
||||||
except Exception as fallback_error:
|
except Exception as fallback_error:
|
||||||
logger.warning(f"ML fallback failed: {fallback_error}")
|
logger.warning(f"ML fallback failed: {fallback_error}")
|
||||||
self.mode = "hash"
|
self.mode = "hash"
|
||||||
@ -101,8 +107,8 @@ class OllamaEmbedder:
|
|||||||
raise ConnectionError("Ollama service timeout")
|
raise ConnectionError("Ollama service timeout")
|
||||||
|
|
||||||
# Check if our model is available
|
# Check if our model is available
|
||||||
models = response.json().get('models', [])
|
models = response.json().get("models", [])
|
||||||
model_names = [model['name'] for model in models]
|
model_names = [model["name"] for model in models]
|
||||||
|
|
||||||
if self.model_name not in model_names:
|
if self.model_name not in model_names:
|
||||||
print(f"📦 Model '{self.model_name}' Not Found")
|
print(f"📦 Model '{self.model_name}' Not Found")
|
||||||
@ -121,7 +127,11 @@ class OllamaEmbedder:
|
|||||||
|
|
||||||
# Try lightweight models first for better compatibility
|
# Try lightweight models first for better compatibility
|
||||||
fallback_models = [
|
fallback_models = [
|
||||||
("sentence-transformers/all-MiniLM-L6-v2", 384, self._init_sentence_transformer),
|
(
|
||||||
|
"sentence-transformers/all-MiniLM-L6-v2",
|
||||||
|
384,
|
||||||
|
self._init_sentence_transformer,
|
||||||
|
),
|
||||||
("microsoft/codebert-base", 768, self._init_transformer_model),
|
("microsoft/codebert-base", 768, self._init_transformer_model),
|
||||||
("microsoft/unixcoder-base", 768, self._init_transformer_model),
|
("microsoft/unixcoder-base", 768, self._init_transformer_model),
|
||||||
]
|
]
|
||||||
@ -141,22 +151,24 @@ class OllamaEmbedder:
|
|||||||
def _init_sentence_transformer(self, model_name: str):
|
def _init_sentence_transformer(self, model_name: str):
|
||||||
"""Initialize sentence-transformers model."""
|
"""Initialize sentence-transformers model."""
|
||||||
self.fallback_embedder = SentenceTransformer(model_name)
|
self.fallback_embedder = SentenceTransformer(model_name)
|
||||||
self.fallback_embedder.model_type = 'sentence_transformer'
|
self.fallback_embedder.model_type = "sentence_transformer"
|
||||||
|
|
||||||
def _init_transformer_model(self, model_name: str):
|
def _init_transformer_model(self, model_name: str):
|
||||||
"""Initialize transformer model."""
|
"""Initialize transformer model."""
|
||||||
device = 'cuda' if torch.cuda.is_available() else 'cpu'
|
device = "cuda" if torch.cuda.is_available() else "cpu"
|
||||||
tokenizer = AutoTokenizer.from_pretrained(model_name)
|
tokenizer = AutoTokenizer.from_pretrained(model_name)
|
||||||
model = AutoModel.from_pretrained(model_name).to(device)
|
model = AutoModel.from_pretrained(model_name).to(device)
|
||||||
model.eval()
|
model.eval()
|
||||||
|
|
||||||
# Create a simple wrapper
|
# Create a simple wrapper
|
||||||
|
|
||||||
class TransformerWrapper:
|
class TransformerWrapper:
|
||||||
|
|
||||||
def __init__(self, model, tokenizer, device):
|
def __init__(self, model, tokenizer, device):
|
||||||
self.model = model
|
self.model = model
|
||||||
self.tokenizer = tokenizer
|
self.tokenizer = tokenizer
|
||||||
self.device = device
|
self.device = device
|
||||||
self.model_type = 'transformer'
|
self.model_type = "transformer"
|
||||||
|
|
||||||
self.fallback_embedder = TransformerWrapper(model, tokenizer, device)
|
self.fallback_embedder = TransformerWrapper(model, tokenizer, device)
|
||||||
|
|
||||||
@ -167,7 +179,7 @@ class OllamaEmbedder:
|
|||||||
response = requests.post(
|
response = requests.post(
|
||||||
f"{self.base_url}/api/pull",
|
f"{self.base_url}/api/pull",
|
||||||
json={"name": self.model_name},
|
json={"name": self.model_name},
|
||||||
timeout=300 # 5 minutes for model download
|
timeout=300, # 5 minutes for model download
|
||||||
)
|
)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
logger.info(f"Successfully pulled {self.model_name}")
|
logger.info(f"Successfully pulled {self.model_name}")
|
||||||
@ -189,16 +201,13 @@ class OllamaEmbedder:
|
|||||||
try:
|
try:
|
||||||
response = requests.post(
|
response = requests.post(
|
||||||
f"{self.base_url}/api/embeddings",
|
f"{self.base_url}/api/embeddings",
|
||||||
json={
|
json={"model": self.model_name, "prompt": text},
|
||||||
"model": self.model_name,
|
timeout=30,
|
||||||
"prompt": text
|
|
||||||
},
|
|
||||||
timeout=30
|
|
||||||
)
|
)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
|
|
||||||
result = response.json()
|
result = response.json()
|
||||||
embedding = result.get('embedding', [])
|
embedding = result.get("embedding", [])
|
||||||
|
|
||||||
if not embedding:
|
if not embedding:
|
||||||
raise ValueError("No embedding returned from Ollama")
|
raise ValueError("No embedding returned from Ollama")
|
||||||
@ -220,33 +229,37 @@ class OllamaEmbedder:
|
|||||||
def _get_fallback_embedding(self, text: str) -> np.ndarray:
|
def _get_fallback_embedding(self, text: str) -> np.ndarray:
|
||||||
"""Get embedding from ML fallback."""
|
"""Get embedding from ML fallback."""
|
||||||
try:
|
try:
|
||||||
if self.fallback_embedder.model_type == 'sentence_transformer':
|
if self.fallback_embedder.model_type == "sentence_transformer":
|
||||||
embedding = self.fallback_embedder.encode([text], convert_to_numpy=True)[0]
|
embedding = self.fallback_embedder.encode([text], convert_to_numpy=True)[0]
|
||||||
return embedding.astype(np.float32)
|
return embedding.astype(np.float32)
|
||||||
|
|
||||||
elif self.fallback_embedder.model_type == 'transformer':
|
elif self.fallback_embedder.model_type == "transformer":
|
||||||
# Tokenize and generate embedding
|
# Tokenize and generate embedding
|
||||||
inputs = self.fallback_embedder.tokenizer(
|
inputs = self.fallback_embedder.tokenizer(
|
||||||
text,
|
text,
|
||||||
padding=True,
|
padding=True,
|
||||||
truncation=True,
|
truncation=True,
|
||||||
max_length=512,
|
max_length=512,
|
||||||
return_tensors="pt"
|
return_tensors="pt",
|
||||||
).to(self.fallback_embedder.device)
|
).to(self.fallback_embedder.device)
|
||||||
|
|
||||||
with torch.no_grad():
|
with torch.no_grad():
|
||||||
outputs = self.fallback_embedder.model(**inputs)
|
outputs = self.fallback_embedder.model(**inputs)
|
||||||
|
|
||||||
# Use pooler output if available, otherwise mean pooling
|
# Use pooler output if available, otherwise mean pooling
|
||||||
if hasattr(outputs, 'pooler_output') and outputs.pooler_output is not None:
|
if hasattr(outputs, "pooler_output") and outputs.pooler_output is not None:
|
||||||
embedding = outputs.pooler_output[0]
|
embedding = outputs.pooler_output[0]
|
||||||
else:
|
else:
|
||||||
# Mean pooling over sequence length
|
# Mean pooling over sequence length
|
||||||
attention_mask = inputs['attention_mask']
|
attention_mask = inputs["attention_mask"]
|
||||||
token_embeddings = outputs.last_hidden_state[0]
|
token_embeddings = outputs.last_hidden_state[0]
|
||||||
|
|
||||||
# Mask and average
|
# Mask and average
|
||||||
input_mask_expanded = attention_mask.unsqueeze(-1).expand(token_embeddings.size()).float()
|
input_mask_expanded = (
|
||||||
|
attention_mask.unsqueeze(-1)
|
||||||
|
.expand(token_embeddings.size())
|
||||||
|
.float()
|
||||||
|
)
|
||||||
sum_embeddings = torch.sum(token_embeddings * input_mask_expanded, 0)
|
sum_embeddings = torch.sum(token_embeddings * input_mask_expanded, 0)
|
||||||
sum_mask = torch.clamp(input_mask_expanded.sum(0), min=1e-9)
|
sum_mask = torch.clamp(input_mask_expanded.sum(0), min=1e-9)
|
||||||
embedding = sum_embeddings / sum_mask
|
embedding = sum_embeddings / sum_mask
|
||||||
@ -254,7 +267,9 @@ class OllamaEmbedder:
|
|||||||
return embedding.cpu().numpy().astype(np.float32)
|
return embedding.cpu().numpy().astype(np.float32)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"Unknown fallback model type: {self.fallback_embedder.model_type}")
|
raise ValueError(
|
||||||
|
f"Unknown fallback model type: {self.fallback_embedder.model_type}"
|
||||||
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Fallback embedding failed: {e}")
|
logger.error(f"Fallback embedding failed: {e}")
|
||||||
@ -265,7 +280,7 @@ class OllamaEmbedder:
|
|||||||
import hashlib
|
import hashlib
|
||||||
|
|
||||||
# Create deterministic hash
|
# Create deterministic hash
|
||||||
hash_obj = hashlib.sha256(text.encode('utf-8'))
|
hash_obj = hashlib.sha256(text.encode("utf-8"))
|
||||||
hash_bytes = hash_obj.digest()
|
hash_bytes = hash_obj.digest()
|
||||||
|
|
||||||
# Convert to numbers and normalize
|
# Convert to numbers and normalize
|
||||||
@ -276,7 +291,7 @@ class OllamaEmbedder:
|
|||||||
hash_nums = np.concatenate([hash_nums, hash_nums])
|
hash_nums = np.concatenate([hash_nums, hash_nums])
|
||||||
|
|
||||||
# Take exactly the dimension we need
|
# Take exactly the dimension we need
|
||||||
embedding = hash_nums[:self.embedding_dim].astype(np.float32)
|
embedding = hash_nums[: self.embedding_dim].astype(np.float32)
|
||||||
|
|
||||||
# Normalize to [-1, 1] range
|
# Normalize to [-1, 1] range
|
||||||
embedding = (embedding / 127.5) - 1.0
|
embedding = (embedding / 127.5) - 1.0
|
||||||
@ -325,7 +340,7 @@ class OllamaEmbedder:
|
|||||||
code = code.strip()
|
code = code.strip()
|
||||||
|
|
||||||
# Normalize whitespace but preserve structure
|
# Normalize whitespace but preserve structure
|
||||||
lines = code.split('\n')
|
lines = code.split("\n")
|
||||||
processed_lines = []
|
processed_lines = []
|
||||||
|
|
||||||
for line in lines:
|
for line in lines:
|
||||||
@ -335,7 +350,7 @@ class OllamaEmbedder:
|
|||||||
if line:
|
if line:
|
||||||
processed_lines.append(line)
|
processed_lines.append(line)
|
||||||
|
|
||||||
cleaned_code = '\n'.join(processed_lines)
|
cleaned_code = "\n".join(processed_lines)
|
||||||
|
|
||||||
# Add language context for better embeddings
|
# Add language context for better embeddings
|
||||||
if language and cleaned_code:
|
if language and cleaned_code:
|
||||||
@ -380,33 +395,36 @@ class OllamaEmbedder:
|
|||||||
"""Sequential processing for small batches."""
|
"""Sequential processing for small batches."""
|
||||||
results = []
|
results = []
|
||||||
for file_dict in file_contents:
|
for file_dict in file_contents:
|
||||||
content = file_dict['content']
|
content = file_dict["content"]
|
||||||
language = file_dict.get('language', 'python')
|
language = file_dict.get("language", "python")
|
||||||
embedding = self.embed_code(content, language)
|
embedding = self.embed_code(content, language)
|
||||||
|
|
||||||
result = file_dict.copy()
|
result = file_dict.copy()
|
||||||
result['embedding'] = embedding
|
result["embedding"] = embedding
|
||||||
results.append(result)
|
results.append(result)
|
||||||
|
|
||||||
return results
|
return results
|
||||||
|
|
||||||
def _batch_embed_concurrent(self, file_contents: List[dict], max_workers: int) -> List[dict]:
|
def _batch_embed_concurrent(
|
||||||
|
self, file_contents: List[dict], max_workers: int
|
||||||
|
) -> List[dict]:
|
||||||
"""Concurrent processing for larger batches."""
|
"""Concurrent processing for larger batches."""
|
||||||
|
|
||||||
def embed_single(item_with_index):
|
def embed_single(item_with_index):
|
||||||
index, file_dict = item_with_index
|
index, file_dict = item_with_index
|
||||||
content = file_dict['content']
|
content = file_dict["content"]
|
||||||
language = file_dict.get('language', 'python')
|
language = file_dict.get("language", "python")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
embedding = self.embed_code(content, language)
|
embedding = self.embed_code(content, language)
|
||||||
result = file_dict.copy()
|
result = file_dict.copy()
|
||||||
result['embedding'] = embedding
|
result["embedding"] = embedding
|
||||||
return index, result
|
return index, result
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to embed content at index {index}: {e}")
|
logger.error(f"Failed to embed content at index {index}: {e}")
|
||||||
# Return with hash fallback
|
# Return with hash fallback
|
||||||
result = file_dict.copy()
|
result = file_dict.copy()
|
||||||
result['embedding'] = self._hash_embedding(content)
|
result["embedding"] = self._hash_embedding(content)
|
||||||
return index, result
|
return index, result
|
||||||
|
|
||||||
# Create indexed items to preserve order
|
# Create indexed items to preserve order
|
||||||
@ -420,7 +438,9 @@ class OllamaEmbedder:
|
|||||||
indexed_results.sort(key=lambda x: x[0])
|
indexed_results.sort(key=lambda x: x[0])
|
||||||
return [result for _, result in indexed_results]
|
return [result for _, result in indexed_results]
|
||||||
|
|
||||||
def _batch_embed_chunked(self, file_contents: List[dict], max_workers: int, chunk_size: int = 200) -> List[dict]:
|
def _batch_embed_chunked(
|
||||||
|
self, file_contents: List[dict], max_workers: int, chunk_size: int = 200
|
||||||
|
) -> List[dict]:
|
||||||
"""
|
"""
|
||||||
Process very large batches in smaller chunks to prevent memory issues.
|
Process very large batches in smaller chunks to prevent memory issues.
|
||||||
This is important for beginners who might try to index huge projects.
|
This is important for beginners who might try to index huge projects.
|
||||||
@ -430,13 +450,15 @@ class OllamaEmbedder:
|
|||||||
|
|
||||||
# Process in chunks
|
# Process in chunks
|
||||||
for i in range(0, len(file_contents), chunk_size):
|
for i in range(0, len(file_contents), chunk_size):
|
||||||
chunk = file_contents[i:i + chunk_size]
|
chunk = file_contents[i : i + chunk_size]
|
||||||
|
|
||||||
# Log progress for large operations
|
# Log progress for large operations
|
||||||
if total_chunks > chunk_size:
|
if total_chunks > chunk_size:
|
||||||
chunk_num = i // chunk_size + 1
|
chunk_num = i // chunk_size + 1
|
||||||
total_chunk_count = (total_chunks + chunk_size - 1) // chunk_size
|
total_chunk_count = (total_chunks + chunk_size - 1) // chunk_size
|
||||||
logger.info(f"Processing chunk {chunk_num}/{total_chunk_count} ({len(chunk)} files)")
|
logger.info(
|
||||||
|
f"Processing chunk {chunk_num}/{total_chunk_count} ({len(chunk)} files)"
|
||||||
|
)
|
||||||
|
|
||||||
# Process this chunk using concurrent method
|
# Process this chunk using concurrent method
|
||||||
chunk_results = self._batch_embed_concurrent(chunk, max_workers)
|
chunk_results = self._batch_embed_concurrent(chunk, max_workers)
|
||||||
@ -444,7 +466,7 @@ class OllamaEmbedder:
|
|||||||
|
|
||||||
# Brief pause between chunks to prevent overwhelming the system
|
# Brief pause between chunks to prevent overwhelming the system
|
||||||
if i + chunk_size < len(file_contents):
|
if i + chunk_size < len(file_contents):
|
||||||
import time
|
|
||||||
time.sleep(0.1) # 100ms pause between chunks
|
time.sleep(0.1) # 100ms pause between chunks
|
||||||
|
|
||||||
return results
|
return results
|
||||||
@ -463,10 +485,14 @@ class OllamaEmbedder:
|
|||||||
"mode": self.mode,
|
"mode": self.mode,
|
||||||
"ollama_available": self.ollama_available,
|
"ollama_available": self.ollama_available,
|
||||||
"fallback_available": FALLBACK_AVAILABLE and self.enable_fallback,
|
"fallback_available": FALLBACK_AVAILABLE and self.enable_fallback,
|
||||||
"fallback_model": getattr(self.fallback_embedder, 'model_type', None) if self.fallback_embedder else None,
|
"fallback_model": (
|
||||||
|
getattr(self.fallback_embedder, "model_type", None)
|
||||||
|
if self.fallback_embedder
|
||||||
|
else None
|
||||||
|
),
|
||||||
"embedding_dim": self.embedding_dim,
|
"embedding_dim": self.embedding_dim,
|
||||||
"ollama_model": self.model_name if self.mode == "ollama" else None,
|
"ollama_model": self.model_name if self.mode == "ollama" else None,
|
||||||
"ollama_url": self.base_url if self.mode == "ollama" else None
|
"ollama_url": self.base_url if self.mode == "ollama" else None,
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_embedding_info(self) -> Dict[str, str]:
|
def get_embedding_info(self) -> Dict[str, str]:
|
||||||
@ -474,25 +500,16 @@ class OllamaEmbedder:
|
|||||||
status = self.get_status()
|
status = self.get_status()
|
||||||
mode = status.get("mode", "unknown")
|
mode = status.get("mode", "unknown")
|
||||||
if mode == "ollama":
|
if mode == "ollama":
|
||||||
return {
|
return {"method": f"Ollama ({status['ollama_model']})", "status": "working"}
|
||||||
"method": f"Ollama ({status['ollama_model']})",
|
|
||||||
"status": "working"
|
|
||||||
}
|
|
||||||
# Treat legacy/alternate naming uniformly
|
# Treat legacy/alternate naming uniformly
|
||||||
if mode in ("fallback", "ml"):
|
if mode in ("fallback", "ml"):
|
||||||
return {
|
return {
|
||||||
"method": f"ML Fallback ({status['fallback_model']})",
|
"method": f"ML Fallback ({status['fallback_model']})",
|
||||||
"status": "working"
|
"status": "working",
|
||||||
}
|
}
|
||||||
if mode == "hash":
|
if mode == "hash":
|
||||||
return {
|
return {"method": "Hash-based (basic similarity)", "status": "working"}
|
||||||
"method": "Hash-based (basic similarity)",
|
return {"method": "Unknown", "status": "error"}
|
||||||
"status": "working"
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
"method": "Unknown",
|
|
||||||
"status": "error"
|
|
||||||
}
|
|
||||||
|
|
||||||
def warmup(self):
|
def warmup(self):
|
||||||
"""Warm up the embedding system with a dummy request."""
|
"""Warm up the embedding system with a dummy request."""
|
||||||
@ -503,7 +520,11 @@ class OllamaEmbedder:
|
|||||||
|
|
||||||
|
|
||||||
# Convenience function for quick embedding
|
# Convenience function for quick embedding
|
||||||
def embed_code(code: Union[str, List[str]], model_name: str = "nomic-embed-text:latest") -> np.ndarray:
|
|
||||||
|
|
||||||
|
def embed_code(
|
||||||
|
code: Union[str, List[str]], model_name: str = "nomic-embed-text:latest"
|
||||||
|
) -> np.ndarray:
|
||||||
"""
|
"""
|
||||||
Quick function to embed code without managing embedder instance.
|
Quick function to embed code without managing embedder instance.
|
||||||
|
|
||||||
|
|||||||
@ -4,10 +4,9 @@ Handles forward/backward slashes on any file system.
|
|||||||
Robust cross-platform path handling.
|
Robust cross-platform path handling.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Union, List
|
from typing import List, Union
|
||||||
|
|
||||||
|
|
||||||
def normalize_path(path: Union[str, Path]) -> str:
|
def normalize_path(path: Union[str, Path]) -> str:
|
||||||
@ -25,10 +24,10 @@ def normalize_path(path: Union[str, Path]) -> str:
|
|||||||
path_obj = Path(path)
|
path_obj = Path(path)
|
||||||
|
|
||||||
# Convert to string and replace backslashes
|
# Convert to string and replace backslashes
|
||||||
path_str = str(path_obj).replace('\\', '/')
|
path_str = str(path_obj).replace("\\", "/")
|
||||||
|
|
||||||
# Handle UNC paths on Windows
|
# Handle UNC paths on Windows
|
||||||
if sys.platform == 'win32' and path_str.startswith('//'):
|
if sys.platform == "win32" and path_str.startswith("//"):
|
||||||
# Keep UNC paths as they are
|
# Keep UNC paths as they are
|
||||||
return path_str
|
return path_str
|
||||||
|
|
||||||
@ -120,7 +119,7 @@ def ensure_forward_slashes(path_str: str) -> str:
|
|||||||
Returns:
|
Returns:
|
||||||
Path with forward slashes
|
Path with forward slashes
|
||||||
"""
|
"""
|
||||||
return path_str.replace('\\', '/')
|
return path_str.replace("\\", "/")
|
||||||
|
|
||||||
|
|
||||||
def ensure_native_slashes(path_str: str) -> str:
|
def ensure_native_slashes(path_str: str) -> str:
|
||||||
@ -137,6 +136,8 @@ def ensure_native_slashes(path_str: str) -> str:
|
|||||||
|
|
||||||
|
|
||||||
# Convenience functions for common operations
|
# Convenience functions for common operations
|
||||||
|
|
||||||
|
|
||||||
def storage_path(path: Union[str, Path]) -> str:
|
def storage_path(path: Union[str, Path]) -> str:
|
||||||
"""Convert path to storage format (forward slashes)."""
|
"""Convert path to storage format (forward slashes)."""
|
||||||
return normalize_path(path)
|
return normalize_path(path)
|
||||||
|
|||||||
@ -3,12 +3,13 @@ Performance monitoring for RAG system.
|
|||||||
Track loading times, query times, and resource usage.
|
Track loading times, query times, and resource usage.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import time
|
|
||||||
import psutil
|
|
||||||
import os
|
|
||||||
from contextlib import contextmanager
|
|
||||||
from typing import Dict, Any, Optional
|
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
from contextlib import contextmanager
|
||||||
|
from typing import Any, Dict, Optional
|
||||||
|
|
||||||
|
import psutil
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -39,9 +40,9 @@ class PerformanceMonitor:
|
|||||||
|
|
||||||
# Store metrics
|
# Store metrics
|
||||||
self.metrics[operation] = {
|
self.metrics[operation] = {
|
||||||
'duration_seconds': duration,
|
"duration_seconds": duration,
|
||||||
'memory_delta_mb': memory_delta,
|
"memory_delta_mb": memory_delta,
|
||||||
'final_memory_mb': end_memory,
|
"final_memory_mb": end_memory,
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
@ -51,19 +52,19 @@ class PerformanceMonitor:
|
|||||||
|
|
||||||
def get_summary(self) -> Dict[str, Any]:
|
def get_summary(self) -> Dict[str, Any]:
|
||||||
"""Get performance summary."""
|
"""Get performance summary."""
|
||||||
total_time = sum(m['duration_seconds'] for m in self.metrics.values())
|
total_time = sum(m["duration_seconds"] for m in self.metrics.values())
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'total_time_seconds': total_time,
|
"total_time_seconds": total_time,
|
||||||
'operations': self.metrics,
|
"operations": self.metrics,
|
||||||
'current_memory_mb': self.process.memory_info().rss / 1024 / 1024,
|
"current_memory_mb": self.process.memory_info().rss / 1024 / 1024,
|
||||||
}
|
}
|
||||||
|
|
||||||
def print_summary(self):
|
def print_summary(self):
|
||||||
"""Print a formatted summary."""
|
"""Print a formatted summary."""
|
||||||
print("\n" + "="*50)
|
print("\n" + "=" * 50)
|
||||||
print("PERFORMANCE SUMMARY")
|
print("PERFORMANCE SUMMARY")
|
||||||
print("="*50)
|
print("=" * 50)
|
||||||
|
|
||||||
for op, metrics in self.metrics.items():
|
for op, metrics in self.metrics.items():
|
||||||
print(f"\n{op}:")
|
print(f"\n{op}:")
|
||||||
@ -73,12 +74,13 @@ class PerformanceMonitor:
|
|||||||
summary = self.get_summary()
|
summary = self.get_summary()
|
||||||
print(f"\nTotal Time: {summary['total_time_seconds']:.2f}s")
|
print(f"\nTotal Time: {summary['total_time_seconds']:.2f}s")
|
||||||
print(f"Current Memory: {summary['current_memory_mb']:.1f}MB")
|
print(f"Current Memory: {summary['current_memory_mb']:.1f}MB")
|
||||||
print("="*50)
|
print("=" * 50)
|
||||||
|
|
||||||
|
|
||||||
# Global instance for easy access
|
# Global instance for easy access
|
||||||
_monitor = None
|
_monitor = None
|
||||||
|
|
||||||
|
|
||||||
def get_monitor() -> PerformanceMonitor:
|
def get_monitor() -> PerformanceMonitor:
|
||||||
"""Get or create global monitor instance."""
|
"""Get or create global monitor instance."""
|
||||||
global _monitor
|
global _monitor
|
||||||
|
|||||||
@ -33,12 +33,15 @@ disable in CLI for maximum speed.
|
|||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
import threading
|
import threading
|
||||||
from typing import List, Optional
|
from typing import Optional
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
from .config import RAGConfig
|
from .config import RAGConfig
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class QueryExpander:
|
class QueryExpander:
|
||||||
"""Expands search queries using LLM to improve search recall."""
|
"""Expands search queries using LLM to improve search recall."""
|
||||||
|
|
||||||
@ -107,7 +110,7 @@ class QueryExpander:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
# Create expansion prompt
|
# Create expansion prompt
|
||||||
prompt = f"""You are a search query expert. Expand the following search query with {self.max_terms} additional related terms that would help find relevant content.
|
prompt = """You are a search query expert. Expand the following search query with {self.max_terms} additional related terms that would help find relevant content.
|
||||||
|
|
||||||
Original query: "{query}"
|
Original query: "{query}"
|
||||||
|
|
||||||
@ -134,18 +137,18 @@ Expanded query:"""
|
|||||||
"options": {
|
"options": {
|
||||||
"temperature": 0.1, # Very low temperature for consistent expansions
|
"temperature": 0.1, # Very low temperature for consistent expansions
|
||||||
"top_p": 0.8,
|
"top_p": 0.8,
|
||||||
"max_tokens": 100 # Keep it short
|
"max_tokens": 100, # Keep it short
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
response = requests.post(
|
response = requests.post(
|
||||||
f"{self.ollama_url}/api/generate",
|
f"{self.ollama_url}/api/generate",
|
||||||
json=payload,
|
json=payload,
|
||||||
timeout=10 # Quick timeout for low latency
|
timeout=10, # Quick timeout for low latency
|
||||||
)
|
)
|
||||||
|
|
||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
result = response.json().get('response', '').strip()
|
result = response.json().get("response", "").strip()
|
||||||
|
|
||||||
# Clean up the response - extract just the expanded query
|
# Clean up the response - extract just the expanded query
|
||||||
expanded = self._clean_expansion(result, query)
|
expanded = self._clean_expansion(result, query)
|
||||||
@ -166,12 +169,16 @@ Expanded query:"""
|
|||||||
response = requests.get(f"{self.ollama_url}/api/tags", timeout=5)
|
response = requests.get(f"{self.ollama_url}/api/tags", timeout=5)
|
||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
data = response.json()
|
data = response.json()
|
||||||
available = [model['name'] for model in data.get('models', [])]
|
available = [model["name"] for model in data.get("models", [])]
|
||||||
|
|
||||||
# Use same model rankings as main synthesizer for consistency
|
# Use same model rankings as main synthesizer for consistency
|
||||||
expansion_preferences = [
|
expansion_preferences = [
|
||||||
"qwen3:1.7b", "qwen3:0.6b", "qwen3:4b", "qwen2.5:3b",
|
"qwen3:1.7b",
|
||||||
"qwen2.5:1.5b", "qwen2.5-coder:1.5b"
|
"qwen3:0.6b",
|
||||||
|
"qwen3:4b",
|
||||||
|
"qwen2.5:3b",
|
||||||
|
"qwen2.5:1.5b",
|
||||||
|
"qwen2.5-coder:1.5b",
|
||||||
]
|
]
|
||||||
|
|
||||||
for preferred in expansion_preferences:
|
for preferred in expansion_preferences:
|
||||||
@ -200,11 +207,11 @@ Expanded query:"""
|
|||||||
clean_response = clean_response[1:-1]
|
clean_response = clean_response[1:-1]
|
||||||
|
|
||||||
# Take only the first line if multiline
|
# Take only the first line if multiline
|
||||||
clean_response = clean_response.split('\n')[0].strip()
|
clean_response = clean_response.split("\n")[0].strip()
|
||||||
|
|
||||||
# Remove excessive punctuation and normalize spaces
|
# Remove excessive punctuation and normalize spaces
|
||||||
clean_response = re.sub(r'[^\w\s-]', ' ', clean_response)
|
clean_response = re.sub(r"[^\w\s-]", " ", clean_response)
|
||||||
clean_response = re.sub(r'\s+', ' ', clean_response).strip()
|
clean_response = re.sub(r"\s+", " ", clean_response).strip()
|
||||||
|
|
||||||
# Ensure it starts with the original query
|
# Ensure it starts with the original query
|
||||||
if not clean_response.lower().startswith(original_query.lower()):
|
if not clean_response.lower().startswith(original_query.lower()):
|
||||||
@ -213,8 +220,8 @@ Expanded query:"""
|
|||||||
# Limit the total length to avoid very long queries
|
# Limit the total length to avoid very long queries
|
||||||
words = clean_response.split()
|
words = clean_response.split()
|
||||||
if len(words) > len(original_query.split()) + self.max_terms:
|
if len(words) > len(original_query.split()) + self.max_terms:
|
||||||
words = words[:len(original_query.split()) + self.max_terms]
|
words = words[: len(original_query.split()) + self.max_terms]
|
||||||
clean_response = ' '.join(words)
|
clean_response = " ".join(words)
|
||||||
|
|
||||||
return clean_response
|
return clean_response
|
||||||
|
|
||||||
@ -242,10 +249,13 @@ Expanded query:"""
|
|||||||
try:
|
try:
|
||||||
response = requests.get(f"{self.ollama_url}/api/tags", timeout=5)
|
response = requests.get(f"{self.ollama_url}/api/tags", timeout=5)
|
||||||
return response.status_code == 200
|
return response.status_code == 200
|
||||||
except:
|
except (ConnectionError, TimeoutError, requests.RequestException):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
# Quick test function
|
# Quick test function
|
||||||
|
|
||||||
|
|
||||||
def test_expansion():
|
def test_expansion():
|
||||||
"""Test the query expander."""
|
"""Test the query expander."""
|
||||||
from .config import RAGConfig
|
from .config import RAGConfig
|
||||||
@ -264,7 +274,7 @@ def test_expansion():
|
|||||||
"authentication",
|
"authentication",
|
||||||
"error handling",
|
"error handling",
|
||||||
"database query",
|
"database query",
|
||||||
"user interface"
|
"user interface",
|
||||||
]
|
]
|
||||||
|
|
||||||
print("🔍 Testing Query Expansion:")
|
print("🔍 Testing Query Expansion:")
|
||||||
@ -272,5 +282,6 @@ def test_expansion():
|
|||||||
expanded = expander.expand_query(query)
|
expanded = expander.expand_query(query)
|
||||||
print(f" '{query}' → '{expanded}'")
|
print(f" '{query}' → '{expanded}'")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
test_expansion()
|
test_expansion()
|
||||||
@ -4,29 +4,33 @@ Optimized for code search with relevance scoring.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
from collections import defaultdict
|
||||||
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import List, Dict, Any, Optional, Tuple
|
from typing import Any, Dict, List, Optional, Tuple
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
from rich.console import Console
|
|
||||||
from rich.table import Table
|
|
||||||
from rich.syntax import Syntax
|
|
||||||
from rank_bm25 import BM25Okapi
|
from rank_bm25 import BM25Okapi
|
||||||
from collections import defaultdict
|
from rich.console import Console
|
||||||
|
from rich.syntax import Syntax
|
||||||
|
from rich.table import Table
|
||||||
|
|
||||||
# Optional LanceDB import
|
# Optional LanceDB import
|
||||||
try:
|
try:
|
||||||
import lancedb
|
import lancedb
|
||||||
|
|
||||||
LANCEDB_AVAILABLE = True
|
LANCEDB_AVAILABLE = True
|
||||||
except ImportError:
|
except ImportError:
|
||||||
lancedb = None
|
lancedb = None
|
||||||
LANCEDB_AVAILABLE = False
|
LANCEDB_AVAILABLE = False
|
||||||
|
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
from .config import ConfigManager
|
||||||
from .ollama_embeddings import OllamaEmbedder as CodeEmbedder
|
from .ollama_embeddings import OllamaEmbedder as CodeEmbedder
|
||||||
from .path_handler import display_path
|
from .path_handler import display_path
|
||||||
from .query_expander import QueryExpander
|
from .query_expander import QueryExpander
|
||||||
from .config import ConfigManager
|
|
||||||
from datetime import datetime, timedelta
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
console = Console()
|
console = Console()
|
||||||
@ -35,7 +39,8 @@ console = Console()
|
|||||||
class SearchResult:
|
class SearchResult:
|
||||||
"""Represents a single search result."""
|
"""Represents a single search result."""
|
||||||
|
|
||||||
def __init__(self,
|
def __init__(
|
||||||
|
self,
|
||||||
file_path: str,
|
file_path: str,
|
||||||
content: str,
|
content: str,
|
||||||
score: float,
|
score: float,
|
||||||
@ -46,7 +51,8 @@ class SearchResult:
|
|||||||
language: str,
|
language: str,
|
||||||
context_before: Optional[str] = None,
|
context_before: Optional[str] = None,
|
||||||
context_after: Optional[str] = None,
|
context_after: Optional[str] = None,
|
||||||
parent_chunk: Optional['SearchResult'] = None):
|
parent_chunk: Optional["SearchResult"] = None,
|
||||||
|
):
|
||||||
self.file_path = file_path
|
self.file_path = file_path
|
||||||
self.content = content
|
self.content = content
|
||||||
self.score = score
|
self.score = score
|
||||||
@ -65,17 +71,17 @@ class SearchResult:
|
|||||||
def to_dict(self) -> Dict[str, Any]:
|
def to_dict(self) -> Dict[str, Any]:
|
||||||
"""Convert to dictionary."""
|
"""Convert to dictionary."""
|
||||||
return {
|
return {
|
||||||
'file_path': self.file_path,
|
"file_path": self.file_path,
|
||||||
'content': self.content,
|
"content": self.content,
|
||||||
'score': self.score,
|
"score": self.score,
|
||||||
'start_line': self.start_line,
|
"start_line": self.start_line,
|
||||||
'end_line': self.end_line,
|
"end_line": self.end_line,
|
||||||
'chunk_type': self.chunk_type,
|
"chunk_type": self.chunk_type,
|
||||||
'name': self.name,
|
"name": self.name,
|
||||||
'language': self.language,
|
"language": self.language,
|
||||||
'context_before': self.context_before,
|
"context_before": self.context_before,
|
||||||
'context_after': self.context_after,
|
"context_after": self.context_after,
|
||||||
'parent_chunk': self.parent_chunk.to_dict() if self.parent_chunk else None,
|
"parent_chunk": self.parent_chunk.to_dict() if self.parent_chunk else None,
|
||||||
}
|
}
|
||||||
|
|
||||||
def format_for_display(self, max_lines: int = 10) -> str:
|
def format_for_display(self, max_lines: int = 10) -> str:
|
||||||
@ -84,17 +90,15 @@ class SearchResult:
|
|||||||
if len(lines) > max_lines:
|
if len(lines) > max_lines:
|
||||||
# Show first and last few lines
|
# Show first and last few lines
|
||||||
half = max_lines // 2
|
half = max_lines // 2
|
||||||
lines = lines[:half] + ['...'] + lines[-half:]
|
lines = lines[:half] + ["..."] + lines[-half:]
|
||||||
|
|
||||||
return '\n'.join(lines)
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
class CodeSearcher:
|
class CodeSearcher:
|
||||||
"""Semantic code search using vector similarity."""
|
"""Semantic code search using vector similarity."""
|
||||||
|
|
||||||
def __init__(self,
|
def __init__(self, project_path: Path, embedder: Optional[CodeEmbedder] = None):
|
||||||
project_path: Path,
|
|
||||||
embedder: Optional[CodeEmbedder] = None):
|
|
||||||
"""
|
"""
|
||||||
Initialize searcher.
|
Initialize searcher.
|
||||||
|
|
||||||
@ -103,7 +107,7 @@ class CodeSearcher:
|
|||||||
embedder: CodeEmbedder instance (creates one if not provided)
|
embedder: CodeEmbedder instance (creates one if not provided)
|
||||||
"""
|
"""
|
||||||
self.project_path = Path(project_path).resolve()
|
self.project_path = Path(project_path).resolve()
|
||||||
self.rag_dir = self.project_path / '.mini-rag'
|
self.rag_dir = self.project_path / ".mini-rag"
|
||||||
self.embedder = embedder or CodeEmbedder()
|
self.embedder = embedder or CodeEmbedder()
|
||||||
|
|
||||||
# Load configuration and initialize query expander
|
# Load configuration and initialize query expander
|
||||||
@ -128,7 +132,9 @@ class CodeSearcher:
|
|||||||
print(" Install it with: pip install lancedb pyarrow")
|
print(" Install it with: pip install lancedb pyarrow")
|
||||||
print(" For basic Ollama functionality, use hash-based search instead")
|
print(" For basic Ollama functionality, use hash-based search instead")
|
||||||
print()
|
print()
|
||||||
raise ImportError("LanceDB dependency is required for search. Install with: pip install lancedb pyarrow")
|
raise ImportError(
|
||||||
|
"LanceDB dependency is required for search. Install with: pip install lancedb pyarrow"
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if not self.rag_dir.exists():
|
if not self.rag_dir.exists():
|
||||||
@ -144,7 +150,9 @@ class CodeSearcher:
|
|||||||
if "code_vectors" not in self.db.table_names():
|
if "code_vectors" not in self.db.table_names():
|
||||||
print("🔧 Index Database Corrupted")
|
print("🔧 Index Database Corrupted")
|
||||||
print(" The search index exists but is missing data tables")
|
print(" The search index exists but is missing data tables")
|
||||||
print(f" Rebuild index: rm -rf {self.rag_dir} && ./rag-mini index {self.project_path}")
|
print(
|
||||||
|
f" Rebuild index: rm -rf {self.rag_dir} && ./rag-mini index {self.project_path}"
|
||||||
|
)
|
||||||
print(" (This will recreate the search database)")
|
print(" (This will recreate the search database)")
|
||||||
print()
|
print()
|
||||||
raise ValueError("No code_vectors table found. Run indexing first.")
|
raise ValueError("No code_vectors table found. Run indexing first.")
|
||||||
@ -186,7 +194,9 @@ class CodeSearcher:
|
|||||||
logger.error(f"Failed to build BM25 index: {e}")
|
logger.error(f"Failed to build BM25 index: {e}")
|
||||||
self.bm25 = None
|
self.bm25 = None
|
||||||
|
|
||||||
def get_chunk_context(self, chunk_id: str, include_adjacent: bool = True, include_parent: bool = True) -> Dict[str, Any]:
|
def get_chunk_context(
|
||||||
|
self, chunk_id: str, include_adjacent: bool = True, include_parent: bool = True
|
||||||
|
) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Get context for a specific chunk including adjacent and parent chunks.
|
Get context for a specific chunk including adjacent and parent chunks.
|
||||||
|
|
||||||
@ -204,72 +214,81 @@ class CodeSearcher:
|
|||||||
try:
|
try:
|
||||||
# Get the main chunk by ID
|
# Get the main chunk by ID
|
||||||
df = self.table.to_pandas()
|
df = self.table.to_pandas()
|
||||||
chunk_rows = df[df['chunk_id'] == chunk_id]
|
chunk_rows = df[df["chunk_id"] == chunk_id]
|
||||||
|
|
||||||
if chunk_rows.empty:
|
if chunk_rows.empty:
|
||||||
return {'chunk': None, 'prev': None, 'next': None, 'parent': None}
|
return {"chunk": None, "prev": None, "next": None, "parent": None}
|
||||||
|
|
||||||
chunk_row = chunk_rows.iloc[0]
|
chunk_row = chunk_rows.iloc[0]
|
||||||
context = {'chunk': self._row_to_search_result(chunk_row, score=1.0)}
|
context = {"chunk": self._row_to_search_result(chunk_row, score=1.0)}
|
||||||
|
|
||||||
# Get adjacent chunks if requested
|
# Get adjacent chunks if requested
|
||||||
if include_adjacent:
|
if include_adjacent:
|
||||||
# Get previous chunk
|
# Get previous chunk
|
||||||
if pd.notna(chunk_row.get('prev_chunk_id')):
|
if pd.notna(chunk_row.get("prev_chunk_id")):
|
||||||
prev_rows = df[df['chunk_id'] == chunk_row['prev_chunk_id']]
|
prev_rows = df[df["chunk_id"] == chunk_row["prev_chunk_id"]]
|
||||||
if not prev_rows.empty:
|
if not prev_rows.empty:
|
||||||
context['prev'] = self._row_to_search_result(prev_rows.iloc[0], score=1.0)
|
context["prev"] = self._row_to_search_result(
|
||||||
|
prev_rows.iloc[0], score=1.0
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
context['prev'] = None
|
context["prev"] = None
|
||||||
else:
|
else:
|
||||||
context['prev'] = None
|
context["prev"] = None
|
||||||
|
|
||||||
# Get next chunk
|
# Get next chunk
|
||||||
if pd.notna(chunk_row.get('next_chunk_id')):
|
if pd.notna(chunk_row.get("next_chunk_id")):
|
||||||
next_rows = df[df['chunk_id'] == chunk_row['next_chunk_id']]
|
next_rows = df[df["chunk_id"] == chunk_row["next_chunk_id"]]
|
||||||
if not next_rows.empty:
|
if not next_rows.empty:
|
||||||
context['next'] = self._row_to_search_result(next_rows.iloc[0], score=1.0)
|
context["next"] = self._row_to_search_result(
|
||||||
|
next_rows.iloc[0], score=1.0
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
context['next'] = None
|
context["next"] = None
|
||||||
else:
|
else:
|
||||||
context['next'] = None
|
context["next"] = None
|
||||||
else:
|
else:
|
||||||
context['prev'] = None
|
context["prev"] = None
|
||||||
context['next'] = None
|
context["next"] = None
|
||||||
|
|
||||||
# Get parent class chunk if requested and applicable
|
# Get parent class chunk if requested and applicable
|
||||||
if include_parent and pd.notna(chunk_row.get('parent_class')):
|
if include_parent and pd.notna(chunk_row.get("parent_class")):
|
||||||
# Find the parent class chunk
|
# Find the parent class chunk
|
||||||
parent_rows = df[(df['name'] == chunk_row['parent_class']) &
|
parent_rows = df[
|
||||||
(df['chunk_type'] == 'class') &
|
(df["name"] == chunk_row["parent_class"])
|
||||||
(df['file_path'] == chunk_row['file_path'])]
|
& (df["chunk_type"] == "class")
|
||||||
|
& (df["file_path"] == chunk_row["file_path"])
|
||||||
|
]
|
||||||
if not parent_rows.empty:
|
if not parent_rows.empty:
|
||||||
context['parent'] = self._row_to_search_result(parent_rows.iloc[0], score=1.0)
|
context["parent"] = self._row_to_search_result(
|
||||||
|
parent_rows.iloc[0], score=1.0
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
context['parent'] = None
|
context["parent"] = None
|
||||||
else:
|
else:
|
||||||
context['parent'] = None
|
context["parent"] = None
|
||||||
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to get chunk context: {e}")
|
logger.error(f"Failed to get chunk context: {e}")
|
||||||
return {'chunk': None, 'prev': None, 'next': None, 'parent': None}
|
return {"chunk": None, "prev": None, "next": None, "parent": None}
|
||||||
|
|
||||||
def _row_to_search_result(self, row: pd.Series, score: float) -> SearchResult:
|
def _row_to_search_result(self, row: pd.Series, score: float) -> SearchResult:
|
||||||
"""Convert a DataFrame row to a SearchResult."""
|
"""Convert a DataFrame row to a SearchResult."""
|
||||||
return SearchResult(
|
return SearchResult(
|
||||||
file_path=display_path(row['file_path']),
|
file_path=display_path(row["file_path"]),
|
||||||
content=row['content'],
|
content=row["content"],
|
||||||
score=score,
|
score=score,
|
||||||
start_line=row['start_line'],
|
start_line=row["start_line"],
|
||||||
end_line=row['end_line'],
|
end_line=row["end_line"],
|
||||||
chunk_type=row['chunk_type'],
|
chunk_type=row["chunk_type"],
|
||||||
name=row['name'],
|
name=row["name"],
|
||||||
language=row['language']
|
language=row["language"],
|
||||||
)
|
)
|
||||||
|
|
||||||
def search(self,
|
def search(
|
||||||
|
self,
|
||||||
query: str,
|
query: str,
|
||||||
top_k: int = 10,
|
top_k: int = 10,
|
||||||
chunk_types: Optional[List[str]] = None,
|
chunk_types: Optional[List[str]] = None,
|
||||||
@ -277,7 +296,8 @@ class CodeSearcher:
|
|||||||
file_pattern: Optional[str] = None,
|
file_pattern: Optional[str] = None,
|
||||||
semantic_weight: float = 0.7,
|
semantic_weight: float = 0.7,
|
||||||
bm25_weight: float = 0.3,
|
bm25_weight: float = 0.3,
|
||||||
include_context: bool = False) -> List[SearchResult]:
|
include_context: bool = False,
|
||||||
|
) -> List[SearchResult]:
|
||||||
"""
|
"""
|
||||||
Hybrid search for code similar to the query using both semantic and BM25.
|
Hybrid search for code similar to the query using both semantic and BM25.
|
||||||
|
|
||||||
@ -324,16 +344,15 @@ class CodeSearcher:
|
|||||||
|
|
||||||
# Apply filters first
|
# Apply filters first
|
||||||
if chunk_types:
|
if chunk_types:
|
||||||
results_df = results_df[results_df['chunk_type'].isin(chunk_types)]
|
results_df = results_df[results_df["chunk_type"].isin(chunk_types)]
|
||||||
|
|
||||||
if languages:
|
if languages:
|
||||||
results_df = results_df[results_df['language'].isin(languages)]
|
results_df = results_df[results_df["language"].isin(languages)]
|
||||||
|
|
||||||
if file_pattern:
|
if file_pattern:
|
||||||
import fnmatch
|
import fnmatch
|
||||||
mask = results_df['file_path'].apply(
|
|
||||||
lambda x: fnmatch.fnmatch(x, file_pattern)
|
mask = results_df["file_path"].apply(lambda x: fnmatch.fnmatch(x, file_pattern))
|
||||||
)
|
|
||||||
results_df = results_df[mask]
|
results_df = results_df[mask]
|
||||||
|
|
||||||
# Calculate BM25 scores if available
|
# Calculate BM25 scores if available
|
||||||
@ -358,25 +377,24 @@ class CodeSearcher:
|
|||||||
hybrid_results = []
|
hybrid_results = []
|
||||||
for idx, row in results_df.iterrows():
|
for idx, row in results_df.iterrows():
|
||||||
# Semantic score (convert distance to similarity)
|
# Semantic score (convert distance to similarity)
|
||||||
distance = row['_distance']
|
distance = row["_distance"]
|
||||||
semantic_score = 1 / (1 + distance)
|
semantic_score = 1 / (1 + distance)
|
||||||
|
|
||||||
# BM25 score
|
# BM25 score
|
||||||
bm25_score = bm25_scores.get(idx, 0.0)
|
bm25_score = bm25_scores.get(idx, 0.0)
|
||||||
|
|
||||||
# Combined score
|
# Combined score
|
||||||
combined_score = (semantic_weight * semantic_score +
|
combined_score = semantic_weight * semantic_score + bm25_weight * bm25_score
|
||||||
bm25_weight * bm25_score)
|
|
||||||
|
|
||||||
result = SearchResult(
|
result = SearchResult(
|
||||||
file_path=display_path(row['file_path']),
|
file_path=display_path(row["file_path"]),
|
||||||
content=row['content'],
|
content=row["content"],
|
||||||
score=combined_score,
|
score=combined_score,
|
||||||
start_line=row['start_line'],
|
start_line=row["start_line"],
|
||||||
end_line=row['end_line'],
|
end_line=row["end_line"],
|
||||||
chunk_type=row['chunk_type'],
|
chunk_type=row["chunk_type"],
|
||||||
name=row['name'],
|
name=row["name"],
|
||||||
language=row['language']
|
language=row["language"],
|
||||||
)
|
)
|
||||||
hybrid_results.append(result)
|
hybrid_results.append(result)
|
||||||
|
|
||||||
@ -407,9 +425,20 @@ class CodeSearcher:
|
|||||||
# File importance boost (20% boost for important files)
|
# File importance boost (20% boost for important files)
|
||||||
file_path_lower = str(result.file_path).lower()
|
file_path_lower = str(result.file_path).lower()
|
||||||
important_patterns = [
|
important_patterns = [
|
||||||
'readme', 'main.', 'index.', '__init__', 'config',
|
"readme",
|
||||||
'setup', 'install', 'getting', 'started', 'docs/',
|
"main.",
|
||||||
'documentation', 'guide', 'tutorial', 'example'
|
"index.",
|
||||||
|
"__init__",
|
||||||
|
"config",
|
||||||
|
"setup",
|
||||||
|
"install",
|
||||||
|
"getting",
|
||||||
|
"started",
|
||||||
|
"docs/",
|
||||||
|
"documentation",
|
||||||
|
"guide",
|
||||||
|
"tutorial",
|
||||||
|
"example",
|
||||||
]
|
]
|
||||||
|
|
||||||
if any(pattern in file_path_lower for pattern in important_patterns):
|
if any(pattern in file_path_lower for pattern in important_patterns):
|
||||||
@ -426,7 +455,9 @@ class CodeSearcher:
|
|||||||
|
|
||||||
if days_old <= 7: # Modified in last week
|
if days_old <= 7: # Modified in last week
|
||||||
result.score *= 1.1
|
result.score *= 1.1
|
||||||
logger.debug(f"Recent file boost: {result.file_path} ({days_old} days old)")
|
logger.debug(
|
||||||
|
f"Recent file boost: {result.file_path} ({days_old} days old)"
|
||||||
|
)
|
||||||
elif days_old <= 30: # Modified in last month
|
elif days_old <= 30: # Modified in last month
|
||||||
result.score *= 1.05
|
result.score *= 1.05
|
||||||
|
|
||||||
@ -435,11 +466,11 @@ class CodeSearcher:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
# Content type relevance boost
|
# Content type relevance boost
|
||||||
if hasattr(result, 'chunk_type'):
|
if hasattr(result, "chunk_type"):
|
||||||
if result.chunk_type in ['function', 'class', 'method']:
|
if result.chunk_type in ["function", "class", "method"]:
|
||||||
# Code definitions are usually more valuable
|
# Code definitions are usually more valuable
|
||||||
result.score *= 1.1
|
result.score *= 1.1
|
||||||
elif result.chunk_type in ['comment', 'docstring']:
|
elif result.chunk_type in ["comment", "docstring"]:
|
||||||
# Documentation is valuable for understanding
|
# Documentation is valuable for understanding
|
||||||
result.score *= 1.05
|
result.score *= 1.05
|
||||||
|
|
||||||
@ -448,14 +479,16 @@ class CodeSearcher:
|
|||||||
result.score *= 0.9
|
result.score *= 0.9
|
||||||
|
|
||||||
# Small boost for content with good structure (has multiple lines)
|
# Small boost for content with good structure (has multiple lines)
|
||||||
lines = result.content.strip().split('\n')
|
lines = result.content.strip().split("\n")
|
||||||
if len(lines) >= 3 and any(len(line.strip()) > 10 for line in lines):
|
if len(lines) >= 3 and any(len(line.strip()) > 10 for line in lines):
|
||||||
result.score *= 1.02
|
result.score *= 1.02
|
||||||
|
|
||||||
# Sort by updated scores
|
# Sort by updated scores
|
||||||
return sorted(results, key=lambda x: x.score, reverse=True)
|
return sorted(results, key=lambda x: x.score, reverse=True)
|
||||||
|
|
||||||
def _apply_diversity_constraints(self, results: List[SearchResult], top_k: int) -> List[SearchResult]:
|
def _apply_diversity_constraints(
|
||||||
|
self, results: List[SearchResult], top_k: int
|
||||||
|
) -> List[SearchResult]:
|
||||||
"""
|
"""
|
||||||
Apply diversity constraints to search results.
|
Apply diversity constraints to search results.
|
||||||
|
|
||||||
@ -479,7 +512,10 @@ class CodeSearcher:
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
# Prefer diverse chunk types
|
# Prefer diverse chunk types
|
||||||
if len(final_results) >= top_k // 2 and chunk_type_counts[result.chunk_type] > top_k // 3:
|
if (
|
||||||
|
len(final_results) >= top_k // 2
|
||||||
|
and chunk_type_counts[result.chunk_type] > top_k // 3
|
||||||
|
):
|
||||||
# Skip if we have too many of this type already
|
# Skip if we have too many of this type already
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@ -494,7 +530,9 @@ class CodeSearcher:
|
|||||||
|
|
||||||
return final_results
|
return final_results
|
||||||
|
|
||||||
def _add_context_to_results(self, results: List[SearchResult], search_df: pd.DataFrame) -> List[SearchResult]:
|
def _add_context_to_results(
|
||||||
|
self, results: List[SearchResult], search_df: pd.DataFrame
|
||||||
|
) -> List[SearchResult]:
|
||||||
"""
|
"""
|
||||||
Add context (adjacent and parent chunks) to search results.
|
Add context (adjacent and parent chunks) to search results.
|
||||||
|
|
||||||
@ -513,12 +551,12 @@ class CodeSearcher:
|
|||||||
for result in results:
|
for result in results:
|
||||||
# Find matching row in search_df
|
# Find matching row in search_df
|
||||||
matching_rows = search_df[
|
matching_rows = search_df[
|
||||||
(search_df['file_path'] == result.file_path) &
|
(search_df["file_path"] == result.file_path)
|
||||||
(search_df['start_line'] == result.start_line) &
|
& (search_df["start_line"] == result.start_line)
|
||||||
(search_df['end_line'] == result.end_line)
|
& (search_df["end_line"] == result.end_line)
|
||||||
]
|
]
|
||||||
if not matching_rows.empty:
|
if not matching_rows.empty:
|
||||||
result_to_chunk_id[result] = matching_rows.iloc[0]['chunk_id']
|
result_to_chunk_id[result] = matching_rows.iloc[0]["chunk_id"]
|
||||||
|
|
||||||
# Add context to each result
|
# Add context to each result
|
||||||
for result in results:
|
for result in results:
|
||||||
@ -527,49 +565,48 @@ class CodeSearcher:
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
# Get the row for this chunk
|
# Get the row for this chunk
|
||||||
chunk_rows = full_df[full_df['chunk_id'] == chunk_id]
|
chunk_rows = full_df[full_df["chunk_id"] == chunk_id]
|
||||||
if chunk_rows.empty:
|
if chunk_rows.empty:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
chunk_row = chunk_rows.iloc[0]
|
chunk_row = chunk_rows.iloc[0]
|
||||||
|
|
||||||
# Add adjacent chunks as context
|
# Add adjacent chunks as context
|
||||||
if pd.notna(chunk_row.get('prev_chunk_id')):
|
if pd.notna(chunk_row.get("prev_chunk_id")):
|
||||||
prev_rows = full_df[full_df['chunk_id'] == chunk_row['prev_chunk_id']]
|
prev_rows = full_df[full_df["chunk_id"] == chunk_row["prev_chunk_id"]]
|
||||||
if not prev_rows.empty:
|
if not prev_rows.empty:
|
||||||
result.context_before = prev_rows.iloc[0]['content']
|
result.context_before = prev_rows.iloc[0]["content"]
|
||||||
|
|
||||||
if pd.notna(chunk_row.get('next_chunk_id')):
|
if pd.notna(chunk_row.get("next_chunk_id")):
|
||||||
next_rows = full_df[full_df['chunk_id'] == chunk_row['next_chunk_id']]
|
next_rows = full_df[full_df["chunk_id"] == chunk_row["next_chunk_id"]]
|
||||||
if not next_rows.empty:
|
if not next_rows.empty:
|
||||||
result.context_after = next_rows.iloc[0]['content']
|
result.context_after = next_rows.iloc[0]["content"]
|
||||||
|
|
||||||
# Add parent class chunk if applicable
|
# Add parent class chunk if applicable
|
||||||
if pd.notna(chunk_row.get('parent_class')):
|
if pd.notna(chunk_row.get("parent_class")):
|
||||||
parent_rows = full_df[
|
parent_rows = full_df[
|
||||||
(full_df['name'] == chunk_row['parent_class']) &
|
(full_df["name"] == chunk_row["parent_class"])
|
||||||
(full_df['chunk_type'] == 'class') &
|
& (full_df["chunk_type"] == "class")
|
||||||
(full_df['file_path'] == chunk_row['file_path'])
|
& (full_df["file_path"] == chunk_row["file_path"])
|
||||||
]
|
]
|
||||||
if not parent_rows.empty:
|
if not parent_rows.empty:
|
||||||
parent_row = parent_rows.iloc[0]
|
parent_row = parent_rows.iloc[0]
|
||||||
result.parent_chunk = SearchResult(
|
result.parent_chunk = SearchResult(
|
||||||
file_path=display_path(parent_row['file_path']),
|
file_path=display_path(parent_row["file_path"]),
|
||||||
content=parent_row['content'],
|
content=parent_row["content"],
|
||||||
score=1.0,
|
score=1.0,
|
||||||
start_line=parent_row['start_line'],
|
start_line=parent_row["start_line"],
|
||||||
end_line=parent_row['end_line'],
|
end_line=parent_row["end_line"],
|
||||||
chunk_type=parent_row['chunk_type'],
|
chunk_type=parent_row["chunk_type"],
|
||||||
name=parent_row['name'],
|
name=parent_row["name"],
|
||||||
language=parent_row['language']
|
language=parent_row["language"],
|
||||||
)
|
)
|
||||||
|
|
||||||
return results
|
return results
|
||||||
|
|
||||||
def search_similar_code(self,
|
def search_similar_code(
|
||||||
code_snippet: str,
|
self, code_snippet: str, top_k: int = 10, exclude_self: bool = True
|
||||||
top_k: int = 10,
|
) -> List[SearchResult]:
|
||||||
exclude_self: bool = True) -> List[SearchResult]:
|
|
||||||
"""
|
"""
|
||||||
Find code similar to a given snippet using hybrid search.
|
Find code similar to a given snippet using hybrid search.
|
||||||
|
|
||||||
@ -587,7 +624,7 @@ class CodeSearcher:
|
|||||||
query=code_snippet,
|
query=code_snippet,
|
||||||
top_k=top_k * 2 if exclude_self else top_k,
|
top_k=top_k * 2 if exclude_self else top_k,
|
||||||
semantic_weight=0.8, # Higher semantic weight for code similarity
|
semantic_weight=0.8, # Higher semantic weight for code similarity
|
||||||
bm25_weight=0.2
|
bm25_weight=0.2,
|
||||||
)
|
)
|
||||||
|
|
||||||
if exclude_self:
|
if exclude_self:
|
||||||
@ -617,11 +654,7 @@ class CodeSearcher:
|
|||||||
query = f"function {function_name} implementation definition"
|
query = f"function {function_name} implementation definition"
|
||||||
|
|
||||||
# Search with filters
|
# Search with filters
|
||||||
results = self.search(
|
results = self.search(query, top_k=top_k * 2, chunk_types=["function", "method"])
|
||||||
query,
|
|
||||||
top_k=top_k * 2,
|
|
||||||
chunk_types=['function', 'method']
|
|
||||||
)
|
|
||||||
|
|
||||||
# Further filter by name
|
# Further filter by name
|
||||||
filtered = []
|
filtered = []
|
||||||
@ -646,11 +679,7 @@ class CodeSearcher:
|
|||||||
query = f"class {class_name} definition implementation"
|
query = f"class {class_name} definition implementation"
|
||||||
|
|
||||||
# Search with filters
|
# Search with filters
|
||||||
results = self.search(
|
results = self.search(query, top_k=top_k * 2, chunk_types=["class"])
|
||||||
query,
|
|
||||||
top_k=top_k * 2,
|
|
||||||
chunk_types=['class']
|
|
||||||
)
|
|
||||||
|
|
||||||
# Further filter by name
|
# Further filter by name
|
||||||
filtered = []
|
filtered = []
|
||||||
@ -700,10 +729,12 @@ class CodeSearcher:
|
|||||||
|
|
||||||
return filtered[:top_k]
|
return filtered[:top_k]
|
||||||
|
|
||||||
def display_results(self,
|
def display_results(
|
||||||
|
self,
|
||||||
results: List[SearchResult],
|
results: List[SearchResult],
|
||||||
show_content: bool = True,
|
show_content: bool = True,
|
||||||
max_content_lines: int = 10):
|
max_content_lines: int = 10,
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
Display search results in a formatted table.
|
Display search results in a formatted table.
|
||||||
|
|
||||||
@ -730,7 +761,7 @@ class CodeSearcher:
|
|||||||
result.file_path,
|
result.file_path,
|
||||||
result.chunk_type,
|
result.chunk_type,
|
||||||
result.name or "-",
|
result.name or "-",
|
||||||
f"{result.start_line}-{result.end_line}"
|
f"{result.start_line}-{result.end_line}",
|
||||||
)
|
)
|
||||||
|
|
||||||
console.print(table)
|
console.print(table)
|
||||||
@ -740,7 +771,9 @@ class CodeSearcher:
|
|||||||
console.print("\n[bold]Top Results:[/bold]\n")
|
console.print("\n[bold]Top Results:[/bold]\n")
|
||||||
|
|
||||||
for i, result in enumerate(results[:3], 1):
|
for i, result in enumerate(results[:3], 1):
|
||||||
console.print(f"[bold cyan]#{i}[/bold cyan] {result.file_path}:{result.start_line}")
|
console.print(
|
||||||
|
f"[bold cyan]#{i}[/bold cyan] {result.file_path}:{result.start_line}"
|
||||||
|
)
|
||||||
console.print(f"[dim]Type: {result.chunk_type} | Name: {result.name}[/dim]")
|
console.print(f"[dim]Type: {result.chunk_type} | Name: {result.name}[/dim]")
|
||||||
|
|
||||||
# Display code with syntax highlighting
|
# Display code with syntax highlighting
|
||||||
@ -749,7 +782,7 @@ class CodeSearcher:
|
|||||||
result.language,
|
result.language,
|
||||||
theme="monokai",
|
theme="monokai",
|
||||||
line_numbers=True,
|
line_numbers=True,
|
||||||
start_line=result.start_line
|
start_line=result.start_line,
|
||||||
)
|
)
|
||||||
console.print(syntax)
|
console.print(syntax)
|
||||||
console.print()
|
console.print()
|
||||||
@ -757,7 +790,7 @@ class CodeSearcher:
|
|||||||
def get_statistics(self) -> Dict[str, Any]:
|
def get_statistics(self) -> Dict[str, Any]:
|
||||||
"""Get search index statistics."""
|
"""Get search index statistics."""
|
||||||
if not self.table:
|
if not self.table:
|
||||||
return {'error': 'Database not connected'}
|
return {"error": "Database not connected"}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Get table statistics
|
# Get table statistics
|
||||||
@ -765,28 +798,30 @@ class CodeSearcher:
|
|||||||
|
|
||||||
# Get unique files
|
# Get unique files
|
||||||
df = self.table.to_pandas()
|
df = self.table.to_pandas()
|
||||||
unique_files = df['file_path'].nunique()
|
unique_files = df["file_path"].nunique()
|
||||||
|
|
||||||
# Get chunk type distribution
|
# Get chunk type distribution
|
||||||
chunk_types = df['chunk_type'].value_counts().to_dict()
|
chunk_types = df["chunk_type"].value_counts().to_dict()
|
||||||
|
|
||||||
# Get language distribution
|
# Get language distribution
|
||||||
languages = df['language'].value_counts().to_dict()
|
languages = df["language"].value_counts().to_dict()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'total_chunks': num_rows,
|
"total_chunks": num_rows,
|
||||||
'unique_files': unique_files,
|
"unique_files": unique_files,
|
||||||
'chunk_types': chunk_types,
|
"chunk_types": chunk_types,
|
||||||
'languages': languages,
|
"languages": languages,
|
||||||
'index_ready': True,
|
"index_ready": True,
|
||||||
}
|
}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to get statistics: {e}")
|
logger.error(f"Failed to get statistics: {e}")
|
||||||
return {'error': str(e)}
|
return {"error": str(e)}
|
||||||
|
|
||||||
|
|
||||||
# Convenience functions
|
# Convenience functions
|
||||||
|
|
||||||
|
|
||||||
def search_code(project_path: Path, query: str, top_k: int = 10) -> List[SearchResult]:
|
def search_code(project_path: Path, query: str, top_k: int = 10) -> List[SearchResult]:
|
||||||
"""
|
"""
|
||||||
Quick search function.
|
Quick search function.
|
||||||
|
|||||||
@ -4,23 +4,23 @@ No more loading/unloading madness!
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
import socket
|
import socket
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
import subprocess
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Dict, Any, Optional
|
from typing import Any, Dict, Optional
|
||||||
import logging
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
|
|
||||||
# Fix Windows console
|
# Fix Windows console
|
||||||
if sys.platform == 'win32':
|
if sys.platform == "win32":
|
||||||
os.environ['PYTHONUTF8'] = '1'
|
os.environ["PYTHONUTF8"] = "1"
|
||||||
|
|
||||||
from .search import CodeSearcher
|
|
||||||
from .ollama_embeddings import OllamaEmbedder as CodeEmbedder
|
from .ollama_embeddings import OllamaEmbedder as CodeEmbedder
|
||||||
from .performance import PerformanceMonitor
|
from .performance import PerformanceMonitor
|
||||||
|
from .search import CodeSearcher
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -43,31 +43,30 @@ class RAGServer:
|
|||||||
try:
|
try:
|
||||||
# Check if port is in use
|
# Check if port is in use
|
||||||
test_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
test_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
result = test_sock.connect_ex(('localhost', self.port))
|
result = test_sock.connect_ex(("localhost", self.port))
|
||||||
test_sock.close()
|
test_sock.close()
|
||||||
|
|
||||||
if result == 0: # Port is in use
|
if result == 0: # Port is in use
|
||||||
print(f"️ Port {self.port} is already in use, attempting to free it...")
|
print(f"️ Port {self.port} is already in use, attempting to free it...")
|
||||||
|
|
||||||
if sys.platform == 'win32':
|
if sys.platform == "win32":
|
||||||
# Windows: Find and kill process using netstat
|
# Windows: Find and kill process using netstat
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Get process ID using the port
|
# Get process ID using the port
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
['netstat', '-ano'],
|
["netstat", "-ano"], capture_output=True, text=True
|
||||||
capture_output=True,
|
|
||||||
text=True
|
|
||||||
)
|
)
|
||||||
|
|
||||||
for line in result.stdout.split('\n'):
|
for line in result.stdout.split("\n"):
|
||||||
if f':{self.port}' in line and 'LISTENING' in line:
|
if f":{self.port}" in line and "LISTENING" in line:
|
||||||
parts = line.split()
|
parts = line.split()
|
||||||
pid = parts[-1]
|
pid = parts[-1]
|
||||||
print(f" Found process {pid} using port {self.port}")
|
print(f" Found process {pid} using port {self.port}")
|
||||||
|
|
||||||
# Kill the process
|
# Kill the process
|
||||||
subprocess.run(['taskkill', '//PID', pid, '//F'], check=False)
|
subprocess.run(["taskkill", "//PID", pid, "//F"], check=False)
|
||||||
print(f" Killed process {pid}")
|
print(f" Killed process {pid}")
|
||||||
time.sleep(1) # Give it a moment to release the port
|
time.sleep(1) # Give it a moment to release the port
|
||||||
break
|
break
|
||||||
@ -76,15 +75,16 @@ class RAGServer:
|
|||||||
else:
|
else:
|
||||||
# Unix/Linux: Use lsof and kill
|
# Unix/Linux: Use lsof and kill
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
['lsof', '-ti', f':{self.port}'],
|
["lso", "-ti", f":{self.port}"],
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
text=True
|
text=True,
|
||||||
)
|
)
|
||||||
if result.stdout.strip():
|
if result.stdout.strip():
|
||||||
pid = result.stdout.strip()
|
pid = result.stdout.strip()
|
||||||
subprocess.run(['kill', '-9', pid], check=False)
|
subprocess.run(["kill", "-9", pid], check=False)
|
||||||
print(f" Killed process {pid}")
|
print(f" Killed process {pid}")
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -114,7 +114,7 @@ class RAGServer:
|
|||||||
# Start server
|
# Start server
|
||||||
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||||
self.socket.bind(('localhost', self.port))
|
self.socket.bind(("localhost", self.port))
|
||||||
self.socket.listen(5)
|
self.socket.listen(5)
|
||||||
|
|
||||||
self.running = True
|
self.running = True
|
||||||
@ -145,15 +145,15 @@ class RAGServer:
|
|||||||
request = json.loads(data)
|
request = json.loads(data)
|
||||||
|
|
||||||
# Check for shutdown command
|
# Check for shutdown command
|
||||||
if request.get('command') == 'shutdown':
|
if request.get("command") == "shutdown":
|
||||||
print("\n Shutdown requested")
|
print("\n Shutdown requested")
|
||||||
response = {'success': True, 'message': 'Server shutting down'}
|
response = {"success": True, "message": "Server shutting down"}
|
||||||
self._send_json(client, response)
|
self._send_json(client, response)
|
||||||
self.stop()
|
self.stop()
|
||||||
return
|
return
|
||||||
|
|
||||||
query = request.get('query', '')
|
query = request.get("query", "")
|
||||||
top_k = request.get('top_k', 10)
|
top_k = request.get("top_k", 10)
|
||||||
|
|
||||||
self.query_count += 1
|
self.query_count += 1
|
||||||
print(f"[Query #{self.query_count}] {query}")
|
print(f"[Query #{self.query_count}] {query}")
|
||||||
@ -165,13 +165,13 @@ class RAGServer:
|
|||||||
|
|
||||||
# Prepare response
|
# Prepare response
|
||||||
response = {
|
response = {
|
||||||
'success': True,
|
"success": True,
|
||||||
'query': query,
|
"query": query,
|
||||||
'count': len(results),
|
"count": len(results),
|
||||||
'search_time_ms': int(search_time * 1000),
|
"search_time_ms": int(search_time * 1000),
|
||||||
'results': [r.to_dict() for r in results],
|
"results": [r.to_dict() for r in results],
|
||||||
'server_uptime': int(time.time() - self.start_time),
|
"server_uptime": int(time.time() - self.start_time),
|
||||||
'total_queries': self.query_count,
|
"total_queries": self.query_count,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Send response with proper framing
|
# Send response with proper framing
|
||||||
@ -179,7 +179,7 @@ class RAGServer:
|
|||||||
|
|
||||||
print(f" Found {len(results)} results in {search_time*1000:.0f}ms")
|
print(f" Found {len(results)} results in {search_time*1000:.0f}ms")
|
||||||
|
|
||||||
except ConnectionError as e:
|
except ConnectionError:
|
||||||
# Normal disconnection - client closed connection
|
# Normal disconnection - client closed connection
|
||||||
# This is expected behavior, don't log as error
|
# This is expected behavior, don't log as error
|
||||||
pass
|
pass
|
||||||
@ -187,13 +187,10 @@ class RAGServer:
|
|||||||
# Only log actual errors, not normal disconnections
|
# Only log actual errors, not normal disconnections
|
||||||
if "Connection closed" not in str(e):
|
if "Connection closed" not in str(e):
|
||||||
logger.error(f"Client handler error: {e}")
|
logger.error(f"Client handler error: {e}")
|
||||||
error_response = {
|
error_response = {"success": False, "error": str(e)}
|
||||||
'success': False,
|
|
||||||
'error': str(e)
|
|
||||||
}
|
|
||||||
try:
|
try:
|
||||||
self._send_json(client, error_response)
|
self._send_json(client, error_response)
|
||||||
except:
|
except (ConnectionError, OSError, TypeError, ValueError, socket.error):
|
||||||
pass
|
pass
|
||||||
finally:
|
finally:
|
||||||
client.close()
|
client.close()
|
||||||
@ -201,34 +198,34 @@ class RAGServer:
|
|||||||
def _receive_json(self, sock: socket.socket) -> str:
|
def _receive_json(self, sock: socket.socket) -> str:
|
||||||
"""Receive a complete JSON message with length prefix."""
|
"""Receive a complete JSON message with length prefix."""
|
||||||
# First receive the length (4 bytes)
|
# First receive the length (4 bytes)
|
||||||
length_data = b''
|
length_data = b""
|
||||||
while len(length_data) < 4:
|
while len(length_data) < 4:
|
||||||
chunk = sock.recv(4 - len(length_data))
|
chunk = sock.recv(4 - len(length_data))
|
||||||
if not chunk:
|
if not chunk:
|
||||||
raise ConnectionError("Connection closed while receiving length")
|
raise ConnectionError("Connection closed while receiving length")
|
||||||
length_data += chunk
|
length_data += chunk
|
||||||
|
|
||||||
length = int.from_bytes(length_data, 'big')
|
length = int.from_bytes(length_data, "big")
|
||||||
|
|
||||||
# Now receive the actual data
|
# Now receive the actual data
|
||||||
data = b''
|
data = b""
|
||||||
while len(data) < length:
|
while len(data) < length:
|
||||||
chunk = sock.recv(min(65536, length - len(data)))
|
chunk = sock.recv(min(65536, length - len(data)))
|
||||||
if not chunk:
|
if not chunk:
|
||||||
raise ConnectionError("Connection closed while receiving data")
|
raise ConnectionError("Connection closed while receiving data")
|
||||||
data += chunk
|
data += chunk
|
||||||
|
|
||||||
return data.decode('utf-8')
|
return data.decode("utf-8")
|
||||||
|
|
||||||
def _send_json(self, sock: socket.socket, data: dict):
|
def _send_json(self, sock: socket.socket, data: dict):
|
||||||
"""Send a JSON message with length prefix."""
|
"""Send a JSON message with length prefix."""
|
||||||
# Sanitize the data to ensure JSON compatibility
|
# Sanitize the data to ensure JSON compatibility
|
||||||
json_str = json.dumps(data, ensure_ascii=False, separators=(',', ':'))
|
json_str = json.dumps(data, ensure_ascii=False, separators=(",", ":"))
|
||||||
json_bytes = json_str.encode('utf-8')
|
json_bytes = json_str.encode("utf-8")
|
||||||
|
|
||||||
# Send length prefix (4 bytes)
|
# Send length prefix (4 bytes)
|
||||||
length = len(json_bytes)
|
length = len(json_bytes)
|
||||||
sock.send(length.to_bytes(4, 'big'))
|
sock.send(length.to_bytes(4, "big"))
|
||||||
|
|
||||||
# Send the data
|
# Send the data
|
||||||
sock.sendall(json_bytes)
|
sock.sendall(json_bytes)
|
||||||
@ -253,13 +250,10 @@ class RAGClient:
|
|||||||
try:
|
try:
|
||||||
# Connect to server
|
# Connect to server
|
||||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
sock.connect(('localhost', self.port))
|
sock.connect(("localhost", self.port))
|
||||||
|
|
||||||
# Send request with proper framing
|
# Send request with proper framing
|
||||||
request = {
|
request = {"query": query, "top_k": top_k}
|
||||||
'query': query,
|
|
||||||
'top_k': top_k
|
|
||||||
}
|
|
||||||
self._send_json(sock, request)
|
self._send_json(sock, request)
|
||||||
|
|
||||||
# Receive response with proper framing
|
# Receive response with proper framing
|
||||||
@ -271,54 +265,48 @@ class RAGClient:
|
|||||||
|
|
||||||
except ConnectionRefusedError:
|
except ConnectionRefusedError:
|
||||||
return {
|
return {
|
||||||
'success': False,
|
"success": False,
|
||||||
'error': 'RAG server not running. Start with: rag-mini server'
|
"error": "RAG server not running. Start with: rag-mini server",
|
||||||
}
|
}
|
||||||
except ConnectionError as e:
|
except ConnectionError as e:
|
||||||
# Try legacy mode without message framing
|
# Try legacy mode without message framing
|
||||||
if not self.use_legacy and "receiving length" in str(e):
|
if not self.use_legacy and "receiving length" in str(e):
|
||||||
self.use_legacy = True
|
self.use_legacy = True
|
||||||
return self._search_legacy(query, top_k)
|
return self._search_legacy(query, top_k)
|
||||||
return {
|
return {"success": False, "error": str(e)}
|
||||||
'success': False,
|
|
||||||
'error': str(e)
|
|
||||||
}
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return {
|
return {"success": False, "error": str(e)}
|
||||||
'success': False,
|
|
||||||
'error': str(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
def _receive_json(self, sock: socket.socket) -> str:
|
def _receive_json(self, sock: socket.socket) -> str:
|
||||||
"""Receive a complete JSON message with length prefix."""
|
"""Receive a complete JSON message with length prefix."""
|
||||||
# First receive the length (4 bytes)
|
# First receive the length (4 bytes)
|
||||||
length_data = b''
|
length_data = b""
|
||||||
while len(length_data) < 4:
|
while len(length_data) < 4:
|
||||||
chunk = sock.recv(4 - len(length_data))
|
chunk = sock.recv(4 - len(length_data))
|
||||||
if not chunk:
|
if not chunk:
|
||||||
raise ConnectionError("Connection closed while receiving length")
|
raise ConnectionError("Connection closed while receiving length")
|
||||||
length_data += chunk
|
length_data += chunk
|
||||||
|
|
||||||
length = int.from_bytes(length_data, 'big')
|
length = int.from_bytes(length_data, "big")
|
||||||
|
|
||||||
# Now receive the actual data
|
# Now receive the actual data
|
||||||
data = b''
|
data = b""
|
||||||
while len(data) < length:
|
while len(data) < length:
|
||||||
chunk = sock.recv(min(65536, length - len(data)))
|
chunk = sock.recv(min(65536, length - len(data)))
|
||||||
if not chunk:
|
if not chunk:
|
||||||
raise ConnectionError("Connection closed while receiving data")
|
raise ConnectionError("Connection closed while receiving data")
|
||||||
data += chunk
|
data += chunk
|
||||||
|
|
||||||
return data.decode('utf-8')
|
return data.decode("utf-8")
|
||||||
|
|
||||||
def _send_json(self, sock: socket.socket, data: dict):
|
def _send_json(self, sock: socket.socket, data: dict):
|
||||||
"""Send a JSON message with length prefix."""
|
"""Send a JSON message with length prefix."""
|
||||||
json_str = json.dumps(data, ensure_ascii=False, separators=(',', ':'))
|
json_str = json.dumps(data, ensure_ascii=False, separators=(",", ":"))
|
||||||
json_bytes = json_str.encode('utf-8')
|
json_bytes = json_str.encode("utf-8")
|
||||||
|
|
||||||
# Send length prefix (4 bytes)
|
# Send length prefix (4 bytes)
|
||||||
length = len(json_bytes)
|
length = len(json_bytes)
|
||||||
sock.send(length.to_bytes(4, 'big'))
|
sock.send(length.to_bytes(4, "big"))
|
||||||
|
|
||||||
# Send the data
|
# Send the data
|
||||||
sock.sendall(json_bytes)
|
sock.sendall(json_bytes)
|
||||||
@ -327,17 +315,14 @@ class RAGClient:
|
|||||||
"""Legacy search without message framing for old servers."""
|
"""Legacy search without message framing for old servers."""
|
||||||
try:
|
try:
|
||||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
sock.connect(('localhost', self.port))
|
sock.connect(("localhost", self.port))
|
||||||
|
|
||||||
# Send request (old way)
|
# Send request (old way)
|
||||||
request = {
|
request = {"query": query, "top_k": top_k}
|
||||||
'query': query,
|
sock.send(json.dumps(request).encode("utf-8"))
|
||||||
'top_k': top_k
|
|
||||||
}
|
|
||||||
sock.send(json.dumps(request).encode('utf-8'))
|
|
||||||
|
|
||||||
# Receive response (accumulate until we get valid JSON)
|
# Receive response (accumulate until we get valid JSON)
|
||||||
data = b''
|
data = b""
|
||||||
while True:
|
while True:
|
||||||
chunk = sock.recv(65536)
|
chunk = sock.recv(65536)
|
||||||
if not chunk:
|
if not chunk:
|
||||||
@ -345,7 +330,7 @@ class RAGClient:
|
|||||||
data += chunk
|
data += chunk
|
||||||
try:
|
try:
|
||||||
# Try to decode as JSON
|
# Try to decode as JSON
|
||||||
response = json.loads(data.decode('utf-8'))
|
response = json.loads(data.decode("utf-8"))
|
||||||
sock.close()
|
sock.close()
|
||||||
return response
|
return response
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
@ -353,24 +338,18 @@ class RAGClient:
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
sock.close()
|
sock.close()
|
||||||
return {
|
return {"success": False, "error": "Incomplete response from server"}
|
||||||
'success': False,
|
|
||||||
'error': 'Incomplete response from server'
|
|
||||||
}
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return {
|
return {"success": False, "error": str(e)}
|
||||||
'success': False,
|
|
||||||
'error': str(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
def is_running(self) -> bool:
|
def is_running(self) -> bool:
|
||||||
"""Check if server is running."""
|
"""Check if server is running."""
|
||||||
try:
|
try:
|
||||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
result = sock.connect_ex(('localhost', self.port))
|
result = sock.connect_ex(("localhost", self.port))
|
||||||
sock.close()
|
sock.close()
|
||||||
return result == 0
|
return result == 0
|
||||||
except:
|
except (ConnectionError, OSError, TypeError, ValueError, socket.error):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
@ -389,12 +368,20 @@ def auto_start_if_needed(project_path: Path) -> Optional[subprocess.Popen]:
|
|||||||
if not client.is_running():
|
if not client.is_running():
|
||||||
# Start server in background
|
# Start server in background
|
||||||
import subprocess
|
import subprocess
|
||||||
cmd = [sys.executable, "-m", "mini_rag.cli", "server", "--path", str(project_path)]
|
|
||||||
|
cmd = [
|
||||||
|
sys.executable,
|
||||||
|
"-m",
|
||||||
|
"mini_rag.cli",
|
||||||
|
"server",
|
||||||
|
"--path",
|
||||||
|
str(project_path),
|
||||||
|
]
|
||||||
process = subprocess.Popen(
|
process = subprocess.Popen(
|
||||||
cmd,
|
cmd,
|
||||||
stdout=subprocess.PIPE,
|
stdout=subprocess.PIPE,
|
||||||
stderr=subprocess.PIPE,
|
stderr=subprocess.PIPE,
|
||||||
creationflags=subprocess.CREATE_NEW_CONSOLE if sys.platform == 'win32' else 0
|
creationflags=(subprocess.CREATE_NEW_CONSOLE if sys.platform == "win32" else 0),
|
||||||
)
|
)
|
||||||
|
|
||||||
# Wait for server to start
|
# Wait for server to start
|
||||||
|
|||||||
@ -3,61 +3,49 @@ Smart language-aware chunking strategies for FSS-Mini-RAG.
|
|||||||
Automatically adapts chunking based on file type and content patterns.
|
Automatically adapts chunking based on file type and content patterns.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Dict, Any, List
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import json
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
|
|
||||||
class SmartChunkingStrategy:
|
class SmartChunkingStrategy:
|
||||||
"""Intelligent chunking that adapts to file types and content."""
|
"""Intelligent chunking that adapts to file types and content."""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.language_configs = {
|
self.language_configs = {
|
||||||
'python': {
|
"python": {
|
||||||
'max_size': 3000, # Larger for better function context
|
"max_size": 3000, # Larger for better function context
|
||||||
'min_size': 200,
|
"min_size": 200,
|
||||||
'strategy': 'function',
|
"strategy": "function",
|
||||||
'prefer_semantic': True
|
"prefer_semantic": True,
|
||||||
},
|
},
|
||||||
'javascript': {
|
"javascript": {
|
||||||
'max_size': 2500,
|
"max_size": 2500,
|
||||||
'min_size': 150,
|
"min_size": 150,
|
||||||
'strategy': 'function',
|
"strategy": "function",
|
||||||
'prefer_semantic': True
|
"prefer_semantic": True,
|
||||||
},
|
},
|
||||||
'markdown': {
|
"markdown": {
|
||||||
'max_size': 2500,
|
"max_size": 2500,
|
||||||
'min_size': 300, # Larger minimum for complete thoughts
|
"min_size": 300, # Larger minimum for complete thoughts
|
||||||
'strategy': 'header',
|
"strategy": "header",
|
||||||
'preserve_structure': True
|
"preserve_structure": True,
|
||||||
},
|
},
|
||||||
'json': {
|
"json": {
|
||||||
'max_size': 1000, # Smaller for config files
|
"max_size": 1000, # Smaller for config files
|
||||||
'min_size': 50,
|
"min_size": 50,
|
||||||
'skip_if_large': True, # Skip huge config JSONs
|
"skip_if_large": True, # Skip huge config JSONs
|
||||||
'max_file_size': 50000 # 50KB limit
|
"max_file_size": 50000, # 50KB limit
|
||||||
},
|
},
|
||||||
'yaml': {
|
"yaml": {"max_size": 1500, "min_size": 100, "strategy": "key_block"},
|
||||||
'max_size': 1500,
|
"text": {"max_size": 2000, "min_size": 200, "strategy": "paragraph"},
|
||||||
'min_size': 100,
|
"bash": {"max_size": 1500, "min_size": 100, "strategy": "function"},
|
||||||
'strategy': 'key_block'
|
|
||||||
},
|
|
||||||
'text': {
|
|
||||||
'max_size': 2000,
|
|
||||||
'min_size': 200,
|
|
||||||
'strategy': 'paragraph'
|
|
||||||
},
|
|
||||||
'bash': {
|
|
||||||
'max_size': 1500,
|
|
||||||
'min_size': 100,
|
|
||||||
'strategy': 'function'
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Smart defaults for unknown languages
|
# Smart defaults for unknown languages
|
||||||
self.default_config = {
|
self.default_config = {
|
||||||
'max_size': 2000,
|
"max_size": 2000,
|
||||||
'min_size': 150,
|
"min_size": 150,
|
||||||
'strategy': 'semantic'
|
"strategy": "semantic",
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_config_for_language(self, language: str, file_size: int = 0) -> Dict[str, Any]:
|
def get_config_for_language(self, language: str, file_size: int = 0) -> Dict[str, Any]:
|
||||||
@ -67,10 +55,10 @@ class SmartChunkingStrategy:
|
|||||||
# Smart adjustments based on file size
|
# Smart adjustments based on file size
|
||||||
if file_size > 0:
|
if file_size > 0:
|
||||||
if file_size < 500: # Very small files
|
if file_size < 500: # Very small files
|
||||||
config['max_size'] = max(config['max_size'] // 2, 200)
|
config["max_size"] = max(config["max_size"] // 2, 200)
|
||||||
config['min_size'] = 50
|
config["min_size"] = 50
|
||||||
elif file_size > 20000: # Large files
|
elif file_size > 20000: # Large files
|
||||||
config['max_size'] = min(config['max_size'] + 1000, 4000)
|
config["max_size"] = min(config["max_size"] + 1000, 4000)
|
||||||
|
|
||||||
return config
|
return config
|
||||||
|
|
||||||
@ -79,8 +67,8 @@ class SmartChunkingStrategy:
|
|||||||
lang_config = self.language_configs.get(language, {})
|
lang_config = self.language_configs.get(language, {})
|
||||||
|
|
||||||
# Skip huge JSON config files
|
# Skip huge JSON config files
|
||||||
if language == 'json' and lang_config.get('skip_if_large'):
|
if language == "json" and lang_config.get("skip_if_large"):
|
||||||
max_size = lang_config.get('max_file_size', 50000)
|
max_size = lang_config.get("max_file_size", 50000)
|
||||||
if file_size > max_size:
|
if file_size > max_size:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@ -92,58 +80,62 @@ class SmartChunkingStrategy:
|
|||||||
|
|
||||||
def get_smart_defaults(self, project_stats: Dict[str, Any]) -> Dict[str, Any]:
|
def get_smart_defaults(self, project_stats: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""Generate smart defaults based on project language distribution."""
|
"""Generate smart defaults based on project language distribution."""
|
||||||
languages = project_stats.get('languages', {})
|
languages = project_stats.get("languages", {})
|
||||||
total_files = sum(languages.values())
|
# sum(languages.values()) # Unused variable removed
|
||||||
|
|
||||||
# Determine primary language
|
# Determine primary language
|
||||||
primary_lang = max(languages.items(), key=lambda x: x[1])[0] if languages else 'python'
|
primary_lang = max(languages.items(), key=lambda x: x[1])[0] if languages else "python"
|
||||||
primary_config = self.language_configs.get(primary_lang, self.default_config)
|
primary_config = self.language_configs.get(primary_lang, self.default_config)
|
||||||
|
|
||||||
# Smart streaming threshold based on large files
|
# Smart streaming threshold based on large files
|
||||||
large_files = project_stats.get('large_files', 0)
|
large_files = project_stats.get("large_files", 0)
|
||||||
streaming_threshold = 5120 if large_files > 5 else 1048576 # 5KB vs 1MB
|
streaming_threshold = 5120 if large_files > 5 else 1048576 # 5KB vs 1MB
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"chunking": {
|
"chunking": {
|
||||||
"max_size": primary_config['max_size'],
|
"max_size": primary_config["max_size"],
|
||||||
"min_size": primary_config['min_size'],
|
"min_size": primary_config["min_size"],
|
||||||
"strategy": primary_config.get('strategy', 'semantic'),
|
"strategy": primary_config.get("strategy", "semantic"),
|
||||||
"language_specific": {
|
"language_specific": {
|
||||||
lang: config for lang, config in self.language_configs.items()
|
lang: config
|
||||||
|
for lang, config in self.language_configs.items()
|
||||||
if languages.get(lang, 0) > 0
|
if languages.get(lang, 0) > 0
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
"streaming": {
|
"streaming": {
|
||||||
"enabled": True,
|
"enabled": True,
|
||||||
"threshold_bytes": streaming_threshold,
|
"threshold_bytes": streaming_threshold,
|
||||||
"chunk_size_kb": 64
|
"chunk_size_kb": 64,
|
||||||
},
|
},
|
||||||
"files": {
|
"files": {
|
||||||
"skip_tiny_files": True,
|
"skip_tiny_files": True,
|
||||||
"tiny_threshold": 30,
|
"tiny_threshold": 30,
|
||||||
"smart_json_filtering": True
|
"smart_json_filtering": True,
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
# Example usage
|
# Example usage
|
||||||
|
|
||||||
|
|
||||||
def analyze_and_suggest(manifest_data: Dict[str, Any]) -> Dict[str, Any]:
|
def analyze_and_suggest(manifest_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""Analyze project and suggest optimal configuration."""
|
"""Analyze project and suggest optimal configuration."""
|
||||||
from collections import Counter
|
from collections import Counter
|
||||||
|
|
||||||
files = manifest_data.get('files', {})
|
files = manifest_data.get("files", {})
|
||||||
languages = Counter()
|
languages = Counter()
|
||||||
large_files = 0
|
large_files = 0
|
||||||
|
|
||||||
for info in files.values():
|
for info in files.values():
|
||||||
lang = info.get('language', 'unknown')
|
lang = info.get("language", "unknown")
|
||||||
languages[lang] += 1
|
languages[lang] += 1
|
||||||
if info.get('size', 0) > 10000:
|
if info.get("size", 0) > 10000:
|
||||||
large_files += 1
|
large_files += 1
|
||||||
|
|
||||||
stats = {
|
stats = {
|
||||||
'languages': dict(languages),
|
"languages": dict(languages),
|
||||||
'large_files': large_files,
|
"large_files": large_files,
|
||||||
'total_files': len(files)
|
"total_files": len(files),
|
||||||
}
|
}
|
||||||
|
|
||||||
strategy = SmartChunkingStrategy()
|
strategy = SmartChunkingStrategy()
|
||||||
|
|||||||
@ -7,7 +7,6 @@ context-aware assistance without compromising privacy.
|
|||||||
|
|
||||||
import platform
|
import platform
|
||||||
import sys
|
import sys
|
||||||
import os
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Dict, Optional
|
from typing import Dict, Optional
|
||||||
|
|
||||||
@ -32,11 +31,9 @@ class SystemContextCollector:
|
|||||||
python_ver = f"{sys.version_info.major}.{sys.version_info.minor}"
|
python_ver = f"{sys.version_info.major}.{sys.version_info.minor}"
|
||||||
|
|
||||||
# Simplified OS names
|
# Simplified OS names
|
||||||
os_short = {
|
os_short = {"Windows": "Win", "Linux": "Linux", "Darwin": "macOS"}.get(
|
||||||
'Windows': 'Win',
|
os_name, os_name
|
||||||
'Linux': 'Linux',
|
)
|
||||||
'Darwin': 'macOS'
|
|
||||||
}.get(os_name, os_name)
|
|
||||||
|
|
||||||
# Working directory info
|
# Working directory info
|
||||||
if project_path:
|
if project_path:
|
||||||
@ -85,19 +82,19 @@ class SystemContextCollector:
|
|||||||
return {
|
return {
|
||||||
"launcher": "rag.bat",
|
"launcher": "rag.bat",
|
||||||
"index": "rag.bat index C:\\path\\to\\project",
|
"index": "rag.bat index C:\\path\\to\\project",
|
||||||
"search": "rag.bat search C:\\path\\to\\project \"query\"",
|
"search": 'rag.bat search C:\\path\\to\\project "query"',
|
||||||
"explore": "rag.bat explore C:\\path\\to\\project",
|
"explore": "rag.bat explore C:\\path\\to\\project",
|
||||||
"path_sep": "\\",
|
"path_sep": "\\",
|
||||||
"example_path": "C:\\Users\\username\\Documents\\myproject"
|
"example_path": "C:\\Users\\username\\Documents\\myproject",
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
return {
|
return {
|
||||||
"launcher": "./rag-mini",
|
"launcher": "./rag-mini",
|
||||||
"index": "./rag-mini index /path/to/project",
|
"index": "./rag-mini index /path/to/project",
|
||||||
"search": "./rag-mini search /path/to/project \"query\"",
|
"search": './rag-mini search /path/to/project "query"',
|
||||||
"explore": "./rag-mini explore /path/to/project",
|
"explore": "./rag-mini explore /path/to/project",
|
||||||
"path_sep": "/",
|
"path_sep": "/",
|
||||||
"example_path": "~/Documents/myproject"
|
"example_path": "~/Documents/myproject",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -112,6 +109,7 @@ def get_command_context() -> Dict[str, str]:
|
|||||||
|
|
||||||
|
|
||||||
# Test function
|
# Test function
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
print("System Context Test:")
|
print("System Context Test:")
|
||||||
print(f"Context: {get_system_context()}")
|
print(f"Context: {get_system_context()}")
|
||||||
|
|||||||
@ -6,30 +6,32 @@ Provides seamless GitHub-based updates with user-friendly interface.
|
|||||||
Checks for new releases, downloads updates, and handles installation safely.
|
Checks for new releases, downloads updates, and handles installation safely.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import json
|
import json
|
||||||
import time
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
import zipfile
|
|
||||||
import tempfile
|
|
||||||
import subprocess
|
import subprocess
|
||||||
from pathlib import Path
|
import sys
|
||||||
from typing import Optional, Dict, Any, Tuple
|
import tempfile
|
||||||
from datetime import datetime, timedelta
|
import zipfile
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional, Tuple
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
REQUESTS_AVAILABLE = True
|
REQUESTS_AVAILABLE = True
|
||||||
except ImportError:
|
except ImportError:
|
||||||
REQUESTS_AVAILABLE = False
|
REQUESTS_AVAILABLE = False
|
||||||
|
|
||||||
from .config import ConfigManager
|
from .config import ConfigManager
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class UpdateInfo:
|
class UpdateInfo:
|
||||||
"""Information about an available update."""
|
"""Information about an available update."""
|
||||||
|
|
||||||
version: str
|
version: str
|
||||||
release_url: str
|
release_url: str
|
||||||
download_url: str
|
download_url: str
|
||||||
@ -37,6 +39,7 @@ class UpdateInfo:
|
|||||||
published_at: str
|
published_at: str
|
||||||
is_newer: bool
|
is_newer: bool
|
||||||
|
|
||||||
|
|
||||||
class UpdateChecker:
|
class UpdateChecker:
|
||||||
"""
|
"""
|
||||||
Handles checking for and applying updates from GitHub releases.
|
Handles checking for and applying updates from GitHub releases.
|
||||||
@ -48,10 +51,12 @@ class UpdateChecker:
|
|||||||
- Provides graceful fallbacks if network unavailable
|
- Provides graceful fallbacks if network unavailable
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self,
|
def __init__(
|
||||||
|
self,
|
||||||
repo_owner: str = "FSSCoding",
|
repo_owner: str = "FSSCoding",
|
||||||
repo_name: str = "Fss-Mini-Rag",
|
repo_name: str = "Fss-Mini-Rag",
|
||||||
current_version: str = "2.1.0"):
|
current_version: str = "2.1.0",
|
||||||
|
):
|
||||||
self.repo_owner = repo_owner
|
self.repo_owner = repo_owner
|
||||||
self.repo_name = repo_name
|
self.repo_name = repo_name
|
||||||
self.current_version = current_version
|
self.current_version = current_version
|
||||||
@ -82,16 +87,20 @@ class UpdateChecker:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
# Check user preference
|
# Check user preference
|
||||||
if hasattr(self.config, 'updates') and not getattr(self.config.updates, 'auto_check', True):
|
if hasattr(self.config, "updates") and not getattr(
|
||||||
|
self.config.updates, "auto_check", True
|
||||||
|
):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Check if we've checked recently
|
# Check if we've checked recently
|
||||||
if self.cache_file.exists():
|
if self.cache_file.exists():
|
||||||
try:
|
try:
|
||||||
with open(self.cache_file, 'r') as f:
|
with open(self.cache_file, "r") as f:
|
||||||
cache = json.load(f)
|
cache = json.load(f)
|
||||||
last_check = datetime.fromisoformat(cache.get('last_check', '2020-01-01'))
|
last_check = datetime.fromisoformat(cache.get("last_check", "2020-01-01"))
|
||||||
if datetime.now() - last_check < timedelta(hours=self.check_frequency_hours):
|
if datetime.now() - last_check < timedelta(
|
||||||
|
hours=self.check_frequency_hours
|
||||||
|
):
|
||||||
return False
|
return False
|
||||||
except (json.JSONDecodeError, ValueError, KeyError):
|
except (json.JSONDecodeError, ValueError, KeyError):
|
||||||
pass # Ignore cache errors, will check anyway
|
pass # Ignore cache errors, will check anyway
|
||||||
@ -113,7 +122,7 @@ class UpdateChecker:
|
|||||||
response = requests.get(
|
response = requests.get(
|
||||||
f"{self.github_api_url}/releases/latest",
|
f"{self.github_api_url}/releases/latest",
|
||||||
timeout=10,
|
timeout=10,
|
||||||
headers={"Accept": "application/vnd.github.v3+json"}
|
headers={"Accept": "application/vnd.github.v3+json"},
|
||||||
)
|
)
|
||||||
|
|
||||||
if response.status_code != 200:
|
if response.status_code != 200:
|
||||||
@ -122,16 +131,16 @@ class UpdateChecker:
|
|||||||
release_data = response.json()
|
release_data = response.json()
|
||||||
|
|
||||||
# Extract version info
|
# Extract version info
|
||||||
latest_version = release_data.get('tag_name', '').lstrip('v')
|
latest_version = release_data.get("tag_name", "").lstrip("v")
|
||||||
release_notes = release_data.get('body', 'No release notes available.')
|
release_notes = release_data.get("body", "No release notes available.")
|
||||||
published_at = release_data.get('published_at', '')
|
published_at = release_data.get("published_at", "")
|
||||||
release_url = release_data.get('html_url', '')
|
release_url = release_data.get("html_url", "")
|
||||||
|
|
||||||
# Find download URL for source code
|
# Find download URL for source code
|
||||||
download_url = None
|
download_url = None
|
||||||
for asset in release_data.get('assets', []):
|
for asset in release_data.get("assets", []):
|
||||||
if asset.get('name', '').endswith('.zip'):
|
if asset.get("name", "").endswith(".zip"):
|
||||||
download_url = asset.get('browser_download_url')
|
download_url = asset.get("browser_download_url")
|
||||||
break
|
break
|
||||||
|
|
||||||
# Fallback to source code zip
|
# Fallback to source code zip
|
||||||
@ -151,10 +160,10 @@ class UpdateChecker:
|
|||||||
download_url=download_url,
|
download_url=download_url,
|
||||||
release_notes=release_notes,
|
release_notes=release_notes,
|
||||||
published_at=published_at,
|
published_at=published_at,
|
||||||
is_newer=True
|
is_newer=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception:
|
||||||
# Silently fail for network issues - don't interrupt user experience
|
# Silently fail for network issues - don't interrupt user experience
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@ -168,6 +177,7 @@ class UpdateChecker:
|
|||||||
- Major.Minor.Patch (e.g., 2.1.0)
|
- Major.Minor.Patch (e.g., 2.1.0)
|
||||||
- Major.Minor (e.g., 2.1)
|
- Major.Minor (e.g., 2.1)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def version_tuple(v):
|
def version_tuple(v):
|
||||||
return tuple(map(int, (v.split("."))))
|
return tuple(map(int, (v.split("."))))
|
||||||
|
|
||||||
@ -180,18 +190,20 @@ class UpdateChecker:
|
|||||||
def _update_cache(self, latest_version: str, is_newer: bool):
|
def _update_cache(self, latest_version: str, is_newer: bool):
|
||||||
"""Update the cache file with check results."""
|
"""Update the cache file with check results."""
|
||||||
cache_data = {
|
cache_data = {
|
||||||
'last_check': datetime.now().isoformat(),
|
"last_check": datetime.now().isoformat(),
|
||||||
'latest_version': latest_version,
|
"latest_version": latest_version,
|
||||||
'is_newer': is_newer
|
"is_newer": is_newer,
|
||||||
}
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with open(self.cache_file, 'w') as f:
|
with open(self.cache_file, "w") as f:
|
||||||
json.dump(cache_data, f, indent=2)
|
json.dump(cache_data, f, indent=2)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass # Ignore cache write errors
|
pass # Ignore cache write errors
|
||||||
|
|
||||||
def download_update(self, update_info: UpdateInfo, progress_callback=None) -> Optional[Path]:
|
def download_update(
|
||||||
|
self, update_info: UpdateInfo, progress_callback=None
|
||||||
|
) -> Optional[Path]:
|
||||||
"""
|
"""
|
||||||
Download the update package to a temporary location.
|
Download the update package to a temporary location.
|
||||||
|
|
||||||
@ -207,17 +219,17 @@ class UpdateChecker:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
# Create temporary file for download
|
# Create temporary file for download
|
||||||
with tempfile.NamedTemporaryFile(suffix='.zip', delete=False) as tmp_file:
|
with tempfile.NamedTemporaryFile(suffix=".zip", delete=False) as tmp_file:
|
||||||
tmp_path = Path(tmp_file.name)
|
tmp_path = Path(tmp_file.name)
|
||||||
|
|
||||||
# Download with progress tracking
|
# Download with progress tracking
|
||||||
response = requests.get(update_info.download_url, stream=True, timeout=30)
|
response = requests.get(update_info.download_url, stream=True, timeout=30)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
|
|
||||||
total_size = int(response.headers.get('content-length', 0))
|
total_size = int(response.headers.get("content-length", 0))
|
||||||
downloaded = 0
|
downloaded = 0
|
||||||
|
|
||||||
with open(tmp_path, 'wb') as f:
|
with open(tmp_path, "wb") as f:
|
||||||
for chunk in response.iter_content(chunk_size=8192):
|
for chunk in response.iter_content(chunk_size=8192):
|
||||||
if chunk:
|
if chunk:
|
||||||
f.write(chunk)
|
f.write(chunk)
|
||||||
@ -227,9 +239,9 @@ class UpdateChecker:
|
|||||||
|
|
||||||
return tmp_path
|
return tmp_path
|
||||||
|
|
||||||
except Exception as e:
|
except Exception:
|
||||||
# Clean up on error
|
# Clean up on error
|
||||||
if 'tmp_path' in locals() and tmp_path.exists():
|
if "tmp_path" in locals() and tmp_path.exists():
|
||||||
tmp_path.unlink()
|
tmp_path.unlink()
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@ -250,14 +262,14 @@ class UpdateChecker:
|
|||||||
|
|
||||||
# Copy key files and directories
|
# Copy key files and directories
|
||||||
important_items = [
|
important_items = [
|
||||||
'mini_rag',
|
"mini_rag",
|
||||||
'rag-mini.py',
|
"rag-mini.py",
|
||||||
'rag-tui.py',
|
"rag-tui.py",
|
||||||
'requirements.txt',
|
"requirements.txt",
|
||||||
'install_mini_rag.sh',
|
"install_mini_rag.sh",
|
||||||
'install_windows.bat',
|
"install_windows.bat",
|
||||||
'README.md',
|
"README.md",
|
||||||
'assets'
|
"assets",
|
||||||
]
|
]
|
||||||
|
|
||||||
for item in important_items:
|
for item in important_items:
|
||||||
@ -270,7 +282,7 @@ class UpdateChecker:
|
|||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
except Exception as e:
|
except Exception:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def apply_update(self, update_package_path: Path, update_info: UpdateInfo) -> bool:
|
def apply_update(self, update_package_path: Path, update_info: UpdateInfo) -> bool:
|
||||||
@ -290,7 +302,7 @@ class UpdateChecker:
|
|||||||
tmp_path = Path(tmp_dir)
|
tmp_path = Path(tmp_dir)
|
||||||
|
|
||||||
# Extract the archive
|
# Extract the archive
|
||||||
with zipfile.ZipFile(update_package_path, 'r') as zip_ref:
|
with zipfile.ZipFile(update_package_path, "r") as zip_ref:
|
||||||
zip_ref.extractall(tmp_path)
|
zip_ref.extractall(tmp_path)
|
||||||
|
|
||||||
# Find the extracted directory (may be nested)
|
# Find the extracted directory (may be nested)
|
||||||
@ -302,13 +314,13 @@ class UpdateChecker:
|
|||||||
|
|
||||||
# Copy files to application directory
|
# Copy files to application directory
|
||||||
important_items = [
|
important_items = [
|
||||||
'mini_rag',
|
"mini_rag",
|
||||||
'rag-mini.py',
|
"rag-mini.py",
|
||||||
'rag-tui.py',
|
"rag-tui.py",
|
||||||
'requirements.txt',
|
"requirements.txt",
|
||||||
'install_mini_rag.sh',
|
"install_mini_rag.sh",
|
||||||
'install_windows.bat',
|
"install_windows.bat",
|
||||||
'README.md'
|
"README.md",
|
||||||
]
|
]
|
||||||
|
|
||||||
for item in important_items:
|
for item in important_items:
|
||||||
@ -332,19 +344,19 @@ class UpdateChecker:
|
|||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
except Exception as e:
|
except Exception:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def _update_version_info(self, new_version: str):
|
def _update_version_info(self, new_version: str):
|
||||||
"""Update version information in the application."""
|
"""Update version information in the application."""
|
||||||
# Update __init__.py version
|
# Update __init__.py version
|
||||||
init_file = self.app_root / 'mini_rag' / '__init__.py'
|
init_file = self.app_root / "mini_rag" / "__init__.py"
|
||||||
if init_file.exists():
|
if init_file.exists():
|
||||||
try:
|
try:
|
||||||
content = init_file.read_text()
|
content = init_file.read_text()
|
||||||
updated_content = content.replace(
|
updated_content = content.replace(
|
||||||
f'__version__ = "{self.current_version}"',
|
f'__version__ = "{self.current_version}"',
|
||||||
f'__version__ = "{new_version}"'
|
f'__version__ = "{new_version}"',
|
||||||
)
|
)
|
||||||
init_file.write_text(updated_content)
|
init_file.write_text(updated_content)
|
||||||
except Exception:
|
except Exception:
|
||||||
@ -378,26 +390,26 @@ class UpdateChecker:
|
|||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
except Exception as e:
|
except Exception:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def restart_application(self):
|
def restart_application(self):
|
||||||
"""Restart the application after update."""
|
"""Restart the application after update."""
|
||||||
try:
|
try:
|
||||||
# Get the current script path
|
# Get the current script path
|
||||||
current_script = sys.argv[0]
|
# sys.argv[0] # Unused variable removed
|
||||||
|
|
||||||
# Restart with the same arguments
|
# Restart with the same arguments
|
||||||
if sys.platform.startswith('win'):
|
if sys.platform.startswith("win"):
|
||||||
# Windows
|
# Windows
|
||||||
subprocess.Popen([sys.executable] + sys.argv)
|
subprocess.Popen([sys.executable] + sys.argv)
|
||||||
else:
|
else:
|
||||||
# Unix-like systems
|
# Unix-like systems
|
||||||
os.execv(sys.executable, [sys.executable] + sys.argv)
|
os.execv(sys.executable, [sys.executable] + sys.argv)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception:
|
||||||
# If restart fails, just exit gracefully
|
# If restart fails, just exit gracefully
|
||||||
print(f"\n✅ Update complete! Please restart the application manually.")
|
print("\n✅ Update complete! Please restart the application manually.")
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
|
|
||||||
@ -411,10 +423,10 @@ def get_legacy_notification() -> Optional[str]:
|
|||||||
# Check if this is a very old version by looking for cache file
|
# Check if this is a very old version by looking for cache file
|
||||||
# Old versions won't have update cache, so we can detect them
|
# Old versions won't have update cache, so we can detect them
|
||||||
app_root = Path(__file__).parent.parent
|
app_root = Path(__file__).parent.parent
|
||||||
cache_file = app_root / ".update_cache.json"
|
# app_root / ".update_cache.json" # Unused variable removed
|
||||||
|
|
||||||
# Also check version in __init__.py to see if it's old
|
# Also check version in __init__.py to see if it's old
|
||||||
init_file = app_root / 'mini_rag' / '__init__.py'
|
init_file = app_root / "mini_rag" / "__init__.py"
|
||||||
if init_file.exists():
|
if init_file.exists():
|
||||||
content = init_file.read_text()
|
content = init_file.read_text()
|
||||||
if '__version__ = "2.0.' in content or '__version__ = "1.' in content:
|
if '__version__ = "2.0.' in content or '__version__ = "1.' in content:
|
||||||
@ -443,6 +455,7 @@ Your version of FSS-Mini-RAG is missing critical updates!
|
|||||||
# Global convenience functions
|
# Global convenience functions
|
||||||
_updater_instance = None
|
_updater_instance = None
|
||||||
|
|
||||||
|
|
||||||
def check_for_updates() -> Optional[UpdateInfo]:
|
def check_for_updates() -> Optional[UpdateInfo]:
|
||||||
"""Global function to check for updates."""
|
"""Global function to check for updates."""
|
||||||
global _updater_instance
|
global _updater_instance
|
||||||
@ -453,6 +466,7 @@ def check_for_updates() -> Optional[UpdateInfo]:
|
|||||||
return _updater_instance.check_for_updates()
|
return _updater_instance.check_for_updates()
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def get_updater() -> UpdateChecker:
|
def get_updater() -> UpdateChecker:
|
||||||
"""Get the global updater instance."""
|
"""Get the global updater instance."""
|
||||||
global _updater_instance
|
global _updater_instance
|
||||||
|
|||||||
@ -4,25 +4,27 @@ Virtual Environment Checker
|
|||||||
Ensures scripts run in proper Python virtual environment for consistency and safety.
|
Ensures scripts run in proper Python virtual environment for consistency and safety.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import sys
|
|
||||||
import os
|
import os
|
||||||
import sysconfig
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
def is_in_virtualenv() -> bool:
|
def is_in_virtualenv() -> bool:
|
||||||
"""Check if we're running in a virtual environment."""
|
"""Check if we're running in a virtual environment."""
|
||||||
# Check for virtual environment indicators
|
# Check for virtual environment indicators
|
||||||
return (
|
return (
|
||||||
hasattr(sys, 'real_prefix') or # virtualenv
|
hasattr(sys, "real_prefix")
|
||||||
(hasattr(sys, 'base_prefix') and sys.base_prefix != sys.prefix) or # venv/pyvenv
|
or (hasattr(sys, "base_prefix") and sys.base_prefix != sys.prefix) # virtualenv
|
||||||
os.environ.get('VIRTUAL_ENV') is not None # Environment variable
|
or os.environ.get("VIRTUAL_ENV") is not None # venv/pyvenv # Environment variable
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_expected_venv_path() -> Path:
|
def get_expected_venv_path() -> Path:
|
||||||
"""Get the expected virtual environment path for this project."""
|
"""Get the expected virtual environment path for this project."""
|
||||||
# Assume .venv in the same directory as the script
|
# Assume .venv in the same directory as the script
|
||||||
script_dir = Path(__file__).parent.parent
|
script_dir = Path(__file__).parent.parent
|
||||||
return script_dir / '.venv'
|
return script_dir / ".venv"
|
||||||
|
|
||||||
|
|
||||||
def check_correct_venv() -> tuple[bool, str]:
|
def check_correct_venv() -> tuple[bool, str]:
|
||||||
"""
|
"""
|
||||||
@ -38,16 +40,20 @@ def check_correct_venv() -> tuple[bool, str]:
|
|||||||
if not expected_venv.exists():
|
if not expected_venv.exists():
|
||||||
return False, "expected virtual environment not found"
|
return False, "expected virtual environment not found"
|
||||||
|
|
||||||
current_venv = os.environ.get('VIRTUAL_ENV')
|
current_venv = os.environ.get("VIRTUAL_ENV")
|
||||||
if current_venv:
|
if current_venv:
|
||||||
current_venv_path = Path(current_venv).resolve()
|
current_venv_path = Path(current_venv).resolve()
|
||||||
expected_venv_path = expected_venv.resolve()
|
expected_venv_path = expected_venv.resolve()
|
||||||
|
|
||||||
if current_venv_path != expected_venv_path:
|
if current_venv_path != expected_venv_path:
|
||||||
return False, f"wrong virtual environment (using {current_venv_path}, expected {expected_venv_path})"
|
return (
|
||||||
|
False,
|
||||||
|
f"wrong virtual environment (using {current_venv_path}, expected {expected_venv_path})",
|
||||||
|
)
|
||||||
|
|
||||||
return True, "correct virtual environment"
|
return True, "correct virtual environment"
|
||||||
|
|
||||||
|
|
||||||
def show_venv_warning(script_name: str = "script") -> None:
|
def show_venv_warning(script_name: str = "script") -> None:
|
||||||
"""Show virtual environment warning with helpful instructions."""
|
"""Show virtual environment warning with helpful instructions."""
|
||||||
expected_venv = get_expected_venv_path()
|
expected_venv = get_expected_venv_path()
|
||||||
@ -92,6 +98,7 @@ def show_venv_warning(script_name: str = "script") -> None:
|
|||||||
print(" • Potential system-wide package pollution")
|
print(" • Potential system-wide package pollution")
|
||||||
print()
|
print()
|
||||||
|
|
||||||
|
|
||||||
def check_and_warn_venv(script_name: str = "script", force_exit: bool = False) -> bool:
|
def check_and_warn_venv(script_name: str = "script", force_exit: bool = False) -> bool:
|
||||||
"""
|
"""
|
||||||
Check virtual environment and warn if needed.
|
Check virtual environment and warn if needed.
|
||||||
@ -119,11 +126,15 @@ def check_and_warn_venv(script_name: str = "script", force_exit: bool = False) -
|
|||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def require_venv(script_name: str = "script") -> None:
|
def require_venv(script_name: str = "script") -> None:
|
||||||
"""Require virtual environment or exit."""
|
"""Require virtual environment or exit."""
|
||||||
check_and_warn_venv(script_name, force_exit=True)
|
check_and_warn_venv(script_name, force_exit=True)
|
||||||
|
|
||||||
|
|
||||||
# Quick test function
|
# Quick test function
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
"""Test the virtual environment checker."""
|
"""Test the virtual environment checker."""
|
||||||
print("🧪 Virtual Environment Checker Test")
|
print("🧪 Virtual Environment Checker Test")
|
||||||
@ -138,5 +149,6 @@ def main():
|
|||||||
if not is_correct:
|
if not is_correct:
|
||||||
show_venv_warning("test script")
|
show_venv_warning("test script")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
@ -4,14 +4,21 @@ Monitors project files and updates the index incrementally.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import threading
|
|
||||||
import queue
|
import queue
|
||||||
|
import threading
|
||||||
import time
|
import time
|
||||||
from pathlib import Path
|
|
||||||
from typing import Set, Optional, Callable
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Callable, Optional, Set
|
||||||
|
|
||||||
|
from watchdog.events import (
|
||||||
|
FileCreatedEvent,
|
||||||
|
FileDeletedEvent,
|
||||||
|
FileModifiedEvent,
|
||||||
|
FileMovedEvent,
|
||||||
|
FileSystemEventHandler,
|
||||||
|
)
|
||||||
from watchdog.observers import Observer
|
from watchdog.observers import Observer
|
||||||
from watchdog.events import FileSystemEventHandler, FileModifiedEvent, FileCreatedEvent, FileDeletedEvent, FileMovedEvent
|
|
||||||
|
|
||||||
from .indexer import ProjectIndexer
|
from .indexer import ProjectIndexer
|
||||||
|
|
||||||
@ -73,11 +80,13 @@ class UpdateQueue:
|
|||||||
class CodeFileEventHandler(FileSystemEventHandler):
|
class CodeFileEventHandler(FileSystemEventHandler):
|
||||||
"""Handles file system events for code files."""
|
"""Handles file system events for code files."""
|
||||||
|
|
||||||
def __init__(self,
|
def __init__(
|
||||||
|
self,
|
||||||
update_queue: UpdateQueue,
|
update_queue: UpdateQueue,
|
||||||
include_patterns: Set[str],
|
include_patterns: Set[str],
|
||||||
exclude_patterns: Set[str],
|
exclude_patterns: Set[str],
|
||||||
project_path: Path):
|
project_path: Path,
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
Initialize event handler.
|
Initialize event handler.
|
||||||
|
|
||||||
@ -146,12 +155,14 @@ class CodeFileEventHandler(FileSystemEventHandler):
|
|||||||
class FileWatcher:
|
class FileWatcher:
|
||||||
"""Watches project files and updates index automatically."""
|
"""Watches project files and updates index automatically."""
|
||||||
|
|
||||||
def __init__(self,
|
def __init__(
|
||||||
|
self,
|
||||||
project_path: Path,
|
project_path: Path,
|
||||||
indexer: Optional[ProjectIndexer] = None,
|
indexer: Optional[ProjectIndexer] = None,
|
||||||
update_delay: float = 1.0,
|
update_delay: float = 1.0,
|
||||||
batch_size: int = 10,
|
batch_size: int = 10,
|
||||||
batch_timeout: float = 5.0):
|
batch_timeout: float = 5.0,
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
Initialize file watcher.
|
Initialize file watcher.
|
||||||
|
|
||||||
@ -180,10 +191,10 @@ class FileWatcher:
|
|||||||
|
|
||||||
# Statistics
|
# Statistics
|
||||||
self.stats = {
|
self.stats = {
|
||||||
'files_updated': 0,
|
"files_updated": 0,
|
||||||
'files_failed': 0,
|
"files_failed": 0,
|
||||||
'started_at': None,
|
"started_at": None,
|
||||||
'last_update': None,
|
"last_update": None,
|
||||||
}
|
}
|
||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
@ -199,27 +210,20 @@ class FileWatcher:
|
|||||||
self.update_queue,
|
self.update_queue,
|
||||||
self.include_patterns,
|
self.include_patterns,
|
||||||
self.exclude_patterns,
|
self.exclude_patterns,
|
||||||
self.project_path
|
self.project_path,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.observer.schedule(
|
self.observer.schedule(event_handler, str(self.project_path), recursive=True)
|
||||||
event_handler,
|
|
||||||
str(self.project_path),
|
|
||||||
recursive=True
|
|
||||||
)
|
|
||||||
|
|
||||||
# Start worker thread
|
# Start worker thread
|
||||||
self.running = True
|
self.running = True
|
||||||
self.worker_thread = threading.Thread(
|
self.worker_thread = threading.Thread(target=self._process_updates, daemon=True)
|
||||||
target=self._process_updates,
|
|
||||||
daemon=True
|
|
||||||
)
|
|
||||||
self.worker_thread.start()
|
self.worker_thread.start()
|
||||||
|
|
||||||
# Start observer
|
# Start observer
|
||||||
self.observer.start()
|
self.observer.start()
|
||||||
|
|
||||||
self.stats['started_at'] = datetime.now()
|
self.stats["started_at"] = datetime.now()
|
||||||
logger.info("File watcher started successfully")
|
logger.info("File watcher started successfully")
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
@ -315,27 +319,29 @@ class FileWatcher:
|
|||||||
success = self.indexer.delete_file(file_path)
|
success = self.indexer.delete_file(file_path)
|
||||||
|
|
||||||
if success:
|
if success:
|
||||||
self.stats['files_updated'] += 1
|
self.stats["files_updated"] += 1
|
||||||
else:
|
else:
|
||||||
self.stats['files_failed'] += 1
|
self.stats["files_failed"] += 1
|
||||||
|
|
||||||
self.stats['last_update'] = datetime.now()
|
self.stats["last_update"] = datetime.now()
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to process {file_path}: {e}")
|
logger.error(f"Failed to process {file_path}: {e}")
|
||||||
self.stats['files_failed'] += 1
|
self.stats["files_failed"] += 1
|
||||||
|
|
||||||
logger.info(f"Batch processing complete. Updated: {self.stats['files_updated']}, Failed: {self.stats['files_failed']}")
|
logger.info(
|
||||||
|
f"Batch processing complete. Updated: {self.stats['files_updated']}, Failed: {self.stats['files_failed']}"
|
||||||
|
)
|
||||||
|
|
||||||
def get_statistics(self) -> dict:
|
def get_statistics(self) -> dict:
|
||||||
"""Get watcher statistics."""
|
"""Get watcher statistics."""
|
||||||
stats = self.stats.copy()
|
stats = self.stats.copy()
|
||||||
stats['queue_size'] = self.update_queue.size()
|
stats["queue_size"] = self.update_queue.size()
|
||||||
stats['is_running'] = self.running
|
stats["is_running"] = self.running
|
||||||
|
|
||||||
if stats['started_at']:
|
if stats["started_at"]:
|
||||||
uptime = datetime.now() - stats['started_at']
|
uptime = datetime.now() - stats["started_at"]
|
||||||
stats['uptime_seconds'] = uptime.total_seconds()
|
stats["uptime_seconds"] = uptime.total_seconds()
|
||||||
|
|
||||||
return stats
|
return stats
|
||||||
|
|
||||||
@ -371,6 +377,8 @@ class FileWatcher:
|
|||||||
|
|
||||||
|
|
||||||
# Convenience function
|
# Convenience function
|
||||||
|
|
||||||
|
|
||||||
def watch_project(project_path: Path, callback: Optional[Callable] = None):
|
def watch_project(project_path: Path, callback: Optional[Callable] = None):
|
||||||
"""
|
"""
|
||||||
Watch a project for changes and update index automatically.
|
Watch a project for changes and update index automatically.
|
||||||
|
|||||||
@ -3,9 +3,9 @@ Windows Console Unicode/Emoji Fix
|
|||||||
Reliable Windows console Unicode/emoji support for 2025.
|
Reliable Windows console Unicode/emoji support for 2025.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
import io
|
import io
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
def fix_windows_console():
|
def fix_windows_console():
|
||||||
@ -14,28 +14,33 @@ def fix_windows_console():
|
|||||||
Call this at the start of any script that needs to output Unicode/emojis.
|
Call this at the start of any script that needs to output Unicode/emojis.
|
||||||
"""
|
"""
|
||||||
# Set environment variable for UTF-8 mode
|
# Set environment variable for UTF-8 mode
|
||||||
os.environ['PYTHONUTF8'] = '1'
|
os.environ["PYTHONUTF8"] = "1"
|
||||||
|
|
||||||
# For Python 3.7+
|
# For Python 3.7+
|
||||||
if hasattr(sys.stdout, 'reconfigure'):
|
if hasattr(sys.stdout, "reconfigure"):
|
||||||
sys.stdout.reconfigure(encoding='utf-8')
|
sys.stdout.reconfigure(encoding="utf-8")
|
||||||
sys.stderr.reconfigure(encoding='utf-8')
|
sys.stderr.reconfigure(encoding="utf-8")
|
||||||
if hasattr(sys.stdin, 'reconfigure'):
|
if hasattr(sys.stdin, "reconfigure"):
|
||||||
sys.stdin.reconfigure(encoding='utf-8')
|
sys.stdin.reconfigure(encoding="utf-8")
|
||||||
else:
|
else:
|
||||||
# For older Python versions
|
# For older Python versions
|
||||||
if sys.platform == 'win32':
|
if sys.platform == "win32":
|
||||||
# Replace streams with UTF-8 versions
|
# Replace streams with UTF-8 versions
|
||||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', line_buffering=True)
|
sys.stdout = io.TextIOWrapper(
|
||||||
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', line_buffering=True)
|
sys.stdout.buffer, encoding="utf-8", line_buffering=True
|
||||||
|
)
|
||||||
|
sys.stderr = io.TextIOWrapper(
|
||||||
|
sys.stderr.buffer, encoding="utf-8", line_buffering=True
|
||||||
|
)
|
||||||
|
|
||||||
# Also set the console code page to UTF-8 on Windows
|
# Also set the console code page to UTF-8 on Windows
|
||||||
if sys.platform == 'win32':
|
if sys.platform == "win32":
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Set console to UTF-8 code page
|
# Set console to UTF-8 code page
|
||||||
subprocess.run(['chcp', '65001'], shell=True, capture_output=True)
|
subprocess.run(["chcp", "65001"], shell=True, capture_output=True)
|
||||||
except:
|
except (OSError, subprocess.SubprocessError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@ -44,6 +49,8 @@ fix_windows_console()
|
|||||||
|
|
||||||
|
|
||||||
# Test function to verify it works
|
# Test function to verify it works
|
||||||
|
|
||||||
|
|
||||||
def test_emojis():
|
def test_emojis():
|
||||||
"""Test that emojis work properly."""
|
"""Test that emojis work properly."""
|
||||||
print("Testing emoji output:")
|
print("Testing emoji output:")
|
||||||
|
|||||||
34
pyproject.toml
Normal file
34
pyproject.toml
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
[tool.isort]
|
||||||
|
profile = "black"
|
||||||
|
line_length = 95
|
||||||
|
multi_line_output = 3
|
||||||
|
include_trailing_comma = true
|
||||||
|
force_grid_wrap = 0
|
||||||
|
use_parentheses = true
|
||||||
|
ensure_newline_before_comments = true
|
||||||
|
src_paths = ["mini_rag", "tests", "examples", "scripts"]
|
||||||
|
known_first_party = ["mini_rag"]
|
||||||
|
sections = ["FUTURE", "STDLIB", "THIRDPARTY", "FIRSTPARTY", "LOCALFOLDER"]
|
||||||
|
skip = [".venv", ".venv-linting", "__pycache__", ".git"]
|
||||||
|
skip_glob = ["*.egg-info/*", "build/*", "dist/*"]
|
||||||
|
|
||||||
|
[tool.black]
|
||||||
|
line-length = 95
|
||||||
|
target-version = ['py310']
|
||||||
|
include = '\.pyi?$'
|
||||||
|
extend-exclude = '''
|
||||||
|
/(
|
||||||
|
# directories
|
||||||
|
\.eggs
|
||||||
|
| \.git
|
||||||
|
| \.hg
|
||||||
|
| \.mypy_cache
|
||||||
|
| \.tox
|
||||||
|
| \.venv
|
||||||
|
| \.venv-linting
|
||||||
|
| _build
|
||||||
|
| buck-out
|
||||||
|
| build
|
||||||
|
| dist
|
||||||
|
)/
|
||||||
|
'''
|
||||||
2
rag-mini
2
rag-mini
@ -329,7 +329,7 @@ main() {
|
|||||||
;;
|
;;
|
||||||
"index"|"search"|"explore"|"status"|"update"|"check-update")
|
"index"|"search"|"explore"|"status"|"update"|"check-update")
|
||||||
# Direct CLI commands - call Python script
|
# Direct CLI commands - call Python script
|
||||||
exec "$PYTHON" "$SCRIPT_DIR/rag-mini.py" "$@"
|
exec "$PYTHON" "$SCRIPT_DIR/bin/rag-mini.py" "$@"
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
# Unknown command - show help
|
# Unknown command - show help
|
||||||
|
|||||||
2
rag-tui
2
rag-tui
@ -19,4 +19,4 @@ if [ ! -f "$PYTHON" ]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# Launch TUI
|
# Launch TUI
|
||||||
exec "$PYTHON" "$SCRIPT_DIR/rag-tui.py" "$@"
|
exec "$PYTHON" "$SCRIPT_DIR/bin/rag-tui.py" "$@"
|
||||||
@ -6,20 +6,20 @@ Converts a project to use the auto-update template system.
|
|||||||
This script helps migrate projects from Gitea to GitHub with auto-update capability.
|
This script helps migrate projects from Gitea to GitHub with auto-update capability.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import argparse
|
||||||
import sys
|
|
||||||
import json
|
import json
|
||||||
import shutil
|
import shutil
|
||||||
import argparse
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Dict, Any, Optional
|
from typing import Dict, Optional
|
||||||
|
|
||||||
|
|
||||||
def setup_project_template(
|
def setup_project_template(
|
||||||
project_path: Path,
|
project_path: Path,
|
||||||
repo_owner: str,
|
repo_owner: str,
|
||||||
repo_name: str,
|
repo_name: str,
|
||||||
project_type: str = "python",
|
project_type: str = "python",
|
||||||
include_auto_update: bool = True
|
include_auto_update: bool = True,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""
|
"""
|
||||||
Setup a project to use the GitHub auto-update template system.
|
Setup a project to use the GitHub auto-update template system.
|
||||||
@ -82,13 +82,14 @@ def setup_project_template(
|
|||||||
print(f"❌ Setup failed: {e}")
|
print(f"❌ Setup failed: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def setup_workflows(workflows_dir: Path, repo_owner: str, repo_name: str, project_type: str):
|
def setup_workflows(workflows_dir: Path, repo_owner: str, repo_name: str, project_type: str):
|
||||||
"""Setup GitHub Actions workflow files."""
|
"""Setup GitHub Actions workflow files."""
|
||||||
|
|
||||||
print("🔧 Setting up GitHub Actions workflows...")
|
print("🔧 Setting up GitHub Actions workflows...")
|
||||||
|
|
||||||
# Release workflow
|
# Release workflow
|
||||||
release_workflow = f"""name: Auto Release & Update System
|
release_workflow = """name: Auto Release & Update System
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
@ -186,7 +187,7 @@ jobs:
|
|||||||
|
|
||||||
# CI workflow for Python projects
|
# CI workflow for Python projects
|
||||||
if project_type == "python":
|
if project_type == "python":
|
||||||
ci_workflow = f"""name: CI/CD Pipeline
|
ci_workflow = """name: CI/CD Pipeline
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [ main, develop ]
|
branches: [ main, develop ]
|
||||||
@ -234,6 +235,7 @@ jobs:
|
|||||||
|
|
||||||
print(" ✅ GitHub Actions workflows created")
|
print(" ✅ GitHub Actions workflows created")
|
||||||
|
|
||||||
|
|
||||||
def setup_auto_update_system(project_path: Path, repo_owner: str, repo_name: str):
|
def setup_auto_update_system(project_path: Path, repo_owner: str, repo_name: str):
|
||||||
"""Setup the auto-update system for the project."""
|
"""Setup the auto-update system for the project."""
|
||||||
|
|
||||||
@ -244,7 +246,7 @@ def setup_auto_update_system(project_path: Path, repo_owner: str, repo_name: str
|
|||||||
|
|
||||||
if template_updater.exists():
|
if template_updater.exists():
|
||||||
# Create project module directory if needed
|
# Create project module directory if needed
|
||||||
module_name = repo_name.replace('-', '_')
|
module_name = repo_name.replace("-", "_")
|
||||||
module_dir = project_path / module_name
|
module_dir = project_path / module_name
|
||||||
module_dir.mkdir(exist_ok=True)
|
module_dir.mkdir(exist_ok=True)
|
||||||
|
|
||||||
@ -254,8 +256,12 @@ def setup_auto_update_system(project_path: Path, repo_owner: str, repo_name: str
|
|||||||
|
|
||||||
# Customize for this project
|
# Customize for this project
|
||||||
content = target_updater.read_text()
|
content = target_updater.read_text()
|
||||||
content = content.replace('repo_owner: str = "FSSCoding"', f'repo_owner: str = "{repo_owner}"')
|
content = content.replace(
|
||||||
content = content.replace('repo_name: str = "Fss-Mini-Rag"', f'repo_name: str = "{repo_name}"')
|
'repo_owner: str = "FSSCoding"', f'repo_owner: str = "{repo_owner}"'
|
||||||
|
)
|
||||||
|
content = content.replace(
|
||||||
|
'repo_name: str = "Fss-Mini-Rag"', f'repo_name: str = "{repo_name}"'
|
||||||
|
)
|
||||||
target_updater.write_text(content)
|
target_updater.write_text(content)
|
||||||
|
|
||||||
# Update __init__.py to include updater
|
# Update __init__.py to include updater
|
||||||
@ -277,6 +283,7 @@ except ImportError:
|
|||||||
else:
|
else:
|
||||||
print(" ⚠️ Template updater not found, you'll need to implement manually")
|
print(" ⚠️ Template updater not found, you'll need to implement manually")
|
||||||
|
|
||||||
|
|
||||||
def setup_issue_templates(templates_dir: Path):
|
def setup_issue_templates(templates_dir: Path):
|
||||||
"""Setup GitHub issue templates."""
|
"""Setup GitHub issue templates."""
|
||||||
|
|
||||||
@ -340,7 +347,10 @@ Add any other context or screenshots about the feature request here.
|
|||||||
|
|
||||||
print(" ✅ Issue templates created")
|
print(" ✅ Issue templates created")
|
||||||
|
|
||||||
def setup_project_config(project_path: Path, repo_owner: str, repo_name: str, include_auto_update: bool):
|
|
||||||
|
def setup_project_config(
|
||||||
|
project_path: Path, repo_owner: str, repo_name: str, include_auto_update: bool
|
||||||
|
):
|
||||||
"""Setup project configuration file."""
|
"""Setup project configuration file."""
|
||||||
|
|
||||||
print("⚙️ Setting up project configuration...")
|
print("⚙️ Setting up project configuration...")
|
||||||
@ -350,21 +360,22 @@ def setup_project_config(project_path: Path, repo_owner: str, repo_name: str, in
|
|||||||
"name": repo_name,
|
"name": repo_name,
|
||||||
"owner": repo_owner,
|
"owner": repo_owner,
|
||||||
"github_url": f"https://github.com/{repo_owner}/{repo_name}",
|
"github_url": f"https://github.com/{repo_owner}/{repo_name}",
|
||||||
"auto_update_enabled": include_auto_update
|
"auto_update_enabled": include_auto_update,
|
||||||
},
|
},
|
||||||
"github": {
|
"github": {
|
||||||
"template_version": "1.0.0",
|
"template_version": "1.0.0",
|
||||||
"last_sync": None,
|
"last_sync": None,
|
||||||
"workflows_enabled": True
|
"workflows_enabled": True,
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
config_file = project_path / ".github" / "project-config.json"
|
config_file = project_path / ".github" / "project-config.json"
|
||||||
with open(config_file, 'w') as f:
|
with open(config_file, "w") as f:
|
||||||
json.dump(config, f, indent=2)
|
json.dump(config, f, indent=2)
|
||||||
|
|
||||||
print(" ✅ Project configuration created")
|
print(" ✅ Project configuration created")
|
||||||
|
|
||||||
|
|
||||||
def setup_readme_template(project_path: Path, repo_owner: str, repo_name: str):
|
def setup_readme_template(project_path: Path, repo_owner: str, repo_name: str):
|
||||||
"""Setup README template if one doesn't exist."""
|
"""Setup README template if one doesn't exist."""
|
||||||
|
|
||||||
@ -373,7 +384,7 @@ def setup_readme_template(project_path: Path, repo_owner: str, repo_name: str):
|
|||||||
if not readme_file.exists():
|
if not readme_file.exists():
|
||||||
print("📖 Creating README template...")
|
print("📖 Creating README template...")
|
||||||
|
|
||||||
readme_content = f"""# {repo_name}
|
readme_content = """# {repo_name}
|
||||||
|
|
||||||
> A brief description of your project
|
> A brief description of your project
|
||||||
|
|
||||||
@ -445,6 +456,7 @@ This project includes automatic update checking:
|
|||||||
readme_file.write_text(readme_content)
|
readme_file.write_text(readme_content)
|
||||||
print(" ✅ README template created")
|
print(" ✅ README template created")
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
"""Main entry point."""
|
"""Main entry point."""
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
@ -454,16 +466,21 @@ def main():
|
|||||||
Examples:
|
Examples:
|
||||||
python setup-github-template.py myproject --owner username --name my-project
|
python setup-github-template.py myproject --owner username --name my-project
|
||||||
python setup-github-template.py /path/to/project --owner org --name cool-tool --no-auto-update
|
python setup-github-template.py /path/to/project --owner org --name cool-tool --no-auto-update
|
||||||
"""
|
""",
|
||||||
)
|
)
|
||||||
|
|
||||||
parser.add_argument('project_path', type=Path, help='Path to project directory')
|
parser.add_argument("project_path", type=Path, help="Path to project directory")
|
||||||
parser.add_argument('--owner', required=True, help='GitHub username or organization')
|
parser.add_argument("--owner", required=True, help="GitHub username or organization")
|
||||||
parser.add_argument('--name', required=True, help='GitHub repository name')
|
parser.add_argument("--name", required=True, help="GitHub repository name")
|
||||||
parser.add_argument('--type', choices=['python', 'general'], default='python',
|
parser.add_argument(
|
||||||
help='Project type (default: python)')
|
"--type",
|
||||||
parser.add_argument('--no-auto-update', action='store_true',
|
choices=["python", "general"],
|
||||||
help='Disable auto-update system')
|
default="python",
|
||||||
|
help="Project type (default: python)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--no-auto-update", action="store_true", help="Disable auto-update system"
|
||||||
|
)
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
@ -476,10 +493,11 @@ Examples:
|
|||||||
repo_owner=args.owner,
|
repo_owner=args.owner,
|
||||||
repo_name=args.name,
|
repo_name=args.name,
|
||||||
project_type=args.type,
|
project_type=args.type,
|
||||||
include_auto_update=not args.no_auto_update
|
include_auto_update=not args.no_auto_update,
|
||||||
)
|
)
|
||||||
|
|
||||||
sys.exit(0 if success else 1)
|
sys.exit(0 if success else 1)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
@ -4,62 +4,69 @@ Test script to validate all config examples are syntactically correct
|
|||||||
and contain required fields for FSS-Mini-RAG.
|
and contain required fields for FSS-Mini-RAG.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import yaml
|
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Dict, Any, List
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
|
||||||
def validate_config_structure(config: Dict[str, Any], config_name: str) -> List[str]:
|
def validate_config_structure(config: Dict[str, Any], config_name: str) -> List[str]:
|
||||||
"""Validate that config has required structure."""
|
"""Validate that config has required structure."""
|
||||||
errors = []
|
errors = []
|
||||||
|
|
||||||
# Required sections
|
# Required sections
|
||||||
required_sections = ['chunking', 'streaming', 'files', 'embedding', 'search']
|
required_sections = ["chunking", "streaming", "files", "embedding", "search"]
|
||||||
for section in required_sections:
|
for section in required_sections:
|
||||||
if section not in config:
|
if section not in config:
|
||||||
errors.append(f"{config_name}: Missing required section '{section}'")
|
errors.append(f"{config_name}: Missing required section '{section}'")
|
||||||
|
|
||||||
# Validate chunking section
|
# Validate chunking section
|
||||||
if 'chunking' in config:
|
if "chunking" in config:
|
||||||
chunking = config['chunking']
|
chunking = config["chunking"]
|
||||||
required_chunking = ['max_size', 'min_size', 'strategy']
|
required_chunking = ["max_size", "min_size", "strategy"]
|
||||||
for field in required_chunking:
|
for field in required_chunking:
|
||||||
if field not in chunking:
|
if field not in chunking:
|
||||||
errors.append(f"{config_name}: Missing chunking.{field}")
|
errors.append(f"{config_name}: Missing chunking.{field}")
|
||||||
|
|
||||||
# Validate types and ranges
|
# Validate types and ranges
|
||||||
if 'max_size' in chunking and not isinstance(chunking['max_size'], int):
|
if "max_size" in chunking and not isinstance(chunking["max_size"], int):
|
||||||
errors.append(f"{config_name}: chunking.max_size must be integer")
|
errors.append(f"{config_name}: chunking.max_size must be integer")
|
||||||
if 'min_size' in chunking and not isinstance(chunking['min_size'], int):
|
if "min_size" in chunking and not isinstance(chunking["min_size"], int):
|
||||||
errors.append(f"{config_name}: chunking.min_size must be integer")
|
errors.append(f"{config_name}: chunking.min_size must be integer")
|
||||||
if 'strategy' in chunking and chunking['strategy'] not in ['semantic', 'fixed']:
|
if "strategy" in chunking and chunking["strategy"] not in ["semantic", "fixed"]:
|
||||||
errors.append(f"{config_name}: chunking.strategy must be 'semantic' or 'fixed'")
|
errors.append(f"{config_name}: chunking.strategy must be 'semantic' or 'fixed'")
|
||||||
|
|
||||||
# Validate embedding section
|
# Validate embedding section
|
||||||
if 'embedding' in config:
|
if "embedding" in config:
|
||||||
embedding = config['embedding']
|
embedding = config["embedding"]
|
||||||
if 'preferred_method' in embedding:
|
if "preferred_method" in embedding:
|
||||||
valid_methods = ['ollama', 'ml', 'hash', 'auto']
|
valid_methods = ["ollama", "ml", "hash", "auto"]
|
||||||
if embedding['preferred_method'] not in valid_methods:
|
if embedding["preferred_method"] not in valid_methods:
|
||||||
errors.append(f"{config_name}: embedding.preferred_method must be one of {valid_methods}")
|
errors.append(
|
||||||
|
f"{config_name}: embedding.preferred_method must be one of {valid_methods}"
|
||||||
|
)
|
||||||
|
|
||||||
# Validate LLM section (if present)
|
# Validate LLM section (if present)
|
||||||
if 'llm' in config:
|
if "llm" in config:
|
||||||
llm = config['llm']
|
llm = config["llm"]
|
||||||
if 'synthesis_temperature' in llm:
|
if "synthesis_temperature" in llm:
|
||||||
temp = llm['synthesis_temperature']
|
temp = llm["synthesis_temperature"]
|
||||||
if not isinstance(temp, (int, float)) or temp < 0 or temp > 1:
|
if not isinstance(temp, (int, float)) or temp < 0 or temp > 1:
|
||||||
errors.append(f"{config_name}: llm.synthesis_temperature must be number between 0-1")
|
errors.append(
|
||||||
|
f"{config_name}: llm.synthesis_temperature must be number between 0-1"
|
||||||
|
)
|
||||||
|
|
||||||
return errors
|
return errors
|
||||||
|
|
||||||
|
|
||||||
def test_config_file(config_path: Path) -> bool:
|
def test_config_file(config_path: Path) -> bool:
|
||||||
"""Test a single config file."""
|
"""Test a single config file."""
|
||||||
print(f"Testing {config_path.name}...")
|
print(f"Testing {config_path.name}...")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Test YAML parsing
|
# Test YAML parsing
|
||||||
with open(config_path, 'r') as f:
|
with open(config_path, "r") as f:
|
||||||
config = yaml.safe_load(f)
|
config = yaml.safe_load(f)
|
||||||
|
|
||||||
if not config:
|
if not config:
|
||||||
@ -85,18 +92,19 @@ def test_config_file(config_path: Path) -> bool:
|
|||||||
print(f" ❌ {config_path.name}: Unexpected error: {e}")
|
print(f" ❌ {config_path.name}: Unexpected error: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
"""Test all config examples."""
|
"""Test all config examples."""
|
||||||
script_dir = Path(__file__).parent
|
script_dir = Path(__file__).parent
|
||||||
project_root = script_dir.parent
|
project_root = script_dir.parent
|
||||||
examples_dir = project_root / 'examples'
|
examples_dir = project_root / "examples"
|
||||||
|
|
||||||
if not examples_dir.exists():
|
if not examples_dir.exists():
|
||||||
print(f"❌ Examples directory not found: {examples_dir}")
|
print(f"❌ Examples directory not found: {examples_dir}")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
# Find all config files
|
# Find all config files
|
||||||
config_files = list(examples_dir.glob('config*.yaml'))
|
config_files = list(examples_dir.glob("config*.yaml"))
|
||||||
|
|
||||||
if not config_files:
|
if not config_files:
|
||||||
print(f"❌ No config files found in {examples_dir}")
|
print(f"❌ No config files found in {examples_dir}")
|
||||||
@ -120,5 +128,6 @@ def main():
|
|||||||
print("❌ Some config files have issues - please fix before release")
|
print("❌ Some config files have issues - please fix before release")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
@ -14,25 +14,31 @@ import sys
|
|||||||
import tempfile
|
import tempfile
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
from mini_rag.chunker import CodeChunker
|
||||||
|
from mini_rag.indexer import ProjectIndexer
|
||||||
|
from mini_rag.ollama_embeddings import OllamaEmbedder as CodeEmbedder
|
||||||
|
from mini_rag.search import CodeSearcher
|
||||||
|
|
||||||
# Check if virtual environment is activated
|
# Check if virtual environment is activated
|
||||||
|
|
||||||
|
|
||||||
def check_venv():
|
def check_venv():
|
||||||
if 'VIRTUAL_ENV' not in os.environ:
|
if "VIRTUAL_ENV" not in os.environ:
|
||||||
print("⚠️ WARNING: Virtual environment not detected!")
|
print("⚠️ WARNING: Virtual environment not detected!")
|
||||||
print(" This test requires the virtual environment to be activated.")
|
print(" This test requires the virtual environment to be activated.")
|
||||||
print(" Run: source .venv/bin/activate && PYTHONPATH=. python tests/01_basic_integration_test.py")
|
print(
|
||||||
|
" Run: source .venv/bin/activate && PYTHONPATH=. python tests/01_basic_integration_test.py"
|
||||||
|
)
|
||||||
print(" Continuing anyway...\n")
|
print(" Continuing anyway...\n")
|
||||||
|
|
||||||
|
|
||||||
check_venv()
|
check_venv()
|
||||||
|
|
||||||
# Fix Windows encoding
|
# Fix Windows encoding
|
||||||
if sys.platform == 'win32':
|
if sys.platform == "win32":
|
||||||
os.environ['PYTHONUTF8'] = '1'
|
os.environ["PYTHONUTF8"] = "1"
|
||||||
sys.stdout.reconfigure(encoding='utf-8')
|
sys.stdout.reconfigure(encoding="utf-8")
|
||||||
|
|
||||||
from mini_rag.chunker import CodeChunker
|
|
||||||
from mini_rag.indexer import ProjectIndexer
|
|
||||||
from mini_rag.search import CodeSearcher
|
|
||||||
from mini_rag.ollama_embeddings import OllamaEmbedder as CodeEmbedder
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
print("=" * 60)
|
print("=" * 60)
|
||||||
@ -46,13 +52,15 @@ def main():
|
|||||||
print("\n1. Creating sample project files...")
|
print("\n1. Creating sample project files...")
|
||||||
|
|
||||||
# Main calculator module
|
# Main calculator module
|
||||||
(project_path / "calculator.py").write_text('''"""
|
(project_path / "calculator.py").write_text(
|
||||||
|
'''"""
|
||||||
Advanced calculator module with various mathematical operations.
|
Advanced calculator module with various mathematical operations.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import math
|
import math
|
||||||
from typing import List, Union
|
from typing import List, Union
|
||||||
|
|
||||||
|
|
||||||
class BasicCalculator:
|
class BasicCalculator:
|
||||||
"""Basic calculator with fundamental operations."""
|
"""Basic calculator with fundamental operations."""
|
||||||
|
|
||||||
@ -91,6 +99,7 @@ class BasicCalculator:
|
|||||||
self.last_result = result
|
self.last_result = result
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
class ScientificCalculator(BasicCalculator):
|
class ScientificCalculator(BasicCalculator):
|
||||||
"""Scientific calculator extending basic operations."""
|
"""Scientific calculator extending basic operations."""
|
||||||
|
|
||||||
@ -123,6 +132,7 @@ def calculate_mean(numbers: List[float]) -> float:
|
|||||||
return 0.0
|
return 0.0
|
||||||
return sum(numbers) / len(numbers)
|
return sum(numbers) / len(numbers)
|
||||||
|
|
||||||
|
|
||||||
def calculate_median(numbers: List[float]) -> float:
|
def calculate_median(numbers: List[float]) -> float:
|
||||||
"""Calculate median of a list of numbers."""
|
"""Calculate median of a list of numbers."""
|
||||||
if not numbers:
|
if not numbers:
|
||||||
@ -133,6 +143,7 @@ def calculate_median(numbers: List[float]) -> float:
|
|||||||
return (sorted_nums[n//2-1] + sorted_nums[n//2]) / 2
|
return (sorted_nums[n//2-1] + sorted_nums[n//2]) / 2
|
||||||
return sorted_nums[n//2]
|
return sorted_nums[n//2]
|
||||||
|
|
||||||
|
|
||||||
def calculate_mode(numbers: List[float]) -> float:
|
def calculate_mode(numbers: List[float]) -> float:
|
||||||
"""Calculate mode (most frequent value)."""
|
"""Calculate mode (most frequent value)."""
|
||||||
if not numbers:
|
if not numbers:
|
||||||
@ -142,16 +153,19 @@ def calculate_mode(numbers: List[float]) -> float:
|
|||||||
frequency[num] = frequency.get(num, 0) + 1
|
frequency[num] = frequency.get(num, 0) + 1
|
||||||
mode = max(frequency.keys(), key=frequency.get)
|
mode = max(frequency.keys(), key=frequency.get)
|
||||||
return mode
|
return mode
|
||||||
''')
|
'''
|
||||||
|
)
|
||||||
|
|
||||||
# Test file for the calculator
|
# Test file for the calculator
|
||||||
(project_path / "test_calculator.py").write_text('''"""
|
(project_path / "test_calculator.py").write_text(
|
||||||
|
'''"""
|
||||||
Unit tests for calculator module.
|
Unit tests for calculator module.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import unittest
|
import unittest
|
||||||
from calculator import BasicCalculator, ScientificCalculator, calculate_mean
|
from calculator import BasicCalculator, ScientificCalculator, calculate_mean
|
||||||
|
|
||||||
|
|
||||||
class TestBasicCalculator(unittest.TestCase):
|
class TestBasicCalculator(unittest.TestCase):
|
||||||
"""Test cases for BasicCalculator."""
|
"""Test cases for BasicCalculator."""
|
||||||
|
|
||||||
@ -170,6 +184,7 @@ class TestBasicCalculator(unittest.TestCase):
|
|||||||
with self.assertRaises(ValueError):
|
with self.assertRaises(ValueError):
|
||||||
self.calc.divide(10, 0)
|
self.calc.divide(10, 0)
|
||||||
|
|
||||||
|
|
||||||
class TestStatistics(unittest.TestCase):
|
class TestStatistics(unittest.TestCase):
|
||||||
"""Test statistical functions."""
|
"""Test statistical functions."""
|
||||||
|
|
||||||
@ -184,7 +199,8 @@ class TestStatistics(unittest.TestCase):
|
|||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
''')
|
'''
|
||||||
|
)
|
||||||
|
|
||||||
print(" Created 2 Python files")
|
print(" Created 2 Python files")
|
||||||
|
|
||||||
@ -208,12 +224,16 @@ if __name__ == "__main__":
|
|||||||
print("\n a) Semantic search for 'calculate average':")
|
print("\n a) Semantic search for 'calculate average':")
|
||||||
results = searcher.search("calculate average", top_k=3)
|
results = searcher.search("calculate average", top_k=3)
|
||||||
for i, result in enumerate(results, 1):
|
for i, result in enumerate(results, 1):
|
||||||
print(f" {i}. {result.chunk_type} '{result.name}' in {result.file_path} (score: {result.score:.3f})")
|
print(
|
||||||
|
f" {i}. {result.chunk_type} '{result.name}' in {result.file_path} (score: {result.score:.3f})"
|
||||||
|
)
|
||||||
|
|
||||||
print("\n b) BM25-weighted search for 'divide zero':")
|
print("\n b) BM25-weighted search for 'divide zero':")
|
||||||
results = searcher.search("divide zero", top_k=3, semantic_weight=0.2, bm25_weight=0.8)
|
results = searcher.search("divide zero", top_k=3, semantic_weight=0.2, bm25_weight=0.8)
|
||||||
for i, result in enumerate(results, 1):
|
for i, result in enumerate(results, 1):
|
||||||
print(f" {i}. {result.chunk_type} '{result.name}' in {result.file_path} (score: {result.score:.3f})")
|
print(
|
||||||
|
f" {i}. {result.chunk_type} '{result.name}' in {result.file_path} (score: {result.score:.3f})"
|
||||||
|
)
|
||||||
|
|
||||||
print("\n c) Search with context for 'test addition':")
|
print("\n c) Search with context for 'test addition':")
|
||||||
results = searcher.search("test addition", top_k=2, include_context=True)
|
results = searcher.search("test addition", top_k=2, include_context=True)
|
||||||
@ -231,24 +251,24 @@ if __name__ == "__main__":
|
|||||||
|
|
||||||
# Get all chunks to find a method
|
# Get all chunks to find a method
|
||||||
df = searcher.table.to_pandas()
|
df = searcher.table.to_pandas()
|
||||||
method_chunks = df[df['chunk_type'] == 'method']
|
method_chunks = df[df["chunk_type"] == "method"]
|
||||||
|
|
||||||
if len(method_chunks) > 0:
|
if len(method_chunks) > 0:
|
||||||
# Pick a method in the middle
|
# Pick a method in the middle
|
||||||
mid_idx = len(method_chunks) // 2
|
mid_idx = len(method_chunks) // 2
|
||||||
chunk_id = method_chunks.iloc[mid_idx]['chunk_id']
|
chunk_id = method_chunks.iloc[mid_idx]["chunk_id"]
|
||||||
chunk_name = method_chunks.iloc[mid_idx]['name']
|
chunk_name = method_chunks.iloc[mid_idx]["name"]
|
||||||
|
|
||||||
print(f"\n Getting context for method '{chunk_name}':")
|
print(f"\n Getting context for method '{chunk_name}':")
|
||||||
context = searcher.get_chunk_context(chunk_id)
|
context = searcher.get_chunk_context(chunk_id)
|
||||||
|
|
||||||
if context['chunk']:
|
if context["chunk"]:
|
||||||
print(f" Current: {context['chunk'].name}")
|
print(f" Current: {context['chunk'].name}")
|
||||||
if context['prev']:
|
if context["prev"]:
|
||||||
print(f" Previous: {context['prev'].name}")
|
print(f" Previous: {context['prev'].name}")
|
||||||
if context['next']:
|
if context["next"]:
|
||||||
print(f" Next: {context['next'].name}")
|
print(f" Next: {context['next'].name}")
|
||||||
if context['parent']:
|
if context["parent"]:
|
||||||
print(f" Parent class: {context['parent'].name}")
|
print(f" Parent class: {context['parent'].name}")
|
||||||
|
|
||||||
# 5. Show statistics
|
# 5. Show statistics
|
||||||
@ -268,5 +288,6 @@ if __name__ == "__main__":
|
|||||||
print("- Context-aware search with adjacent chunks")
|
print("- Context-aware search with adjacent chunks")
|
||||||
print("- Chunk navigation following code relationships")
|
print("- Chunk navigation following code relationships")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
@ -5,9 +5,10 @@ Simple demo of the hybrid search system showing real results.
|
|||||||
|
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
from rich.syntax import Syntax
|
|
||||||
from rich.panel import Panel
|
from rich.panel import Panel
|
||||||
|
from rich.syntax import Syntax
|
||||||
from rich.table import Table
|
from rich.table import Table
|
||||||
|
|
||||||
from mini_rag.search import CodeSearcher
|
from mini_rag.search import CodeSearcher
|
||||||
@ -26,37 +27,39 @@ def demo_search(project_path: Path):
|
|||||||
|
|
||||||
# Get index stats
|
# Get index stats
|
||||||
stats = searcher.get_statistics()
|
stats = searcher.get_statistics()
|
||||||
if 'error' not in stats:
|
if "error" not in stats:
|
||||||
console.print(f"\n[green] Index ready:[/green] {stats['total_chunks']} chunks from {stats['unique_files']} files")
|
console.print(
|
||||||
|
f"\n[green] Index ready:[/green] {stats['total_chunks']} chunks from {stats['unique_files']} files"
|
||||||
|
)
|
||||||
console.print(f"[dim]Languages: {', '.join(stats['languages'].keys())}[/dim]")
|
console.print(f"[dim]Languages: {', '.join(stats['languages'].keys())}[/dim]")
|
||||||
console.print(f"[dim]Chunk types: {', '.join(stats['chunk_types'].keys())}[/dim]\n")
|
console.print(f"[dim]Chunk types: {', '.join(stats['chunk_types'].keys())}[/dim]\n")
|
||||||
|
|
||||||
# Demo queries
|
# Demo queries
|
||||||
demos = [
|
demos = [
|
||||||
{
|
{
|
||||||
'title': 'Keyword-Heavy Search',
|
"title": "Keyword-Heavy Search",
|
||||||
'query': 'BM25Okapi rank_bm25 search scoring',
|
"query": "BM25Okapi rank_bm25 search scoring",
|
||||||
'description': 'This query has specific technical keywords that BM25 excels at finding',
|
"description": "This query has specific technical keywords that BM25 excels at finding",
|
||||||
'top_k': 5
|
"top_k": 5,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'title': 'Natural Language Query',
|
"title": "Natural Language Query",
|
||||||
'query': 'how to build search index from database chunks',
|
"query": "how to build search index from database chunks",
|
||||||
'description': 'This semantic query benefits from transformer embeddings understanding intent',
|
"description": "This semantic query benefits from transformer embeddings understanding intent",
|
||||||
'top_k': 5
|
"top_k": 5,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'title': 'Mixed Technical Query',
|
"title": "Mixed Technical Query",
|
||||||
'query': 'vector embeddings for semantic code search with transformers',
|
"query": "vector embeddings for semantic code search with transformers",
|
||||||
'description': 'This hybrid query combines technical terms with conceptual understanding',
|
"description": "This hybrid query combines technical terms with conceptual understanding",
|
||||||
'top_k': 5
|
"top_k": 5,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'title': 'Function Search',
|
"title": "Function Search",
|
||||||
'query': 'search method implementation with filters',
|
"query": "search method implementation with filters",
|
||||||
'description': 'Looking for specific function implementations',
|
"description": "Looking for specific function implementations",
|
||||||
'top_k': 5
|
"top_k": 5,
|
||||||
}
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
for demo in demos:
|
for demo in demos:
|
||||||
@ -66,10 +69,10 @@ def demo_search(project_path: Path):
|
|||||||
|
|
||||||
# Run search with hybrid mode
|
# Run search with hybrid mode
|
||||||
results = searcher.search(
|
results = searcher.search(
|
||||||
query=demo['query'],
|
query=demo["query"],
|
||||||
top_k=demo['top_k'],
|
top_k=demo["top_k"],
|
||||||
semantic_weight=0.7,
|
semantic_weight=0.7,
|
||||||
bm25_weight=0.3
|
bm25_weight=0.3,
|
||||||
)
|
)
|
||||||
|
|
||||||
if not results:
|
if not results:
|
||||||
@ -86,11 +89,11 @@ def demo_search(project_path: Path):
|
|||||||
# Get code preview
|
# Get code preview
|
||||||
lines = result.content.splitlines()
|
lines = result.content.splitlines()
|
||||||
if len(lines) > 10:
|
if len(lines) > 10:
|
||||||
preview_lines = lines[:8] + ['...'] + lines[-2:]
|
preview_lines = lines[:8] + ["..."] + lines[-2:]
|
||||||
else:
|
else:
|
||||||
preview_lines = lines
|
preview_lines = lines
|
||||||
|
|
||||||
preview = '\n'.join(preview_lines)
|
preview = "\n".join(preview_lines)
|
||||||
|
|
||||||
# Create info table
|
# Create info table
|
||||||
info = Table.grid(padding=0)
|
info = Table.grid(padding=0)
|
||||||
@ -103,16 +106,22 @@ def demo_search(project_path: Path):
|
|||||||
info.add_row("Language:", result.language)
|
info.add_row("Language:", result.language)
|
||||||
|
|
||||||
# Display result
|
# Display result
|
||||||
console.print(Panel(
|
console.print(
|
||||||
|
Panel(
|
||||||
f"{info}\n\n[dim]{preview}[/dim]",
|
f"{info}\n\n[dim]{preview}[/dim]",
|
||||||
title=header,
|
title=header,
|
||||||
title_align="left",
|
title_align="left",
|
||||||
border_style="blue"
|
border_style="blue",
|
||||||
))
|
)
|
||||||
|
)
|
||||||
|
|
||||||
# Show scoring breakdown for top result
|
# Show scoring breakdown for top result
|
||||||
if results:
|
if results:
|
||||||
console.print("\n[dim]Top result hybrid score: {:.3f} (70% semantic + 30% BM25)[/dim]".format(results[0].score))
|
console.print(
|
||||||
|
"\n[dim]Top result hybrid score: {:.3f} (70% semantic + 30% BM25)[/dim]".format(
|
||||||
|
results[0].score
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
@ -123,7 +132,7 @@ def main():
|
|||||||
# Use the RAG system itself as the demo project
|
# Use the RAG system itself as the demo project
|
||||||
project_path = Path(__file__).parent
|
project_path = Path(__file__).parent
|
||||||
|
|
||||||
if not (project_path / '.mini-rag').exists():
|
if not (project_path / ".mini-rag").exists():
|
||||||
console.print("[red]Error: No RAG index found. Run 'rag-mini index' first.[/red]")
|
console.print("[red]Error: No RAG index found. Run 'rag-mini index' first.[/red]")
|
||||||
console.print(f"[dim]Looked in: {project_path / '.mini-rag'}[/dim]")
|
console.print(f"[dim]Looked in: {project_path / '.mini-rag'}[/dim]")
|
||||||
return
|
return
|
||||||
|
|||||||
@ -2,22 +2,23 @@
|
|||||||
Integration test to verify all three agents' work integrates properly.
|
Integration test to verify all three agents' work integrates properly.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import sys
|
|
||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
# Fix Windows encoding
|
# Fix Windows encoding
|
||||||
if sys.platform == 'win32':
|
if sys.platform == "win32":
|
||||||
os.environ['PYTHONUTF8'] = '1'
|
os.environ["PYTHONUTF8"] = "1"
|
||||||
sys.stdout.reconfigure(encoding='utf-8')
|
sys.stdout.reconfigure(encoding="utf-8")
|
||||||
|
|
||||||
from mini_rag.chunker import CodeChunker
|
from mini_rag.chunker import CodeChunker
|
||||||
|
from mini_rag.config import RAGConfig
|
||||||
from mini_rag.indexer import ProjectIndexer
|
from mini_rag.indexer import ProjectIndexer
|
||||||
from mini_rag.search import CodeSearcher
|
|
||||||
from mini_rag.ollama_embeddings import OllamaEmbedder as CodeEmbedder
|
from mini_rag.ollama_embeddings import OllamaEmbedder as CodeEmbedder
|
||||||
from mini_rag.query_expander import QueryExpander
|
from mini_rag.query_expander import QueryExpander
|
||||||
from mini_rag.config import RAGConfig
|
from mini_rag.search import CodeSearcher
|
||||||
|
|
||||||
|
|
||||||
def test_chunker():
|
def test_chunker():
|
||||||
"""Test that chunker creates chunks with all required metadata."""
|
"""Test that chunker creates chunks with all required metadata."""
|
||||||
@ -29,6 +30,7 @@ def test_chunker():
|
|||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
|
||||||
class TestClass:
|
class TestClass:
|
||||||
"""A test class with multiple methods."""
|
"""A test class with multiple methods."""
|
||||||
|
|
||||||
@ -56,6 +58,7 @@ class TestClass:
|
|||||||
data.append(i * self.value)
|
data.append(i * self.value)
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
class AnotherClass:
|
class AnotherClass:
|
||||||
"""Another test class."""
|
"""Another test class."""
|
||||||
|
|
||||||
@ -72,6 +75,7 @@ def standalone_function(arg1, arg2):
|
|||||||
result = arg1 + arg2
|
result = arg1 + arg2
|
||||||
return result * 2
|
return result * 2
|
||||||
|
|
||||||
|
|
||||||
def another_function():
|
def another_function():
|
||||||
"""Another standalone function."""
|
"""Another standalone function."""
|
||||||
data = {"key": "value", "number": 123}
|
data = {"key": "value", "number": 123}
|
||||||
@ -86,7 +90,9 @@ def another_function():
|
|||||||
# Debug: Show what chunks were created
|
# Debug: Show what chunks were created
|
||||||
print(" Chunks created:")
|
print(" Chunks created:")
|
||||||
for chunk in chunks:
|
for chunk in chunks:
|
||||||
print(f" - Type: {chunk.chunk_type}, Name: {chunk.name}, Lines: {chunk.start_line}-{chunk.end_line}")
|
print(
|
||||||
|
f" - Type: {chunk.chunk_type}, Name: {chunk.name}, Lines: {chunk.start_line}-{chunk.end_line}"
|
||||||
|
)
|
||||||
|
|
||||||
# Check metadata
|
# Check metadata
|
||||||
issues = []
|
issues = []
|
||||||
@ -105,12 +111,14 @@ def another_function():
|
|||||||
issues.append(f"Chunk {i} missing next_chunk_id")
|
issues.append(f"Chunk {i} missing next_chunk_id")
|
||||||
|
|
||||||
# Check parent_class for methods
|
# Check parent_class for methods
|
||||||
if chunk.chunk_type == 'method' and chunk.parent_class is None:
|
if chunk.chunk_type == "method" and chunk.parent_class is None:
|
||||||
issues.append(f"Method chunk {chunk.name} missing parent_class")
|
issues.append(f"Method chunk {chunk.name} missing parent_class")
|
||||||
|
|
||||||
print(f" - Chunk {i}: {chunk.chunk_type} '{chunk.name}' "
|
print(
|
||||||
|
f" - Chunk {i}: {chunk.chunk_type} '{chunk.name}' "
|
||||||
f"[{chunk.chunk_index}/{chunk.total_chunks}] "
|
f"[{chunk.chunk_index}/{chunk.total_chunks}] "
|
||||||
f"prev={chunk.prev_chunk_id} next={chunk.next_chunk_id}")
|
f"prev={chunk.prev_chunk_id} next={chunk.next_chunk_id}"
|
||||||
|
)
|
||||||
|
|
||||||
if issues:
|
if issues:
|
||||||
print(" Issues found:")
|
print(" Issues found:")
|
||||||
@ -121,6 +129,7 @@ def another_function():
|
|||||||
|
|
||||||
return len(issues) == 0
|
return len(issues) == 0
|
||||||
|
|
||||||
|
|
||||||
def test_indexer_storage():
|
def test_indexer_storage():
|
||||||
"""Test that indexer stores the new metadata."""
|
"""Test that indexer stores the new metadata."""
|
||||||
print("\n2. Testing Indexer Storage...")
|
print("\n2. Testing Indexer Storage...")
|
||||||
@ -130,14 +139,20 @@ def test_indexer_storage():
|
|||||||
|
|
||||||
# Create test file
|
# Create test file
|
||||||
test_file = project_path / "test.py"
|
test_file = project_path / "test.py"
|
||||||
test_file.write_text('''
|
test_file.write_text(
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
class MyClass:
|
class MyClass:
|
||||||
|
|
||||||
def my_method(self):
|
def my_method(self):
|
||||||
return 42
|
return 42
|
||||||
''')
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
# Index the project with small chunk size for testing
|
# Index the project with small chunk size for testing
|
||||||
from mini_rag.chunker import CodeChunker
|
from mini_rag.chunker import CodeChunker
|
||||||
|
|
||||||
chunker = CodeChunker(min_chunk_size=1)
|
chunker = CodeChunker(min_chunk_size=1)
|
||||||
indexer = ProjectIndexer(project_path, chunker=chunker)
|
indexer = ProjectIndexer(project_path, chunker=chunker)
|
||||||
stats = indexer.index_project()
|
stats = indexer.index_project()
|
||||||
@ -149,7 +164,12 @@ class MyClass:
|
|||||||
df = indexer.table.to_pandas()
|
df = indexer.table.to_pandas()
|
||||||
columns = df.columns.tolist()
|
columns = df.columns.tolist()
|
||||||
|
|
||||||
required_fields = ['chunk_id', 'prev_chunk_id', 'next_chunk_id', 'parent_class']
|
required_fields = [
|
||||||
|
"chunk_id",
|
||||||
|
"prev_chunk_id",
|
||||||
|
"next_chunk_id",
|
||||||
|
"parent_class",
|
||||||
|
]
|
||||||
missing_fields = [f for f in required_fields if f not in columns]
|
missing_fields = [f for f in required_fields if f not in columns]
|
||||||
|
|
||||||
if missing_fields:
|
if missing_fields:
|
||||||
@ -169,6 +189,7 @@ class MyClass:
|
|||||||
|
|
||||||
return len(missing_fields) == 0
|
return len(missing_fields) == 0
|
||||||
|
|
||||||
|
|
||||||
def test_search_integration():
|
def test_search_integration():
|
||||||
"""Test that search uses the new metadata."""
|
"""Test that search uses the new metadata."""
|
||||||
print("\n3. Testing Search Integration...")
|
print("\n3. Testing Search Integration...")
|
||||||
@ -177,10 +198,12 @@ def test_search_integration():
|
|||||||
project_path = Path(tmpdir)
|
project_path = Path(tmpdir)
|
||||||
|
|
||||||
# Create test files with proper content that will create multiple chunks
|
# Create test files with proper content that will create multiple chunks
|
||||||
(project_path / "math_utils.py").write_text('''"""Math utilities module."""
|
(project_path / "math_utils.py").write_text(
|
||||||
|
'''"""Math utilities module."""
|
||||||
|
|
||||||
import math
|
import math
|
||||||
|
|
||||||
|
|
||||||
class Calculator:
|
class Calculator:
|
||||||
"""A simple calculator class."""
|
"""A simple calculator class."""
|
||||||
|
|
||||||
@ -205,6 +228,7 @@ class Calculator:
|
|||||||
self.result = a / b
|
self.result = a / b
|
||||||
return self.result
|
return self.result
|
||||||
|
|
||||||
|
|
||||||
class AdvancedCalculator(Calculator):
|
class AdvancedCalculator(Calculator):
|
||||||
"""Advanced calculator with more operations."""
|
"""Advanced calculator with more operations."""
|
||||||
|
|
||||||
@ -224,6 +248,7 @@ def compute_average(numbers):
|
|||||||
return 0
|
return 0
|
||||||
return sum(numbers) / len(numbers)
|
return sum(numbers) / len(numbers)
|
||||||
|
|
||||||
|
|
||||||
def compute_median(numbers):
|
def compute_median(numbers):
|
||||||
"""Compute median of a list."""
|
"""Compute median of a list."""
|
||||||
if not numbers:
|
if not numbers:
|
||||||
@ -233,7 +258,8 @@ def compute_median(numbers):
|
|||||||
if n % 2 == 0:
|
if n % 2 == 0:
|
||||||
return (sorted_nums[n//2-1] + sorted_nums[n//2]) / 2
|
return (sorted_nums[n//2-1] + sorted_nums[n//2]) / 2
|
||||||
return sorted_nums[n//2]
|
return sorted_nums[n//2]
|
||||||
''')
|
'''
|
||||||
|
)
|
||||||
|
|
||||||
# Index with small chunk size for testing
|
# Index with small chunk size for testing
|
||||||
chunker = CodeChunker(min_chunk_size=1)
|
chunker = CodeChunker(min_chunk_size=1)
|
||||||
@ -244,8 +270,9 @@ def compute_median(numbers):
|
|||||||
searcher = CodeSearcher(project_path)
|
searcher = CodeSearcher(project_path)
|
||||||
|
|
||||||
# Test BM25 integration
|
# Test BM25 integration
|
||||||
results = searcher.search("multiply numbers", top_k=5,
|
results = searcher.search(
|
||||||
semantic_weight=0.3, bm25_weight=0.7)
|
"multiply numbers", top_k=5, semantic_weight=0.3, bm25_weight=0.7
|
||||||
|
)
|
||||||
|
|
||||||
if results:
|
if results:
|
||||||
print(f" BM25 + semantic search returned {len(results)} results")
|
print(f" BM25 + semantic search returned {len(results)} results")
|
||||||
@ -261,38 +288,43 @@ def compute_median(numbers):
|
|||||||
df = searcher.table.to_pandas()
|
df = searcher.table.to_pandas()
|
||||||
print(f" Total chunks in DB: {len(df)}")
|
print(f" Total chunks in DB: {len(df)}")
|
||||||
|
|
||||||
# Find a method chunk to test parent context
|
# Find a method/function chunk to test parent context
|
||||||
method_chunks = df[df['chunk_type'] == 'method']
|
method_chunks = df[df["chunk_type"].isin(["method", "function"])]
|
||||||
if len(method_chunks) > 0:
|
if len(method_chunks) > 0:
|
||||||
method_chunk_id = method_chunks.iloc[0]['chunk_id']
|
method_chunk_id = method_chunks.iloc[0]["chunk_id"]
|
||||||
context = searcher.get_chunk_context(method_chunk_id)
|
context = searcher.get_chunk_context(method_chunk_id)
|
||||||
|
|
||||||
if context['chunk']:
|
if context["chunk"]:
|
||||||
print(f" Got main chunk: {context['chunk'].name}")
|
print(f" Got main chunk: {context['chunk'].name}")
|
||||||
if context['prev']:
|
if context["prev"]:
|
||||||
print(f" Got previous chunk: {context['prev'].name}")
|
print(f" Got previous chunk: {context['prev'].name}")
|
||||||
else:
|
else:
|
||||||
print(f" - No previous chunk (might be first)")
|
print(" - No previous chunk (might be first)")
|
||||||
if context['next']:
|
if context["next"]:
|
||||||
print(f" Got next chunk: {context['next'].name}")
|
print(f" Got next chunk: {context['next'].name}")
|
||||||
else:
|
else:
|
||||||
print(f" - No next chunk (might be last)")
|
print(" - No next chunk (might be last)")
|
||||||
if context['parent']:
|
if context["parent"]:
|
||||||
print(f" Got parent chunk: {context['parent'].name}")
|
print(f" Got parent chunk: {context['parent'].name}")
|
||||||
else:
|
else:
|
||||||
print(f" - No parent chunk")
|
print(" - No parent chunk")
|
||||||
|
|
||||||
# Test include_context in search
|
# Test include_context in search
|
||||||
results_with_context = searcher.search("add", include_context=True, top_k=2)
|
results_with_context = searcher.search("add", include_context=True, top_k=2)
|
||||||
if results_with_context:
|
if results_with_context:
|
||||||
print(f" Found {len(results_with_context)} results with context")
|
print(f" Found {len(results_with_context)} results with context")
|
||||||
for r in results_with_context:
|
for r in results_with_context:
|
||||||
has_context = bool(r.context_before or r.context_after or r.parent_chunk)
|
# Check if result has context (unused variable removed)
|
||||||
print(f" - {r.name}: context_before={bool(r.context_before)}, "
|
print(
|
||||||
f"context_after={bool(r.context_after)}, parent={bool(r.parent_chunk)}")
|
f" - {r.name}: context_before={bool(r.context_before)}, "
|
||||||
|
f"context_after={bool(r.context_after)}, parent={bool(r.parent_chunk)}"
|
||||||
|
)
|
||||||
|
|
||||||
# Check if at least one result has some context
|
# Check if at least one result has some context
|
||||||
if any(r.context_before or r.context_after or r.parent_chunk for r in results_with_context):
|
if any(
|
||||||
|
r.context_before or r.context_after or r.parent_chunk
|
||||||
|
for r in results_with_context
|
||||||
|
):
|
||||||
print(" Search with context working")
|
print(" Search with context working")
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
@ -307,6 +339,7 @@ def compute_median(numbers):
|
|||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def test_server():
|
def test_server():
|
||||||
"""Test that server still works."""
|
"""Test that server still works."""
|
||||||
print("\n4. Testing Server...")
|
print("\n4. Testing Server...")
|
||||||
@ -314,13 +347,15 @@ def test_server():
|
|||||||
# Just check if we can import and create server instance
|
# Just check if we can import and create server instance
|
||||||
try:
|
try:
|
||||||
from mini_rag.server import RAGServer
|
from mini_rag.server import RAGServer
|
||||||
server = RAGServer(Path("."), port=7778)
|
|
||||||
|
# RAGServer(Path("."), port=7778) # Unused variable removed
|
||||||
print(" Server can be instantiated")
|
print(" Server can be instantiated")
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f" Server error: {e}")
|
print(f" Server error: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def test_new_features():
|
def test_new_features():
|
||||||
"""Test new features: query expansion and smart ranking."""
|
"""Test new features: query expansion and smart ranking."""
|
||||||
print("\n5. Testing New Features (Query Expansion & Smart Ranking)...")
|
print("\n5. Testing New Features (Query Expansion & Smart Ranking)...")
|
||||||
@ -328,7 +363,7 @@ def test_new_features():
|
|||||||
try:
|
try:
|
||||||
# Test configuration loading
|
# Test configuration loading
|
||||||
config = RAGConfig()
|
config = RAGConfig()
|
||||||
print(f" ✅ Configuration loaded successfully")
|
print(" ✅ Configuration loaded successfully")
|
||||||
print(f" Query expansion enabled: {config.search.expand_queries}")
|
print(f" Query expansion enabled: {config.search.expand_queries}")
|
||||||
print(f" Max expansion terms: {config.llm.max_expansion_terms}")
|
print(f" Max expansion terms: {config.llm.max_expansion_terms}")
|
||||||
|
|
||||||
@ -340,13 +375,13 @@ def test_new_features():
|
|||||||
expanded = expander.expand_query(test_query)
|
expanded = expander.expand_query(test_query)
|
||||||
print(f" ✅ Query expansion working: '{test_query}' → '{expanded}'")
|
print(f" ✅ Query expansion working: '{test_query}' → '{expanded}'")
|
||||||
else:
|
else:
|
||||||
print(f" ⚠️ Query expansion offline (Ollama not available)")
|
print(" ⚠️ Query expansion offline (Ollama not available)")
|
||||||
# Test that it still returns original query
|
# Test that it still returns original query
|
||||||
expanded = expander.expand_query(test_query)
|
expanded = expander.expand_query(test_query)
|
||||||
if expanded == test_query:
|
if expanded == test_query:
|
||||||
print(f" ✅ Graceful degradation working: returns original query")
|
print(" ✅ Graceful degradation working: returns original query")
|
||||||
else:
|
else:
|
||||||
print(f" ❌ Error: should return original query when offline")
|
print(" ❌ Error: should return original query when offline")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Test smart ranking (this always works as it's zero-overhead)
|
# Test smart ranking (this always works as it's zero-overhead)
|
||||||
@ -363,7 +398,7 @@ def test_new_features():
|
|||||||
try:
|
try:
|
||||||
searcher = CodeSearcher(temp_path)
|
searcher = CodeSearcher(temp_path)
|
||||||
# Test that the _smart_rerank method exists
|
# Test that the _smart_rerank method exists
|
||||||
if hasattr(searcher, '_smart_rerank'):
|
if hasattr(searcher, "_smart_rerank"):
|
||||||
print(" ✅ Smart ranking method available")
|
print(" ✅ Smart ranking method available")
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
@ -378,6 +413,7 @@ def test_new_features():
|
|||||||
print(f" ❌ New features test failed: {e}")
|
print(f" ❌ New features test failed: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
"""Run all integration tests."""
|
"""Run all integration tests."""
|
||||||
print("=" * 50)
|
print("=" * 50)
|
||||||
@ -389,7 +425,7 @@ def main():
|
|||||||
"Indexer": test_indexer_storage(),
|
"Indexer": test_indexer_storage(),
|
||||||
"Search": test_search_integration(),
|
"Search": test_search_integration(),
|
||||||
"Server": test_server(),
|
"Server": test_server(),
|
||||||
"New Features": test_new_features()
|
"New Features": test_new_features(),
|
||||||
}
|
}
|
||||||
|
|
||||||
print("\n" + "=" * 50)
|
print("\n" + "=" * 50)
|
||||||
@ -410,6 +446,7 @@ def main():
|
|||||||
|
|
||||||
return all_passed
|
return all_passed
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
success = main()
|
success = main()
|
||||||
sys.exit(0 if success else 1)
|
sys.exit(0 if success else 1)
|
||||||
@ -3,19 +3,19 @@
|
|||||||
Show what files are actually indexed in the RAG system.
|
Show what files are actually indexed in the RAG system.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import sys
|
|
||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
|
from collections import Counter
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
if sys.platform == 'win32':
|
from mini_rag.vector_store import VectorStore
|
||||||
os.environ['PYTHONUTF8'] = '1'
|
|
||||||
sys.stdout.reconfigure(encoding='utf-8')
|
if sys.platform == "win32":
|
||||||
|
os.environ["PYTHONUTF8"] = "1"
|
||||||
|
sys.stdout.reconfigure(encoding="utf-8")
|
||||||
|
|
||||||
sys.path.insert(0, str(Path(__file__).parent))
|
sys.path.insert(0, str(Path(__file__).parent))
|
||||||
|
|
||||||
from mini_rag.vector_store import VectorStore
|
|
||||||
from collections import Counter
|
|
||||||
|
|
||||||
project_path = Path.cwd()
|
project_path = Path.cwd()
|
||||||
store = VectorStore(project_path)
|
store = VectorStore(project_path)
|
||||||
store._connect()
|
store._connect()
|
||||||
@ -32,16 +32,16 @@ for row in store.table.to_pandas().itertuples():
|
|||||||
|
|
||||||
unique_files = sorted(set(files))
|
unique_files = sorted(set(files))
|
||||||
|
|
||||||
print(f"\n Indexed Files Summary")
|
print("\n Indexed Files Summary")
|
||||||
print(f"Total files: {len(unique_files)}")
|
print(f"Total files: {len(unique_files)}")
|
||||||
print(f"Total chunks: {len(files)}")
|
print(f"Total chunks: {len(files)}")
|
||||||
print(f"\nChunk types: {dict(chunk_types)}")
|
print(f"\nChunk types: {dict(chunk_types)}")
|
||||||
|
|
||||||
print(f"\n Files with most chunks:")
|
print("\n Files with most chunks:")
|
||||||
for file, count in chunks_by_file.most_common(10):
|
for file, count in chunks_by_file.most_common(10):
|
||||||
print(f" {count:3d} chunks: {file}")
|
print(f" {count:3d} chunks: {file}")
|
||||||
|
|
||||||
print(f"\n Text-to-speech files:")
|
print("\n Text-to-speech files:")
|
||||||
tts_files = [f for f in unique_files if 'text-to-speech' in f or 'speak' in f.lower()]
|
tts_files = [f for f in unique_files if "text-to-speech" in f or "speak" in f.lower()]
|
||||||
for f in tts_files:
|
for f in tts_files:
|
||||||
print(f" - {f} ({chunks_by_file[f]} chunks)")
|
print(f" - {f} ({chunks_by_file[f]} chunks)")
|
||||||
@ -12,19 +12,26 @@ Or run directly with venv:
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from mini_rag.search import CodeSearcher
|
|
||||||
from mini_rag.ollama_embeddings import OllamaEmbedder as CodeEmbedder
|
from mini_rag.ollama_embeddings import OllamaEmbedder as CodeEmbedder
|
||||||
|
from mini_rag.search import CodeSearcher
|
||||||
|
|
||||||
# Check if virtual environment is activated
|
# Check if virtual environment is activated
|
||||||
|
|
||||||
|
|
||||||
def check_venv():
|
def check_venv():
|
||||||
if 'VIRTUAL_ENV' not in os.environ:
|
if "VIRTUAL_ENV" not in os.environ:
|
||||||
print("⚠️ WARNING: Virtual environment not detected!")
|
print("⚠️ WARNING: Virtual environment not detected!")
|
||||||
print(" This test requires the virtual environment to be activated.")
|
print(" This test requires the virtual environment to be activated.")
|
||||||
print(" Run: source .venv/bin/activate && PYTHONPATH=. python tests/test_context_retrieval.py")
|
print(
|
||||||
|
" Run: source .venv/bin/activate && PYTHONPATH=. python tests/test_context_retrieval.py"
|
||||||
|
)
|
||||||
print(" Continuing anyway...\n")
|
print(" Continuing anyway...\n")
|
||||||
|
|
||||||
|
|
||||||
check_venv()
|
check_venv()
|
||||||
|
|
||||||
|
|
||||||
def test_context_retrieval():
|
def test_context_retrieval():
|
||||||
"""Test the new context retrieval functionality."""
|
"""Test the new context retrieval functionality."""
|
||||||
|
|
||||||
@ -61,33 +68,45 @@ def test_context_retrieval():
|
|||||||
if result.context_after:
|
if result.context_after:
|
||||||
print(f" Context after preview: {result.context_after[:50]}...")
|
print(f" Context after preview: {result.context_after[:50]}...")
|
||||||
if result.parent_chunk:
|
if result.parent_chunk:
|
||||||
print(f" Parent chunk: {result.parent_chunk.name} ({result.parent_chunk.chunk_type})")
|
print(
|
||||||
|
f" Parent chunk: {result.parent_chunk.name} ({result.parent_chunk.chunk_type})"
|
||||||
|
)
|
||||||
|
|
||||||
# Test 3: get_chunk_context method
|
# Test 3: get_chunk_context method
|
||||||
print("\n3. Testing get_chunk_context method:")
|
print("\n3. Testing get_chunk_context method:")
|
||||||
# Get a sample chunk_id from the first result
|
# Get a sample chunk_id from the first result
|
||||||
df = searcher.table.to_pandas()
|
df = searcher.table.to_pandas()
|
||||||
if not df.empty:
|
if not df.empty:
|
||||||
sample_chunk_id = df.iloc[0]['chunk_id']
|
sample_chunk_id = df.iloc[0]["chunk_id"]
|
||||||
print(f" Getting context for chunk_id: {sample_chunk_id}")
|
print(f" Getting context for chunk_id: {sample_chunk_id}")
|
||||||
|
|
||||||
context = searcher.get_chunk_context(sample_chunk_id)
|
context = searcher.get_chunk_context(sample_chunk_id)
|
||||||
|
|
||||||
if context['chunk']:
|
if context["chunk"]:
|
||||||
print(f" Main chunk: {context['chunk'].file_path}:{context['chunk'].start_line}")
|
print(
|
||||||
if context['prev']:
|
f" Main chunk: {context['chunk'].file_path}:{context['chunk'].start_line}"
|
||||||
print(f" Previous chunk: lines {context['prev'].start_line}-{context['prev'].end_line}")
|
)
|
||||||
if context['next']:
|
if context["prev"]:
|
||||||
print(f" Next chunk: lines {context['next'].start_line}-{context['next'].end_line}")
|
print(
|
||||||
if context['parent']:
|
f" Previous chunk: lines {context['prev'].start_line}-{context['prev'].end_line}"
|
||||||
print(f" Parent chunk: {context['parent'].name} ({context['parent'].chunk_type})")
|
)
|
||||||
|
if context["next"]:
|
||||||
|
print(
|
||||||
|
f" Next chunk: lines {context['next'].start_line}-{context['next'].end_line}"
|
||||||
|
)
|
||||||
|
if context["parent"]:
|
||||||
|
print(
|
||||||
|
f" Parent chunk: {context['parent'].name} ({context['parent'].chunk_type})"
|
||||||
|
)
|
||||||
|
|
||||||
print("\nAll tests completed successfully!")
|
print("\nAll tests completed successfully!")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error during testing: {e}")
|
print(f"Error during testing: {e}")
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
test_context_retrieval()
|
test_context_retrieval()
|
||||||
@ -10,23 +10,27 @@ Or run directly with venv:
|
|||||||
source .venv/bin/activate && python test_fixes.py
|
source .venv/bin/activate && python test_fixes.py
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import sys
|
|
||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
# Check if virtual environment is activated
|
# Check if virtual environment is activated
|
||||||
|
|
||||||
|
|
||||||
def check_venv():
|
def check_venv():
|
||||||
if 'VIRTUAL_ENV' not in os.environ:
|
if "VIRTUAL_ENV" not in os.environ:
|
||||||
print("⚠️ WARNING: Virtual environment not detected!")
|
print("⚠️ WARNING: Virtual environment not detected!")
|
||||||
print(" This test requires the virtual environment to be activated.")
|
print(" This test requires the virtual environment to be activated.")
|
||||||
print(" Run: source .venv/bin/activate && python test_fixes.py")
|
print(" Run: source .venv/bin/activate && python test_fixes.py")
|
||||||
print(" Continuing anyway...\n")
|
print(" Continuing anyway...\n")
|
||||||
|
|
||||||
|
|
||||||
check_venv()
|
check_venv()
|
||||||
|
|
||||||
# Add current directory to Python path
|
# Add current directory to Python path
|
||||||
sys.path.insert(0, '.')
|
sys.path.insert(0, ".")
|
||||||
|
|
||||||
|
|
||||||
def test_config_model_rankings():
|
def test_config_model_rankings():
|
||||||
"""Test that model rankings are properly configured."""
|
"""Test that model rankings are properly configured."""
|
||||||
@ -46,11 +50,11 @@ def test_config_model_rankings():
|
|||||||
print("✓ Config loads successfully")
|
print("✓ Config loads successfully")
|
||||||
|
|
||||||
# Check LLM config and model rankings
|
# Check LLM config and model rankings
|
||||||
if hasattr(config, 'llm'):
|
if hasattr(config, "llm"):
|
||||||
llm_config = config.llm
|
llm_config = config.llm
|
||||||
print(f"✓ LLM config found: {type(llm_config)}")
|
print(f"✓ LLM config found: {type(llm_config)}")
|
||||||
|
|
||||||
if hasattr(llm_config, 'model_rankings'):
|
if hasattr(llm_config, "model_rankings"):
|
||||||
rankings = llm_config.model_rankings
|
rankings = llm_config.model_rankings
|
||||||
print(f"✓ Model rankings: {rankings}")
|
print(f"✓ Model rankings: {rankings}")
|
||||||
|
|
||||||
@ -58,7 +62,9 @@ def test_config_model_rankings():
|
|||||||
print("✓ qwen3:1.7b is FIRST priority - CORRECT!")
|
print("✓ qwen3:1.7b is FIRST priority - CORRECT!")
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
print(f"✗ WRONG: First model is {rankings[0] if rankings else 'None'}, should be qwen3:1.7b")
|
print(
|
||||||
|
f"✗ WRONG: First model is {rankings[0] if rankings else 'None'}, should be qwen3:1.7b"
|
||||||
|
)
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
print("✗ Model rankings not found in LLM config")
|
print("✗ Model rankings not found in LLM config")
|
||||||
@ -74,6 +80,7 @@ def test_config_model_rankings():
|
|||||||
print(f"✗ Error: {e}")
|
print(f"✗ Error: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def test_context_length_fix():
|
def test_context_length_fix():
|
||||||
"""Test that context length is correctly set to 32K."""
|
"""Test that context length is correctly set to 32K."""
|
||||||
print("\n" + "=" * 60)
|
print("\n" + "=" * 60)
|
||||||
@ -82,7 +89,7 @@ def test_context_length_fix():
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
# Read the synthesizer file and check for 32000
|
# Read the synthesizer file and check for 32000
|
||||||
with open('mini_rag/llm_synthesizer.py', 'r') as f:
|
with open("mini_rag/llm_synthesizer.py", "r") as f:
|
||||||
synthesizer_content = f.read()
|
synthesizer_content = f.read()
|
||||||
|
|
||||||
if '"num_ctx": 32000' in synthesizer_content:
|
if '"num_ctx": 32000' in synthesizer_content:
|
||||||
@ -94,13 +101,13 @@ def test_context_length_fix():
|
|||||||
print("? LLM Synthesizer: num_ctx setting not found clearly")
|
print("? LLM Synthesizer: num_ctx setting not found clearly")
|
||||||
|
|
||||||
# Read the safeguards file and check for 32000
|
# Read the safeguards file and check for 32000
|
||||||
with open('mini_rag/llm_safeguards.py', 'r') as f:
|
with open("mini_rag/llm_safeguards.py", "r") as f:
|
||||||
safeguards_content = f.read()
|
safeguards_content = f.read()
|
||||||
|
|
||||||
if 'context_window: int = 32000' in safeguards_content:
|
if "context_window: int = 32000" in safeguards_content:
|
||||||
print("✓ Safeguards: context_window is correctly set to 32000")
|
print("✓ Safeguards: context_window is correctly set to 32000")
|
||||||
return True
|
return True
|
||||||
elif 'context_window: int = 80000' in safeguards_content:
|
elif "context_window: int = 80000" in safeguards_content:
|
||||||
print("✗ Safeguards: context_window is still 80000 - NEEDS FIX")
|
print("✗ Safeguards: context_window is still 80000 - NEEDS FIX")
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
@ -111,6 +118,7 @@ def test_context_length_fix():
|
|||||||
print(f"✗ Error checking context length: {e}")
|
print(f"✗ Error checking context length: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def test_safeguard_preservation():
|
def test_safeguard_preservation():
|
||||||
"""Test that safeguards preserve content instead of dropping it."""
|
"""Test that safeguards preserve content instead of dropping it."""
|
||||||
print("\n" + "=" * 60)
|
print("\n" + "=" * 60)
|
||||||
@ -119,24 +127,27 @@ def test_safeguard_preservation():
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
# Read the synthesizer file and check for the preservation method
|
# Read the synthesizer file and check for the preservation method
|
||||||
with open('mini_rag/llm_synthesizer.py', 'r') as f:
|
with open("mini_rag/llm_synthesizer.py", "r") as f:
|
||||||
synthesizer_content = f.read()
|
synthesizer_content = f.read()
|
||||||
|
|
||||||
if '_create_safeguard_response_with_content' in synthesizer_content:
|
if "_create_safeguard_response_with_content" in synthesizer_content:
|
||||||
print("✓ Safeguard content preservation method exists")
|
print("✓ Safeguard content preservation method exists")
|
||||||
else:
|
else:
|
||||||
print("✗ Safeguard content preservation method missing")
|
print("✗ Safeguard content preservation method missing")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Check for the specific preservation logic
|
# Check for the specific preservation logic
|
||||||
if 'AI Response (use with caution):' in synthesizer_content:
|
if "AI Response (use with caution):" in synthesizer_content:
|
||||||
print("✓ Content preservation warning format found")
|
print("✓ Content preservation warning format found")
|
||||||
else:
|
else:
|
||||||
print("✗ Content preservation warning format missing")
|
print("✗ Content preservation warning format missing")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Check that it's being called instead of dropping content
|
# Check that it's being called instead of dropping content
|
||||||
if 'return self._create_safeguard_response_with_content(issue_type, explanation, raw_response)' in synthesizer_content:
|
if (
|
||||||
|
"return self._create_safeguard_response_with_content(issue_type, explanation, raw_response)"
|
||||||
|
in synthesizer_content
|
||||||
|
):
|
||||||
print("✓ Preservation method is called when safeguards trigger")
|
print("✓ Preservation method is called when safeguards trigger")
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
@ -147,6 +158,7 @@ def test_safeguard_preservation():
|
|||||||
print(f"✗ Error checking safeguard preservation: {e}")
|
print(f"✗ Error checking safeguard preservation: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def test_import_fixes():
|
def test_import_fixes():
|
||||||
"""Test that import statements are fixed from claude_rag to mini_rag."""
|
"""Test that import statements are fixed from claude_rag to mini_rag."""
|
||||||
print("\n" + "=" * 60)
|
print("\n" + "=" * 60)
|
||||||
@ -154,10 +166,10 @@ def test_import_fixes():
|
|||||||
print("=" * 60)
|
print("=" * 60)
|
||||||
|
|
||||||
test_files = [
|
test_files = [
|
||||||
'tests/test_rag_integration.py',
|
"tests/test_rag_integration.py",
|
||||||
'tests/01_basic_integration_test.py',
|
"tests/01_basic_integration_test.py",
|
||||||
'tests/test_hybrid_search.py',
|
"tests/test_hybrid_search.py",
|
||||||
'tests/test_context_retrieval.py'
|
"tests/test_context_retrieval.py",
|
||||||
]
|
]
|
||||||
|
|
||||||
all_good = True
|
all_good = True
|
||||||
@ -165,13 +177,13 @@ def test_import_fixes():
|
|||||||
for test_file in test_files:
|
for test_file in test_files:
|
||||||
if Path(test_file).exists():
|
if Path(test_file).exists():
|
||||||
try:
|
try:
|
||||||
with open(test_file, 'r') as f:
|
with open(test_file, "r") as f:
|
||||||
content = f.read()
|
content = f.read()
|
||||||
|
|
||||||
if 'claude_rag' in content:
|
if "claude_rag" in content:
|
||||||
print(f"✗ {test_file}: Still contains 'claude_rag' imports")
|
print(f"✗ {test_file}: Still contains 'claude_rag' imports")
|
||||||
all_good = False
|
all_good = False
|
||||||
elif 'mini_rag' in content:
|
elif "mini_rag" in content:
|
||||||
print(f"✓ {test_file}: Uses correct 'mini_rag' imports")
|
print(f"✓ {test_file}: Uses correct 'mini_rag' imports")
|
||||||
else:
|
else:
|
||||||
print(f"? {test_file}: No rag imports found")
|
print(f"? {test_file}: No rag imports found")
|
||||||
@ -184,6 +196,7 @@ def test_import_fixes():
|
|||||||
|
|
||||||
return all_good
|
return all_good
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
"""Run all tests."""
|
"""Run all tests."""
|
||||||
print("FSS-Mini-RAG Fix Verification Tests")
|
print("FSS-Mini-RAG Fix Verification Tests")
|
||||||
@ -193,7 +206,7 @@ def main():
|
|||||||
("Model Rankings", test_config_model_rankings),
|
("Model Rankings", test_config_model_rankings),
|
||||||
("Context Length", test_context_length_fix),
|
("Context Length", test_context_length_fix),
|
||||||
("Safeguard Preservation", test_safeguard_preservation),
|
("Safeguard Preservation", test_safeguard_preservation),
|
||||||
("Import Fixes", test_import_fixes)
|
("Import Fixes", test_import_fixes),
|
||||||
]
|
]
|
||||||
|
|
||||||
results = {}
|
results = {}
|
||||||
@ -226,5 +239,6 @@ def main():
|
|||||||
print("❌ SOME TESTS FAILED - System needs more fixes!")
|
print("❌ SOME TESTS FAILED - System needs more fixes!")
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
sys.exit(main())
|
sys.exit(main())
|
||||||
@ -12,18 +12,15 @@ Or run directly with venv:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import time
|
import time
|
||||||
import json
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import List, Dict, Any
|
from typing import Any, Dict
|
||||||
from rich.console import Console
|
|
||||||
from rich.table import Table
|
from rich.console import Console
|
||||||
from rich.panel import Panel
|
from rich.progress import track
|
||||||
from rich.columns import Columns
|
from rich.table import Table
|
||||||
from rich.syntax import Syntax
|
|
||||||
from rich.progress import track
|
|
||||||
|
|
||||||
from mini_rag.search import CodeSearcher, SearchResult
|
|
||||||
from mini_rag.ollama_embeddings import OllamaEmbedder as CodeEmbedder
|
from mini_rag.ollama_embeddings import OllamaEmbedder as CodeEmbedder
|
||||||
|
from mini_rag.search import CodeSearcher
|
||||||
|
|
||||||
console = Console()
|
console = Console()
|
||||||
|
|
||||||
@ -44,12 +41,18 @@ class SearchTester:
|
|||||||
|
|
||||||
# Get statistics
|
# Get statistics
|
||||||
stats = self.searcher.get_statistics()
|
stats = self.searcher.get_statistics()
|
||||||
if 'error' not in stats:
|
if "error" not in stats:
|
||||||
console.print(f"[dim]Index contains {stats['total_chunks']} chunks from {stats['unique_files']} files[/dim]\n")
|
console.print(
|
||||||
|
f"[dim]Index contains {stats['total_chunks']} chunks from {stats['unique_files']} files[/dim]\n"
|
||||||
|
)
|
||||||
|
|
||||||
def run_query(self, query: str, top_k: int = 10,
|
def run_query(
|
||||||
|
self,
|
||||||
|
query: str,
|
||||||
|
top_k: int = 10,
|
||||||
semantic_only: bool = False,
|
semantic_only: bool = False,
|
||||||
bm25_only: bool = False) -> Dict[str, Any]:
|
bm25_only: bool = False,
|
||||||
|
) -> Dict[str, Any]:
|
||||||
"""Run a single query and return metrics."""
|
"""Run a single query and return metrics."""
|
||||||
|
|
||||||
# Set weights based on mode
|
# Set weights based on mode
|
||||||
@ -69,18 +72,18 @@ class SearchTester:
|
|||||||
query=query,
|
query=query,
|
||||||
top_k=top_k,
|
top_k=top_k,
|
||||||
semantic_weight=semantic_weight,
|
semantic_weight=semantic_weight,
|
||||||
bm25_weight=bm25_weight
|
bm25_weight=bm25_weight,
|
||||||
)
|
)
|
||||||
search_time = time.time() - start
|
search_time = time.time() - start
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'query': query,
|
"query": query,
|
||||||
'mode': mode,
|
"mode": mode,
|
||||||
'results': results,
|
"results": results,
|
||||||
'search_time_ms': search_time * 1000,
|
"search_time_ms": search_time * 1000,
|
||||||
'num_results': len(results),
|
"num_results": len(results),
|
||||||
'top_score': results[0].score if results else 0,
|
"top_score": results[0].score if results else 0,
|
||||||
'avg_score': sum(r.score for r in results) / len(results) if results else 0,
|
"avg_score": sum(r.score for r in results) / len(results) if results else 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
def compare_search_modes(self, query: str, top_k: int = 5):
|
def compare_search_modes(self, query: str, top_k: int = 5):
|
||||||
@ -90,9 +93,9 @@ class SearchTester:
|
|||||||
|
|
||||||
# Run searches in all modes
|
# Run searches in all modes
|
||||||
modes = [
|
modes = [
|
||||||
('hybrid', False, False),
|
("hybrid", False, False),
|
||||||
('semantic', True, False),
|
("semantic", True, False),
|
||||||
('bm25', False, True)
|
("bm25", False, True),
|
||||||
]
|
]
|
||||||
|
|
||||||
all_results = {}
|
all_results = {}
|
||||||
@ -112,28 +115,28 @@ class SearchTester:
|
|||||||
"Search Time (ms)",
|
"Search Time (ms)",
|
||||||
f"{all_results['hybrid']['search_time_ms']:.1f}",
|
f"{all_results['hybrid']['search_time_ms']:.1f}",
|
||||||
f"{all_results['semantic']['search_time_ms']:.1f}",
|
f"{all_results['semantic']['search_time_ms']:.1f}",
|
||||||
f"{all_results['bm25']['search_time_ms']:.1f}"
|
f"{all_results['bm25']['search_time_ms']:.1f}",
|
||||||
)
|
)
|
||||||
|
|
||||||
table.add_row(
|
table.add_row(
|
||||||
"Results Found",
|
"Results Found",
|
||||||
str(all_results['hybrid']['num_results']),
|
str(all_results["hybrid"]["num_results"]),
|
||||||
str(all_results['semantic']['num_results']),
|
str(all_results["semantic"]["num_results"]),
|
||||||
str(all_results['bm25']['num_results'])
|
str(all_results["bm25"]["num_results"]),
|
||||||
)
|
)
|
||||||
|
|
||||||
table.add_row(
|
table.add_row(
|
||||||
"Top Score",
|
"Top Score",
|
||||||
f"{all_results['hybrid']['top_score']:.3f}",
|
f"{all_results['hybrid']['top_score']:.3f}",
|
||||||
f"{all_results['semantic']['top_score']:.3f}",
|
f"{all_results['semantic']['top_score']:.3f}",
|
||||||
f"{all_results['bm25']['top_score']:.3f}"
|
f"{all_results['bm25']['top_score']:.3f}",
|
||||||
)
|
)
|
||||||
|
|
||||||
table.add_row(
|
table.add_row(
|
||||||
"Avg Score",
|
"Avg Score",
|
||||||
f"{all_results['hybrid']['avg_score']:.3f}",
|
f"{all_results['hybrid']['avg_score']:.3f}",
|
||||||
f"{all_results['semantic']['avg_score']:.3f}",
|
f"{all_results['semantic']['avg_score']:.3f}",
|
||||||
f"{all_results['bm25']['avg_score']:.3f}"
|
f"{all_results['bm25']['avg_score']:.3f}",
|
||||||
)
|
)
|
||||||
|
|
||||||
console.print(table)
|
console.print(table)
|
||||||
@ -143,62 +146,68 @@ class SearchTester:
|
|||||||
|
|
||||||
for mode_name, result_data in all_results.items():
|
for mode_name, result_data in all_results.items():
|
||||||
console.print(f"\n[bold cyan]{result_data['mode']}:[/bold cyan]")
|
console.print(f"\n[bold cyan]{result_data['mode']}:[/bold cyan]")
|
||||||
for i, result in enumerate(result_data['results'][:3], 1):
|
for i, result in enumerate(result_data["results"][:3], 1):
|
||||||
console.print(f"\n{i}. [green]{result.file_path}[/green]:{result.start_line}-{result.end_line}")
|
console.print(
|
||||||
console.print(f" [dim]Type: {result.chunk_type} | Name: {result.name} | Score: {result.score:.3f}[/dim]")
|
f"\n{i}. [green]{result.file_path}[/green]:{result.start_line}-{result.end_line}"
|
||||||
|
)
|
||||||
|
console.print(
|
||||||
|
f" [dim]Type: {result.chunk_type} | Name: {result.name} | Score: {result.score:.3f}[/dim]"
|
||||||
|
)
|
||||||
|
|
||||||
# Show snippet
|
# Show snippet
|
||||||
lines = result.content.splitlines()[:5]
|
lines = result.content.splitlines()[:5]
|
||||||
for line in lines:
|
for line in lines:
|
||||||
console.print(f" [dim]{line[:80]}{'...' if len(line) > 80 else ''}[/dim]")
|
console.print(
|
||||||
|
f" [dim]{line[:80]}{'...' if len(line) > 80 else ''}[/dim]"
|
||||||
|
)
|
||||||
|
|
||||||
def test_query_types(self):
|
def test_query_types(self):
|
||||||
"""Test different types of queries to show system capabilities."""
|
"""Test different types of queries to show system capabilities."""
|
||||||
test_queries = [
|
test_queries = [
|
||||||
# Keyword-heavy queries (should benefit from BM25)
|
# Keyword-heavy queries (should benefit from BM25)
|
||||||
{
|
{
|
||||||
'query': 'class CodeSearcher search method',
|
"query": "class CodeSearcher search method",
|
||||||
'description': 'Specific class and method names',
|
"description": "Specific class and method names",
|
||||||
'expected': 'Should find exact matches with BM25 boost'
|
"expected": "Should find exact matches with BM25 boost",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'query': 'import pandas numpy torch',
|
"query": "import pandas numpy torch",
|
||||||
'description': 'Multiple import keywords',
|
"description": "Multiple import keywords",
|
||||||
'expected': 'BM25 should excel at finding import statements'
|
"expected": "BM25 should excel at finding import statements",
|
||||||
},
|
},
|
||||||
|
|
||||||
# Semantic queries (should benefit from embeddings)
|
# Semantic queries (should benefit from embeddings)
|
||||||
{
|
{
|
||||||
'query': 'find similar code chunks using vector similarity',
|
"query": "find similar code chunks using vector similarity",
|
||||||
'description': 'Natural language description',
|
"description": "Natural language description",
|
||||||
'expected': 'Semantic search should understand intent'
|
"expected": "Semantic search should understand intent",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'query': 'how to initialize database connection',
|
"query": "how to initialize database connection",
|
||||||
'description': 'How-to question',
|
"description": "How-to question",
|
||||||
'expected': 'Semantic search should find relevant implementations'
|
"expected": "Semantic search should find relevant implementations",
|
||||||
},
|
},
|
||||||
|
|
||||||
# Mixed queries (benefit from hybrid)
|
# Mixed queries (benefit from hybrid)
|
||||||
{
|
{
|
||||||
'query': 'BM25 scoring implementation for search ranking',
|
"query": "BM25 scoring implementation for search ranking",
|
||||||
'description': 'Technical terms + intent',
|
"description": "Technical terms + intent",
|
||||||
'expected': 'Hybrid should balance keyword and semantic matching'
|
"expected": "Hybrid should balance keyword and semantic matching",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'query': 'embedding vectors for code search with transformers',
|
"query": "embedding vectors for code search with transformers",
|
||||||
'description': 'Domain-specific terminology',
|
"description": "Domain-specific terminology",
|
||||||
'expected': 'Hybrid should leverage both approaches'
|
"expected": "Hybrid should leverage both approaches",
|
||||||
}
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
console.print("\n[bold yellow]Query Type Analysis[/bold yellow]")
|
console.print("\n[bold yellow]Query Type Analysis[/bold yellow]")
|
||||||
console.print("[dim]Testing different query patterns to demonstrate hybrid search benefits[/dim]\n")
|
console.print(
|
||||||
|
"[dim]Testing different query patterns to demonstrate hybrid search benefits[/dim]\n"
|
||||||
|
)
|
||||||
|
|
||||||
for test_case in test_queries:
|
for test_case in test_queries:
|
||||||
console.rule(f"\n[cyan]{test_case['description']}[/cyan]")
|
console.rule(f"\n[cyan]{test_case['description']}[/cyan]")
|
||||||
console.print(f"[dim]{test_case['expected']}[/dim]")
|
console.print(f"[dim]{test_case['expected']}[/dim]")
|
||||||
self.compare_search_modes(test_case['query'], top_k=3)
|
self.compare_search_modes(test_case["query"], top_k=3)
|
||||||
time.sleep(0.5) # Brief pause between tests
|
time.sleep(0.5) # Brief pause between tests
|
||||||
|
|
||||||
def benchmark_performance(self, num_queries: int = 50):
|
def benchmark_performance(self, num_queries: int = 50):
|
||||||
@ -217,16 +226,16 @@ class SearchTester:
|
|||||||
"test cases unit testing",
|
"test cases unit testing",
|
||||||
"configuration settings",
|
"configuration settings",
|
||||||
"logging and debugging",
|
"logging and debugging",
|
||||||
"performance optimization"
|
"performance optimization",
|
||||||
] * (num_queries // 10 + 1)
|
] * (num_queries // 10 + 1)
|
||||||
|
|
||||||
benchmark_queries = benchmark_queries[:num_queries]
|
benchmark_queries = benchmark_queries[:num_queries]
|
||||||
|
|
||||||
# Benchmark each mode
|
# Benchmark each mode
|
||||||
modes = [
|
modes = [
|
||||||
('Hybrid (70/30)', 0.7, 0.3),
|
("Hybrid (70/30)", 0.7, 0.3),
|
||||||
('Semantic Only', 1.0, 0.0),
|
("Semantic Only", 1.0, 0.0),
|
||||||
('BM25 Only', 0.0, 1.0)
|
("BM25 Only", 0.0, 1.0),
|
||||||
]
|
]
|
||||||
|
|
||||||
results_table = Table(title="Performance Benchmark Results")
|
results_table = Table(title="Performance Benchmark Results")
|
||||||
@ -246,7 +255,7 @@ class SearchTester:
|
|||||||
query=query,
|
query=query,
|
||||||
limit=10,
|
limit=10,
|
||||||
semantic_weight=sem_weight,
|
semantic_weight=sem_weight,
|
||||||
bm25_weight=bm25_weight
|
bm25_weight=bm25_weight,
|
||||||
)
|
)
|
||||||
elapsed = (time.time() - start) * 1000
|
elapsed = (time.time() - start) * 1000
|
||||||
times.append(elapsed)
|
times.append(elapsed)
|
||||||
@ -262,7 +271,7 @@ class SearchTester:
|
|||||||
f"{avg_time:.2f}",
|
f"{avg_time:.2f}",
|
||||||
f"{min_time:.2f}",
|
f"{min_time:.2f}",
|
||||||
f"{max_time:.2f}",
|
f"{max_time:.2f}",
|
||||||
f"{total_time:.2f}"
|
f"{total_time:.2f}",
|
||||||
)
|
)
|
||||||
|
|
||||||
console.print("\n")
|
console.print("\n")
|
||||||
@ -292,7 +301,9 @@ class SearchTester:
|
|||||||
|
|
||||||
table.add_row("Total Results", str(len(results)))
|
table.add_row("Total Results", str(len(results)))
|
||||||
table.add_row("Unique Files", str(len(file_counts)))
|
table.add_row("Unique Files", str(len(file_counts)))
|
||||||
table.add_row("Max Chunks per File", str(max(file_counts.values()) if file_counts else 0))
|
table.add_row(
|
||||||
|
"Max Chunks per File", str(max(file_counts.values()) if file_counts else 0)
|
||||||
|
)
|
||||||
table.add_row("Unique Chunk Types", str(len(chunk_types)))
|
table.add_row("Unique Chunk Types", str(len(chunk_types)))
|
||||||
|
|
||||||
console.print(table)
|
console.print(table)
|
||||||
@ -300,13 +311,17 @@ class SearchTester:
|
|||||||
# Show file distribution
|
# Show file distribution
|
||||||
if len(file_counts) > 0:
|
if len(file_counts) > 0:
|
||||||
console.print("\n[bold]File Distribution:[/bold]")
|
console.print("\n[bold]File Distribution:[/bold]")
|
||||||
for file_path, count in sorted(file_counts.items(), key=lambda x: x[1], reverse=True)[:5]:
|
for file_path, count in sorted(
|
||||||
|
file_counts.items(), key=lambda x: x[1], reverse=True
|
||||||
|
)[:5]:
|
||||||
console.print(f" {count}x {file_path}")
|
console.print(f" {count}x {file_path}")
|
||||||
|
|
||||||
# Show chunk type distribution
|
# Show chunk type distribution
|
||||||
if len(chunk_types) > 0:
|
if len(chunk_types) > 0:
|
||||||
console.print("\n[bold]Chunk Type Distribution:[/bold]")
|
console.print("\n[bold]Chunk Type Distribution:[/bold]")
|
||||||
for chunk_type, count in sorted(chunk_types.items(), key=lambda x: x[1], reverse=True):
|
for chunk_type, count in sorted(
|
||||||
|
chunk_types.items(), key=lambda x: x[1], reverse=True
|
||||||
|
):
|
||||||
console.print(f" {chunk_type}: {count} chunks")
|
console.print(f" {chunk_type}: {count} chunks")
|
||||||
|
|
||||||
# Verify constraints
|
# Verify constraints
|
||||||
@ -327,7 +342,7 @@ def main():
|
|||||||
else:
|
else:
|
||||||
project_path = Path.cwd()
|
project_path = Path.cwd()
|
||||||
|
|
||||||
if not (project_path / '.mini-rag').exists():
|
if not (project_path / ".mini-rag").exists():
|
||||||
console.print("[red]Error: No RAG index found. Run 'rag-mini index' first.[/red]")
|
console.print("[red]Error: No RAG index found. Run 'rag-mini index' first.[/red]")
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -335,30 +350,30 @@ def main():
|
|||||||
tester = SearchTester(project_path)
|
tester = SearchTester(project_path)
|
||||||
|
|
||||||
# Run all tests
|
# Run all tests
|
||||||
console.print("\n" + "="*80)
|
console.print("\n" + "=" * 80)
|
||||||
console.print("[bold green]Mini RAG Hybrid Search Test Suite[/bold green]")
|
console.print("[bold green]Mini RAG Hybrid Search Test Suite[/bold green]")
|
||||||
console.print("="*80)
|
console.print("=" * 80)
|
||||||
|
|
||||||
# Test 1: Query type analysis
|
# Test 1: Query type analysis
|
||||||
tester.test_query_types()
|
tester.test_query_types()
|
||||||
|
|
||||||
# Test 2: Performance benchmark
|
# Test 2: Performance benchmark
|
||||||
console.print("\n" + "-"*80)
|
console.print("\n" + "-" * 80)
|
||||||
tester.benchmark_performance(num_queries=30)
|
tester.benchmark_performance(num_queries=30)
|
||||||
|
|
||||||
# Test 3: Diversity constraints
|
# Test 3: Diversity constraints
|
||||||
console.print("\n" + "-"*80)
|
console.print("\n" + "-" * 80)
|
||||||
tester.test_diversity_constraints()
|
tester.test_diversity_constraints()
|
||||||
|
|
||||||
# Summary
|
# Summary
|
||||||
console.print("\n" + "="*80)
|
console.print("\n" + "=" * 80)
|
||||||
console.print("[bold green]Test Suite Complete![/bold green]")
|
console.print("[bold green]Test Suite Complete![/bold green]")
|
||||||
console.print("\n[dim]The hybrid search combines:")
|
console.print("\n[dim]The hybrid search combines:")
|
||||||
console.print(" • Semantic understanding from transformer embeddings")
|
console.print(" • Semantic understanding from transformer embeddings")
|
||||||
console.print(" • Keyword relevance from BM25 scoring")
|
console.print(" • Keyword relevance from BM25 scoring")
|
||||||
console.print(" • Result diversity through intelligent filtering")
|
console.print(" • Result diversity through intelligent filtering")
|
||||||
console.print(" • Performance optimization through concurrent processing[/dim]")
|
console.print(" • Performance optimization through concurrent processing[/dim]")
|
||||||
console.print("="*80 + "\n")
|
console.print("=" * 80 + "\n")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@ -1,13 +1,16 @@
|
|||||||
"""Test with smaller min_chunk_size."""
|
"""Test with smaller min_chunk_size."""
|
||||||
|
|
||||||
from mini_rag.chunker import CodeChunker
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
from mini_rag.chunker import CodeChunker
|
||||||
|
|
||||||
test_code = '''"""Test module."""
|
test_code = '''"""Test module."""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
|
||||||
class MyClass:
|
class MyClass:
|
||||||
|
|
||||||
def method(self):
|
def method(self):
|
||||||
return 42
|
return 42
|
||||||
|
|
||||||
|
|||||||
@ -7,7 +7,6 @@ between thinking and no-thinking modes.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
import os
|
|
||||||
import tempfile
|
import tempfile
|
||||||
import unittest
|
import unittest
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@ -16,16 +15,17 @@ from pathlib import Path
|
|||||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from mini_rag.llm_synthesizer import LLMSynthesizer
|
|
||||||
from mini_rag.explorer import CodeExplorer
|
|
||||||
from mini_rag.config import RAGConfig
|
from mini_rag.config import RAGConfig
|
||||||
|
from mini_rag.explorer import CodeExplorer
|
||||||
from mini_rag.indexer import ProjectIndexer
|
from mini_rag.indexer import ProjectIndexer
|
||||||
|
from mini_rag.llm_synthesizer import LLMSynthesizer
|
||||||
from mini_rag.search import CodeSearcher
|
from mini_rag.search import CodeSearcher
|
||||||
except ImportError as e:
|
except ImportError as e:
|
||||||
print(f"❌ Could not import RAG components: {e}")
|
print(f"❌ Could not import RAG components: {e}")
|
||||||
print(" This test requires the full RAG system to be installed")
|
print(" This test requires the full RAG system to be installed")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
class TestModeSeparation(unittest.TestCase):
|
class TestModeSeparation(unittest.TestCase):
|
||||||
"""Test the clean separation between synthesis and exploration modes."""
|
"""Test the clean separation between synthesis and exploration modes."""
|
||||||
|
|
||||||
@ -36,7 +36,8 @@ class TestModeSeparation(unittest.TestCase):
|
|||||||
|
|
||||||
# Create a simple test project
|
# Create a simple test project
|
||||||
test_file = self.project_path / "test_module.py"
|
test_file = self.project_path / "test_module.py"
|
||||||
test_file.write_text('''"""Test module for mode separation testing."""
|
test_file.write_text(
|
||||||
|
'''"""Test module for mode separation testing."""
|
||||||
|
|
||||||
def authenticate_user(username: str, password: str) -> bool:
|
def authenticate_user(username: str, password: str) -> bool:
|
||||||
"""Authenticate a user with username and password."""
|
"""Authenticate a user with username and password."""
|
||||||
@ -48,6 +49,7 @@ def authenticate_user(username: str, password: str) -> bool:
|
|||||||
valid_users = {"admin": "secret", "user": "password"}
|
valid_users = {"admin": "secret", "user": "password"}
|
||||||
return valid_users.get(username) == password
|
return valid_users.get(username) == password
|
||||||
|
|
||||||
|
|
||||||
class UserManager:
|
class UserManager:
|
||||||
"""Manages user operations."""
|
"""Manages user operations."""
|
||||||
|
|
||||||
@ -71,7 +73,8 @@ def process_login_request(username: str, password: str) -> dict:
|
|||||||
return {"success": True, "message": "Login successful"}
|
return {"success": True, "message": "Login successful"}
|
||||||
else:
|
else:
|
||||||
return {"success": False, "message": "Invalid credentials"}
|
return {"success": False, "message": "Invalid credentials"}
|
||||||
''')
|
'''
|
||||||
|
)
|
||||||
|
|
||||||
# Index the project for testing
|
# Index the project for testing
|
||||||
try:
|
try:
|
||||||
@ -83,6 +86,7 @@ def process_login_request(username: str, password: str) -> dict:
|
|||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
"""Clean up test environment."""
|
"""Clean up test environment."""
|
||||||
import shutil
|
import shutil
|
||||||
|
|
||||||
shutil.rmtree(self.temp_dir, ignore_errors=True)
|
shutil.rmtree(self.temp_dir, ignore_errors=True)
|
||||||
|
|
||||||
def test_01_synthesis_mode_defaults(self):
|
def test_01_synthesis_mode_defaults(self):
|
||||||
@ -90,8 +94,9 @@ def process_login_request(username: str, password: str) -> dict:
|
|||||||
synthesizer = LLMSynthesizer()
|
synthesizer = LLMSynthesizer()
|
||||||
|
|
||||||
# Should default to no thinking
|
# Should default to no thinking
|
||||||
self.assertFalse(synthesizer.enable_thinking,
|
self.assertFalse(
|
||||||
"Synthesis mode should default to no thinking")
|
synthesizer.enable_thinking, "Synthesis mode should default to no thinking"
|
||||||
|
)
|
||||||
|
|
||||||
print("✅ Synthesis mode defaults to no thinking")
|
print("✅ Synthesis mode defaults to no thinking")
|
||||||
|
|
||||||
@ -101,8 +106,10 @@ def process_login_request(username: str, password: str) -> dict:
|
|||||||
explorer = CodeExplorer(self.project_path, config)
|
explorer = CodeExplorer(self.project_path, config)
|
||||||
|
|
||||||
# Should enable thinking in exploration mode
|
# Should enable thinking in exploration mode
|
||||||
self.assertTrue(explorer.synthesizer.enable_thinking,
|
self.assertTrue(
|
||||||
"Exploration mode should enable thinking")
|
explorer.synthesizer.enable_thinking,
|
||||||
|
"Exploration mode should enable thinking",
|
||||||
|
)
|
||||||
|
|
||||||
print("✅ Exploration mode enables thinking by default")
|
print("✅ Exploration mode enables thinking by default")
|
||||||
|
|
||||||
@ -111,12 +118,16 @@ def process_login_request(username: str, password: str) -> dict:
|
|||||||
synthesizer = LLMSynthesizer(enable_thinking=False)
|
synthesizer = LLMSynthesizer(enable_thinking=False)
|
||||||
|
|
||||||
# Should not have public methods to toggle thinking
|
# Should not have public methods to toggle thinking
|
||||||
thinking_methods = [method for method in dir(synthesizer)
|
thinking_methods = [
|
||||||
if 'thinking' in method.lower() and not method.startswith('_')]
|
method
|
||||||
|
for method in dir(synthesizer)
|
||||||
|
if "thinking" in method.lower() and not method.startswith("_")
|
||||||
|
]
|
||||||
|
|
||||||
# The only thinking-related attribute should be the readonly enable_thinking
|
# The only thinking-related attribute should be the readonly enable_thinking
|
||||||
self.assertEqual(len(thinking_methods), 0,
|
self.assertEqual(
|
||||||
"Should not have public thinking toggle methods")
|
len(thinking_methods), 0, "Should not have public thinking toggle methods"
|
||||||
|
)
|
||||||
|
|
||||||
print("✅ No runtime thinking toggle methods available")
|
print("✅ No runtime thinking toggle methods available")
|
||||||
|
|
||||||
@ -132,10 +143,14 @@ def process_login_request(username: str, password: str) -> dict:
|
|||||||
exploration_synthesizer = LLMSynthesizer(enable_thinking=True)
|
exploration_synthesizer = LLMSynthesizer(enable_thinking=True)
|
||||||
|
|
||||||
# Both should maintain their thinking settings
|
# Both should maintain their thinking settings
|
||||||
self.assertFalse(synthesis_synthesizer.enable_thinking,
|
self.assertFalse(
|
||||||
"Synthesis synthesizer should remain no-thinking")
|
synthesis_synthesizer.enable_thinking,
|
||||||
self.assertTrue(exploration_synthesizer.enable_thinking,
|
"Synthesis synthesizer should remain no-thinking",
|
||||||
"Exploration synthesizer should remain thinking-enabled")
|
)
|
||||||
|
self.assertTrue(
|
||||||
|
exploration_synthesizer.enable_thinking,
|
||||||
|
"Exploration synthesizer should remain thinking-enabled",
|
||||||
|
)
|
||||||
|
|
||||||
print("✅ Mode contamination prevented")
|
print("✅ Mode contamination prevented")
|
||||||
|
|
||||||
@ -145,13 +160,11 @@ def process_login_request(username: str, password: str) -> dict:
|
|||||||
explorer = CodeExplorer(self.project_path, config)
|
explorer = CodeExplorer(self.project_path, config)
|
||||||
|
|
||||||
# Should start with no active session
|
# Should start with no active session
|
||||||
self.assertIsNone(explorer.current_session,
|
self.assertIsNone(explorer.current_session, "Should start with no active session")
|
||||||
"Should start with no active session")
|
|
||||||
|
|
||||||
# Should be able to create session summary even without session
|
# Should be able to create session summary even without session
|
||||||
summary = explorer.get_session_summary()
|
summary = explorer.get_session_summary()
|
||||||
self.assertIn("No active", summary,
|
self.assertIn("No active", summary, "Should handle no active session gracefully")
|
||||||
"Should handle no active session gracefully")
|
|
||||||
|
|
||||||
print("✅ Session management working correctly")
|
print("✅ Session management working correctly")
|
||||||
|
|
||||||
@ -161,8 +174,10 @@ def process_login_request(username: str, password: str) -> dict:
|
|||||||
explorer = CodeExplorer(self.project_path, config)
|
explorer = CodeExplorer(self.project_path, config)
|
||||||
|
|
||||||
# Should have context tracking attributes
|
# Should have context tracking attributes
|
||||||
self.assertTrue(hasattr(explorer, 'current_session'),
|
self.assertTrue(
|
||||||
"Explorer should have session tracking")
|
hasattr(explorer, "current_session"),
|
||||||
|
"Explorer should have session tracking",
|
||||||
|
)
|
||||||
|
|
||||||
print("✅ Context memory structure present")
|
print("✅ Context memory structure present")
|
||||||
|
|
||||||
@ -174,12 +189,13 @@ def process_login_request(username: str, password: str) -> dict:
|
|||||||
synthesizer = LLMSynthesizer(enable_thinking=False)
|
synthesizer = LLMSynthesizer(enable_thinking=False)
|
||||||
|
|
||||||
# Test the _call_ollama method handling
|
# Test the _call_ollama method handling
|
||||||
if hasattr(synthesizer, '_call_ollama'):
|
if hasattr(synthesizer, "_call_ollama"):
|
||||||
# Should append <no_think> when thinking disabled
|
# Should append <no_think> when thinking disabled
|
||||||
# This is a white-box test of the implementation
|
# This is a white-box test of the implementation
|
||||||
try:
|
try:
|
||||||
# Mock test - just verify the method exists and can be called
|
# Mock test - just verify the method exists and can be called
|
||||||
result = synthesizer._call_ollama("test", temperature=0.1, disable_thinking=True)
|
# Test call (result unused)
|
||||||
|
synthesizer._call_ollama("test", temperature=0.1, disable_thinking=True)
|
||||||
# Don't assert on result since Ollama might not be available
|
# Don't assert on result since Ollama might not be available
|
||||||
print("✅ No-thinking prompt handling available")
|
print("✅ No-thinking prompt handling available")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -191,14 +207,18 @@ def process_login_request(username: str, password: str) -> dict:
|
|||||||
"""Test that modes initialize correctly with lazy loading."""
|
"""Test that modes initialize correctly with lazy loading."""
|
||||||
# Synthesis mode
|
# Synthesis mode
|
||||||
synthesis_synthesizer = LLMSynthesizer(enable_thinking=False)
|
synthesis_synthesizer = LLMSynthesizer(enable_thinking=False)
|
||||||
self.assertFalse(synthesis_synthesizer._initialized,
|
self.assertFalse(
|
||||||
"Should start uninitialized for lazy loading")
|
synthesis_synthesizer._initialized,
|
||||||
|
"Should start uninitialized for lazy loading",
|
||||||
|
)
|
||||||
|
|
||||||
# Exploration mode
|
# Exploration mode
|
||||||
config = RAGConfig()
|
config = RAGConfig()
|
||||||
explorer = CodeExplorer(self.project_path, config)
|
explorer = CodeExplorer(self.project_path, config)
|
||||||
self.assertFalse(explorer.synthesizer._initialized,
|
self.assertFalse(
|
||||||
"Should start uninitialized for lazy loading")
|
explorer.synthesizer._initialized,
|
||||||
|
"Should start uninitialized for lazy loading",
|
||||||
|
)
|
||||||
|
|
||||||
print("✅ Lazy initialization working correctly")
|
print("✅ Lazy initialization working correctly")
|
||||||
|
|
||||||
@ -208,31 +228,31 @@ def process_login_request(username: str, password: str) -> dict:
|
|||||||
searcher = CodeSearcher(self.project_path)
|
searcher = CodeSearcher(self.project_path)
|
||||||
search_results = searcher.search("authentication", top_k=3)
|
search_results = searcher.search("authentication", top_k=3)
|
||||||
|
|
||||||
self.assertGreater(len(search_results), 0,
|
self.assertGreater(len(search_results), 0, "Search should return results")
|
||||||
"Search should return results")
|
|
||||||
|
|
||||||
# Exploration mode setup
|
# Exploration mode setup
|
||||||
config = RAGConfig()
|
config = RAGConfig()
|
||||||
explorer = CodeExplorer(self.project_path, config)
|
explorer = CodeExplorer(self.project_path, config)
|
||||||
|
|
||||||
# Both should work with same project but different approaches
|
# Both should work with same project but different approaches
|
||||||
self.assertTrue(hasattr(explorer, 'synthesizer'),
|
self.assertTrue(
|
||||||
"Explorer should have thinking-enabled synthesizer")
|
hasattr(explorer, "synthesizer"),
|
||||||
|
"Explorer should have thinking-enabled synthesizer",
|
||||||
|
)
|
||||||
|
|
||||||
print("✅ Search and exploration integration working")
|
print("✅ Search and exploration integration working")
|
||||||
|
|
||||||
def test_10_mode_guidance_detection(self):
|
def test_10_mode_guidance_detection(self):
|
||||||
"""Test that the system can detect when to recommend different modes."""
|
"""Test that the system can detect when to recommend different modes."""
|
||||||
# Words that should trigger exploration mode recommendation
|
# Words that should trigger exploration mode recommendation
|
||||||
exploration_triggers = ['why', 'how', 'explain', 'debug']
|
exploration_triggers = ["why", "how", "explain", "debug"]
|
||||||
|
|
||||||
for trigger in exploration_triggers:
|
for trigger in exploration_triggers:
|
||||||
query = f"{trigger} does authentication work"
|
query = f"{trigger} does authentication work"
|
||||||
# This would typically be tested in the main CLI
|
# This would typically be tested in the main CLI
|
||||||
# Here we just verify the trigger detection logic exists
|
# Here we just verify the trigger detection logic exists
|
||||||
has_trigger = any(word in query.lower() for word in exploration_triggers)
|
has_trigger = any(word in query.lower() for word in exploration_triggers)
|
||||||
self.assertTrue(has_trigger,
|
self.assertTrue(has_trigger, f"Should detect '{trigger}' as exploration trigger")
|
||||||
f"Should detect '{trigger}' as exploration trigger")
|
|
||||||
|
|
||||||
print("✅ Mode guidance detection working")
|
print("✅ Mode guidance detection working")
|
||||||
|
|
||||||
@ -240,11 +260,13 @@ def process_login_request(username: str, password: str) -> dict:
|
|||||||
"""Check if Ollama is available for testing."""
|
"""Check if Ollama is available for testing."""
|
||||||
try:
|
try:
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
response = requests.get("http://localhost:11434/api/tags", timeout=5)
|
response = requests.get("http://localhost:11434/api/tags", timeout=5)
|
||||||
return response.status_code == 200
|
return response.status_code == 200
|
||||||
except Exception:
|
except Exception:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
"""Run mode separation tests."""
|
"""Run mode separation tests."""
|
||||||
print("🧪 Testing Mode Separation")
|
print("🧪 Testing Mode Separation")
|
||||||
@ -272,6 +294,7 @@ def main():
|
|||||||
|
|
||||||
return result.wasSuccessful()
|
return result.wasSuccessful()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
success = main()
|
success = main()
|
||||||
sys.exit(0 if success else 1)
|
sys.exit(0 if success else 1)
|
||||||
@ -8,20 +8,20 @@ what's working and what needs attention.
|
|||||||
Run with: python3 tests/test_ollama_integration.py
|
Run with: python3 tests/test_ollama_integration.py
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import unittest
|
|
||||||
import requests
|
|
||||||
import json
|
|
||||||
import sys
|
import sys
|
||||||
|
import unittest
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from unittest.mock import patch, MagicMock
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
from mini_rag.config import RAGConfig
|
||||||
|
from mini_rag.llm_synthesizer import LLMSynthesizer
|
||||||
|
from mini_rag.query_expander import QueryExpander
|
||||||
|
|
||||||
# Add project to path
|
# Add project to path
|
||||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||||
|
|
||||||
from mini_rag.query_expander import QueryExpander
|
|
||||||
from mini_rag.llm_synthesizer import LLMSynthesizer
|
|
||||||
from mini_rag.config import RAGConfig
|
|
||||||
|
|
||||||
|
|
||||||
class TestOllamaIntegration(unittest.TestCase):
|
class TestOllamaIntegration(unittest.TestCase):
|
||||||
"""
|
"""
|
||||||
@ -49,21 +49,20 @@ class TestOllamaIntegration(unittest.TestCase):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
response = requests.get(
|
response = requests.get(
|
||||||
f"http://{self.config.llm.ollama_host}/api/tags",
|
f"http://{self.config.llm.ollama_host}/api/tags", timeout=5
|
||||||
timeout=5
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
data = response.json()
|
data = response.json()
|
||||||
models = data.get('models', [])
|
models = data.get("models", [])
|
||||||
print(f" ✅ Ollama server is running!")
|
print(" ✅ Ollama server is running!")
|
||||||
print(f" 📦 Found {len(models)} models available")
|
print(f" 📦 Found {len(models)} models available")
|
||||||
|
|
||||||
if models:
|
if models:
|
||||||
print(" 🎯 Available models:")
|
print(" 🎯 Available models:")
|
||||||
for model in models[:5]: # Show first 5
|
for model in models[:5]: # Show first 5
|
||||||
name = model.get('name', 'unknown')
|
name = model.get("name", "unknown")
|
||||||
size = model.get('size', 0)
|
size = model.get("size", 0)
|
||||||
print(f" • {name} ({size//1000000:.0f}MB)")
|
print(f" • {name} ({size//1000000:.0f}MB)")
|
||||||
if len(models) > 5:
|
if len(models) > 5:
|
||||||
print(f" ... and {len(models)-5} more")
|
print(f" ... and {len(models)-5} more")
|
||||||
@ -100,19 +99,16 @@ class TestOllamaIntegration(unittest.TestCase):
|
|||||||
# Test embedding generation
|
# Test embedding generation
|
||||||
response = requests.post(
|
response = requests.post(
|
||||||
f"http://{self.config.llm.ollama_host}/api/embeddings",
|
f"http://{self.config.llm.ollama_host}/api/embeddings",
|
||||||
json={
|
json={"model": "nomic-embed-text", "prompt": "test embedding"},
|
||||||
"model": "nomic-embed-text",
|
timeout=10,
|
||||||
"prompt": "test embedding"
|
|
||||||
},
|
|
||||||
timeout=10
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
data = response.json()
|
data = response.json()
|
||||||
embedding = data.get('embedding', [])
|
embedding = data.get("embedding", [])
|
||||||
|
|
||||||
if embedding and len(embedding) > 0:
|
if embedding and len(embedding) > 0:
|
||||||
print(f" ✅ Embedding model working!")
|
print(" ✅ Embedding model working!")
|
||||||
print(f" 📊 Generated {len(embedding)}-dimensional vectors")
|
print(f" 📊 Generated {len(embedding)}-dimensional vectors")
|
||||||
self.assertTrue(len(embedding) > 100) # Should be substantial vectors
|
self.assertTrue(len(embedding) > 100) # Should be substantial vectors
|
||||||
else:
|
else:
|
||||||
@ -155,12 +151,11 @@ class TestOllamaIntegration(unittest.TestCase):
|
|||||||
# Test basic text generation
|
# Test basic text generation
|
||||||
try:
|
try:
|
||||||
response = synthesizer._call_ollama(
|
response = synthesizer._call_ollama(
|
||||||
"Complete this: The capital of France is",
|
"Complete this: The capital of France is", temperature=0.1
|
||||||
temperature=0.1
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if response and len(response.strip()) > 0:
|
if response and len(response.strip()) > 0:
|
||||||
print(f" ✅ Model generating responses!")
|
print(" ✅ Model generating responses!")
|
||||||
print(f" 💬 Sample response: '{response[:50]}...'")
|
print(f" 💬 Sample response: '{response[:50]}...'")
|
||||||
|
|
||||||
# Basic quality check
|
# Basic quality check
|
||||||
@ -231,8 +226,9 @@ class TestOllamaIntegration(unittest.TestCase):
|
|||||||
synthesizer = LLMSynthesizer()
|
synthesizer = LLMSynthesizer()
|
||||||
|
|
||||||
# Should default to no thinking
|
# Should default to no thinking
|
||||||
self.assertFalse(synthesizer.enable_thinking,
|
self.assertFalse(
|
||||||
"Synthesis mode should default to no thinking")
|
synthesizer.enable_thinking, "Synthesis mode should default to no thinking"
|
||||||
|
)
|
||||||
print(" ✅ Defaults to no thinking")
|
print(" ✅ Defaults to no thinking")
|
||||||
|
|
||||||
if synthesizer.is_available():
|
if synthesizer.is_available():
|
||||||
@ -247,9 +243,7 @@ class TestOllamaIntegration(unittest.TestCase):
|
|||||||
content: str
|
content: str
|
||||||
score: float
|
score: float
|
||||||
|
|
||||||
results = [
|
results = [MockResult("auth.py", "def authenticate(user): return True", 0.95)]
|
||||||
MockResult("auth.py", "def authenticate(user): return True", 0.95)
|
|
||||||
]
|
|
||||||
|
|
||||||
# Test synthesis
|
# Test synthesis
|
||||||
synthesis = synthesizer.synthesize_search_results(
|
synthesis = synthesizer.synthesize_search_results(
|
||||||
@ -283,13 +277,14 @@ class TestOllamaIntegration(unittest.TestCase):
|
|||||||
explorer = CodeExplorer(Path("."), self.config)
|
explorer = CodeExplorer(Path("."), self.config)
|
||||||
|
|
||||||
# Should enable thinking
|
# Should enable thinking
|
||||||
self.assertTrue(explorer.synthesizer.enable_thinking,
|
self.assertTrue(
|
||||||
"Exploration mode should enable thinking")
|
explorer.synthesizer.enable_thinking,
|
||||||
|
"Exploration mode should enable thinking",
|
||||||
|
)
|
||||||
print(" ✅ Enables thinking by default")
|
print(" ✅ Enables thinking by default")
|
||||||
|
|
||||||
# Should have session management
|
# Should have session management
|
||||||
self.assertIsNone(explorer.current_session,
|
self.assertIsNone(explorer.current_session, "Should start with no active session")
|
||||||
"Should start with no active session")
|
|
||||||
print(" ✅ Session management available")
|
print(" ✅ Session management available")
|
||||||
|
|
||||||
# Should handle session summary gracefully
|
# Should handle session summary gracefully
|
||||||
@ -313,21 +308,20 @@ class TestOllamaIntegration(unittest.TestCase):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
from mini_rag.explorer import CodeExplorer
|
from mini_rag.explorer import CodeExplorer
|
||||||
|
|
||||||
explorer = CodeExplorer(Path("."), self.config)
|
explorer = CodeExplorer(Path("."), self.config)
|
||||||
except ImportError:
|
except ImportError:
|
||||||
self.skipTest("⏭️ CodeExplorer not available")
|
self.skipTest("⏭️ CodeExplorer not available")
|
||||||
|
|
||||||
# Should have different thinking settings
|
# Should have different thinking settings
|
||||||
self.assertFalse(synthesizer.enable_thinking,
|
self.assertFalse(synthesizer.enable_thinking, "Synthesis should not use thinking")
|
||||||
"Synthesis should not use thinking")
|
self.assertTrue(
|
||||||
self.assertTrue(explorer.synthesizer.enable_thinking,
|
explorer.synthesizer.enable_thinking, "Exploration should use thinking"
|
||||||
"Exploration should use thinking")
|
)
|
||||||
|
|
||||||
# Both should be uninitialized (lazy loading)
|
# Both should be uninitialized (lazy loading)
|
||||||
self.assertFalse(synthesizer._initialized,
|
self.assertFalse(synthesizer._initialized, "Should use lazy loading")
|
||||||
"Should use lazy loading")
|
self.assertFalse(explorer.synthesizer._initialized, "Should use lazy loading")
|
||||||
self.assertFalse(explorer.synthesizer._initialized,
|
|
||||||
"Should use lazy loading")
|
|
||||||
|
|
||||||
print(" ✅ Clean mode separation confirmed")
|
print(" ✅ Clean mode separation confirmed")
|
||||||
|
|
||||||
@ -346,17 +340,17 @@ class TestOllamaIntegration(unittest.TestCase):
|
|||||||
mock_embedding_response = MagicMock()
|
mock_embedding_response = MagicMock()
|
||||||
mock_embedding_response.status_code = 200
|
mock_embedding_response.status_code = 200
|
||||||
mock_embedding_response.json.return_value = {
|
mock_embedding_response.json.return_value = {
|
||||||
'embedding': [0.1] * 768 # Standard embedding size
|
"embedding": [0.1] * 768 # Standard embedding size
|
||||||
}
|
}
|
||||||
|
|
||||||
# Mock LLM response
|
# Mock LLM response
|
||||||
mock_llm_response = MagicMock()
|
mock_llm_response = MagicMock()
|
||||||
mock_llm_response.status_code = 200
|
mock_llm_response.status_code = 200
|
||||||
mock_llm_response.json.return_value = {
|
mock_llm_response.json.return_value = {
|
||||||
'response': 'authentication login user verification credentials'
|
"response": "authentication login user verification credentials"
|
||||||
}
|
}
|
||||||
|
|
||||||
with patch('requests.post', side_effect=[mock_embedding_response, mock_llm_response]):
|
with patch("requests.post", side_effect=[mock_embedding_response, mock_llm_response]):
|
||||||
# Test query expansion with mocked response
|
# Test query expansion with mocked response
|
||||||
expander = QueryExpander(self.config)
|
expander = QueryExpander(self.config)
|
||||||
expander.enabled = True
|
expander.enabled = True
|
||||||
@ -369,7 +363,7 @@ class TestOllamaIntegration(unittest.TestCase):
|
|||||||
print(" ⚠️ Expansion returned None (might be expected)")
|
print(" ⚠️ Expansion returned None (might be expected)")
|
||||||
|
|
||||||
# Test graceful degradation when Ollama unavailable
|
# Test graceful degradation when Ollama unavailable
|
||||||
with patch('requests.get', side_effect=requests.exceptions.ConnectionError()):
|
with patch("requests.get", side_effect=requests.exceptions.ConnectionError()):
|
||||||
expander_offline = QueryExpander(self.config)
|
expander_offline = QueryExpander(self.config)
|
||||||
|
|
||||||
# Should handle unavailable server gracefully
|
# Should handle unavailable server gracefully
|
||||||
@ -397,14 +391,14 @@ class TestOllamaIntegration(unittest.TestCase):
|
|||||||
self.assertTrue(isinstance(self.config.llm.max_expansion_terms, int))
|
self.assertTrue(isinstance(self.config.llm.max_expansion_terms, int))
|
||||||
self.assertGreater(self.config.llm.max_expansion_terms, 0)
|
self.assertGreater(self.config.llm.max_expansion_terms, 0)
|
||||||
|
|
||||||
print(f" ✅ LLM config valid")
|
print(" ✅ LLM config valid")
|
||||||
print(f" Host: {self.config.llm.ollama_host}")
|
print(f" Host: {self.config.llm.ollama_host}")
|
||||||
print(f" Max expansion terms: {self.config.llm.max_expansion_terms}")
|
print(f" Max expansion terms: {self.config.llm.max_expansion_terms}")
|
||||||
|
|
||||||
# Check search config
|
# Check search config
|
||||||
self.assertIsNotNone(self.config.search)
|
self.assertIsNotNone(self.config.search)
|
||||||
self.assertGreater(self.config.search.default_top_k, 0)
|
self.assertGreater(self.config.search.default_top_k, 0)
|
||||||
print(f" ✅ Search config valid")
|
print(" ✅ Search config valid")
|
||||||
print(f" Default top-k: {self.config.search.default_top_k}")
|
print(f" Default top-k: {self.config.search.default_top_k}")
|
||||||
print(f" Query expansion: {self.config.search.expand_queries}")
|
print(f" Query expansion: {self.config.search.expand_queries}")
|
||||||
|
|
||||||
@ -432,5 +426,5 @@ def run_troubleshooting():
|
|||||||
print("📚 For more help, see docs/QUERY_EXPANSION.md")
|
print("📚 For more help, see docs/QUERY_EXPANSION.md")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == "__main__":
|
||||||
run_troubleshooting()
|
run_troubleshooting()
|
||||||
@ -10,21 +10,26 @@ Or run directly with venv:
|
|||||||
source .venv/bin/activate && PYTHONPATH=. python tests/test_rag_integration.py
|
source .venv/bin/activate && PYTHONPATH=. python tests/test_rag_integration.py
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import tempfile
|
|
||||||
import shutil
|
|
||||||
import os
|
import os
|
||||||
|
import tempfile
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from mini_rag.indexer import ProjectIndexer
|
from mini_rag.indexer import ProjectIndexer
|
||||||
from mini_rag.search import CodeSearcher
|
from mini_rag.search import CodeSearcher
|
||||||
|
|
||||||
# Check if virtual environment is activated
|
# Check if virtual environment is activated
|
||||||
|
|
||||||
|
|
||||||
def check_venv():
|
def check_venv():
|
||||||
if 'VIRTUAL_ENV' not in os.environ:
|
if "VIRTUAL_ENV" not in os.environ:
|
||||||
print("⚠️ WARNING: Virtual environment not detected!")
|
print("⚠️ WARNING: Virtual environment not detected!")
|
||||||
print(" This test requires the virtual environment to be activated.")
|
print(" This test requires the virtual environment to be activated.")
|
||||||
print(" Run: source .venv/bin/activate && PYTHONPATH=. python tests/test_rag_integration.py")
|
print(
|
||||||
|
" Run: source .venv/bin/activate && PYTHONPATH=. python tests/test_rag_integration.py"
|
||||||
|
)
|
||||||
print(" Continuing anyway...\n")
|
print(" Continuing anyway...\n")
|
||||||
|
|
||||||
|
|
||||||
check_venv()
|
check_venv()
|
||||||
|
|
||||||
# Sample Python file with proper structure
|
# Sample Python file with proper structure
|
||||||
@ -35,15 +40,16 @@ This module demonstrates various Python constructs.
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
from typing import List, Dict, Optional
|
from typing import List, Optional
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
# Module-level constants
|
# Module-level constants
|
||||||
DEFAULT_TIMEOUT = 30
|
DEFAULT_TIMEOUT = 30
|
||||||
MAX_RETRIES = 3
|
MAX_RETRIES = 3
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
"""Configuration dataclass."""
|
"""Configuration dataclass."""
|
||||||
timeout: int = DEFAULT_TIMEOUT
|
timeout: int = DEFAULT_TIMEOUT
|
||||||
@ -99,7 +105,6 @@ class DataProcessor:
|
|||||||
# Implementation details
|
# Implementation details
|
||||||
return {**item, 'processed': True}
|
return {**item, 'processed': True}
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
"""Main entry point."""
|
"""Main entry point."""
|
||||||
config = Config()
|
config = Config()
|
||||||
@ -113,13 +118,12 @@ def main():
|
|||||||
results = processor.process(test_data)
|
results = processor.process(test_data)
|
||||||
print(f"Processed {len(results)} items")
|
print(f"Processed {len(results)} items")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
'''
|
'''
|
||||||
|
|
||||||
# Sample markdown file
|
# Sample markdown file
|
||||||
sample_markdown = '''# RAG System Documentation
|
sample_markdown = """# RAG System Documentation
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
@ -175,7 +179,7 @@ Main class for indexing projects.
|
|||||||
### CodeSearcher
|
### CodeSearcher
|
||||||
|
|
||||||
Provides semantic search capabilities.
|
Provides semantic search capabilities.
|
||||||
'''
|
"""
|
||||||
|
|
||||||
|
|
||||||
def test_integration():
|
def test_integration():
|
||||||
@ -213,40 +217,40 @@ def test_integration():
|
|||||||
|
|
||||||
# Test 1: Search for class with docstring
|
# Test 1: Search for class with docstring
|
||||||
results = searcher.search("data processor class unified interface", top_k=3)
|
results = searcher.search("data processor class unified interface", top_k=3)
|
||||||
print(f"\n Test 1 - Class search:")
|
print("\n Test 1 - Class search:")
|
||||||
for i, result in enumerate(results[:1]):
|
for i, result in enumerate(results[:1]):
|
||||||
print(f" - Match {i+1}: {result.file_path}")
|
print(f" - Match {i+1}: {result.file_path}")
|
||||||
print(f" Chunk type: {result.chunk_type}")
|
print(f" Chunk type: {result.chunk_type}")
|
||||||
print(f" Score: {result.score:.3f}")
|
print(f" Score: {result.score:.3f}")
|
||||||
if 'This class handles' in result.content:
|
if "This class handles" in result.content:
|
||||||
print(" [OK] Docstring included with class")
|
print(" [OK] Docstring included with class")
|
||||||
else:
|
else:
|
||||||
print(" [FAIL] Docstring not found")
|
print(" [FAIL] Docstring not found")
|
||||||
|
|
||||||
# Test 2: Search for method with docstring
|
# Test 2: Search for method with docstring
|
||||||
results = searcher.search("process list of data items", top_k=3)
|
results = searcher.search("process list of data items", top_k=3)
|
||||||
print(f"\n Test 2 - Method search:")
|
print("\n Test 2 - Method search:")
|
||||||
for i, result in enumerate(results[:1]):
|
for i, result in enumerate(results[:1]):
|
||||||
print(f" - Match {i+1}: {result.file_path}")
|
print(f" - Match {i+1}: {result.file_path}")
|
||||||
print(f" Chunk type: {result.chunk_type}")
|
print(f" Chunk type: {result.chunk_type}")
|
||||||
print(f" Parent class: {getattr(result, 'parent_class', 'N/A')}")
|
print(f" Parent class: {getattr(result, 'parent_class', 'N/A')}")
|
||||||
if 'Args:' in result.content and 'Returns:' in result.content:
|
if "Args:" in result.content and "Returns:" in result.content:
|
||||||
print(" [OK] Docstring included with method")
|
print(" [OK] Docstring included with method")
|
||||||
else:
|
else:
|
||||||
print(" [FAIL] Method docstring not complete")
|
print(" [FAIL] Method docstring not complete")
|
||||||
|
|
||||||
# Test 3: Search markdown content
|
# Test 3: Search markdown content
|
||||||
results = searcher.search("smart chunking capabilities markdown", top_k=3)
|
results = searcher.search("smart chunking capabilities markdown", top_k=3)
|
||||||
print(f"\n Test 3 - Markdown search:")
|
print("\n Test 3 - Markdown search:")
|
||||||
for i, result in enumerate(results[:1]):
|
for i, result in enumerate(results[:1]):
|
||||||
print(f" - Match {i+1}: {result.file_path}")
|
print(f" - Match {i+1}: {result.file_path}")
|
||||||
print(f" Chunk type: {result.chunk_type}")
|
print(f" Chunk type: {result.chunk_type}")
|
||||||
print(f" Lines: {result.start_line}-{result.end_line}")
|
print(f" Lines: {result.start_line}-{result.end_line}")
|
||||||
|
|
||||||
# Test 4: Verify chunk navigation
|
# Test 4: Verify chunk navigation
|
||||||
print(f"\n Test 4 - Chunk navigation:")
|
print("\n Test 4 - Chunk navigation:")
|
||||||
all_results = searcher.search("", top_k=100) # Get all chunks
|
all_results = searcher.search("", top_k=100) # Get all chunks
|
||||||
py_chunks = [r for r in all_results if r.file_path.endswith('.py')]
|
py_chunks = [r for r in all_results if r.file_path.endswith(".py")]
|
||||||
|
|
||||||
if py_chunks:
|
if py_chunks:
|
||||||
first_chunk = py_chunks[0]
|
first_chunk = py_chunks[0]
|
||||||
@ -257,9 +261,9 @@ def test_integration():
|
|||||||
valid_chain = True
|
valid_chain = True
|
||||||
for i in range(len(py_chunks) - 1):
|
for i in range(len(py_chunks) - 1):
|
||||||
curr = py_chunks[i]
|
curr = py_chunks[i]
|
||||||
next_chunk = py_chunks[i + 1]
|
# py_chunks[i + 1] # Unused variable removed
|
||||||
expected_next = f"processor_{i+1}"
|
expected_next = f"processor_{i+1}"
|
||||||
if getattr(curr, 'next_chunk_id', None) != expected_next:
|
if getattr(curr, "next_chunk_id", None) != expected_next:
|
||||||
valid_chain = False
|
valid_chain = False
|
||||||
break
|
break
|
||||||
|
|
||||||
|
|||||||
@ -8,17 +8,17 @@ and producing better quality results.
|
|||||||
Run with: python3 tests/test_smart_ranking.py
|
Run with: python3 tests/test_smart_ranking.py
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import unittest
|
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
import unittest
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from unittest.mock import patch, MagicMock
|
from pathlib import Path
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
|
from mini_rag.search import CodeSearcher, SearchResult
|
||||||
|
|
||||||
# Add project to path
|
# Add project to path
|
||||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||||
|
|
||||||
from mini_rag.search import SearchResult, CodeSearcher
|
|
||||||
|
|
||||||
|
|
||||||
class TestSmartRanking(unittest.TestCase):
|
class TestSmartRanking(unittest.TestCase):
|
||||||
"""
|
"""
|
||||||
@ -40,27 +40,31 @@ class TestSmartRanking(unittest.TestCase):
|
|||||||
end_line=2,
|
end_line=2,
|
||||||
chunk_type="text",
|
chunk_type="text",
|
||||||
name="temp",
|
name="temp",
|
||||||
language="text"
|
language="text",
|
||||||
),
|
),
|
||||||
SearchResult(
|
SearchResult(
|
||||||
file_path=Path("README.md"),
|
file_path=Path("README.md"),
|
||||||
content="This is a comprehensive README file\nwith detailed installation instructions\nand usage examples for beginners.",
|
content=(
|
||||||
|
"This is a comprehensive README file\n"
|
||||||
|
"with detailed installation instructions\n"
|
||||||
|
"and usage examples for beginners."
|
||||||
|
),
|
||||||
score=0.7, # Lower initial score
|
score=0.7, # Lower initial score
|
||||||
start_line=1,
|
start_line=1,
|
||||||
end_line=5,
|
end_line=5,
|
||||||
chunk_type="markdown",
|
chunk_type="markdown",
|
||||||
name="Installation Guide",
|
name="Installation Guide",
|
||||||
language="markdown"
|
language="markdown",
|
||||||
),
|
),
|
||||||
SearchResult(
|
SearchResult(
|
||||||
file_path=Path("src/main.py"),
|
file_path=Path("src/main.py"),
|
||||||
content="def main():\n \"\"\"Main application entry point.\"\"\"\n app = create_app()\n return app.run()",
|
content='def main():\n """Main application entry point."""\n app = create_app()\n return app.run()',
|
||||||
score=0.75,
|
score=0.75,
|
||||||
start_line=10,
|
start_line=10,
|
||||||
end_line=15,
|
end_line=15,
|
||||||
chunk_type="function",
|
chunk_type="function",
|
||||||
name="main",
|
name="main",
|
||||||
language="python"
|
language="python",
|
||||||
),
|
),
|
||||||
SearchResult(
|
SearchResult(
|
||||||
file_path=Path("temp/cache_123.log"),
|
file_path=Path("temp/cache_123.log"),
|
||||||
@ -70,8 +74,8 @@ class TestSmartRanking(unittest.TestCase):
|
|||||||
end_line=1,
|
end_line=1,
|
||||||
chunk_type="text",
|
chunk_type="text",
|
||||||
name="log",
|
name="log",
|
||||||
language="text"
|
language="text",
|
||||||
)
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
def test_01_important_file_boost(self):
|
def test_01_important_file_boost(self):
|
||||||
@ -91,8 +95,8 @@ class TestSmartRanking(unittest.TestCase):
|
|||||||
ranked = searcher._smart_rerank(self.mock_results.copy())
|
ranked = searcher._smart_rerank(self.mock_results.copy())
|
||||||
|
|
||||||
# Find README and temp file results
|
# Find README and temp file results
|
||||||
readme_result = next((r for r in ranked if 'README' in str(r.file_path)), None)
|
readme_result = next((r for r in ranked if "README" in str(r.file_path)), None)
|
||||||
temp_result = next((r for r in ranked if 'temp' in str(r.file_path)), None)
|
temp_result = next((r for r in ranked if "temp" in str(r.file_path)), None)
|
||||||
|
|
||||||
self.assertIsNotNone(readme_result)
|
self.assertIsNotNone(readme_result)
|
||||||
self.assertIsNotNone(temp_result)
|
self.assertIsNotNone(temp_result)
|
||||||
@ -124,7 +128,7 @@ class TestSmartRanking(unittest.TestCase):
|
|||||||
|
|
||||||
# Find short and long content results
|
# Find short and long content results
|
||||||
short_result = next((r for r in ranked if len(r.content.strip()) < 20), None)
|
short_result = next((r for r in ranked if len(r.content.strip()) < 20), None)
|
||||||
structured_result = next((r for r in ranked if 'README' in str(r.file_path)), None)
|
structured_result = next((r for r in ranked if "README" in str(r.file_path)), None)
|
||||||
|
|
||||||
if short_result:
|
if short_result:
|
||||||
# Short content should be penalized (score * 0.9)
|
# Short content should be penalized (score * 0.9)
|
||||||
@ -133,7 +137,7 @@ class TestSmartRanking(unittest.TestCase):
|
|||||||
|
|
||||||
if structured_result:
|
if structured_result:
|
||||||
# Well-structured content gets small boost (score * 1.02)
|
# Well-structured content gets small boost (score * 1.02)
|
||||||
lines = structured_result.content.strip().split('\n')
|
lines = structured_result.content.strip().split("\n")
|
||||||
if len(lines) >= 3:
|
if len(lines) >= 3:
|
||||||
print(f" 📈 Structured content boosted: {structured_result.score:.3f}")
|
print(f" 📈 Structured content boosted: {structured_result.score:.3f}")
|
||||||
print(f" ({len(lines)} lines of content)")
|
print(f" ({len(lines)} lines of content)")
|
||||||
@ -155,7 +159,7 @@ class TestSmartRanking(unittest.TestCase):
|
|||||||
ranked = searcher._smart_rerank(self.mock_results.copy())
|
ranked = searcher._smart_rerank(self.mock_results.copy())
|
||||||
|
|
||||||
# Find function result
|
# Find function result
|
||||||
function_result = next((r for r in ranked if r.chunk_type == 'function'), None)
|
function_result = next((r for r in ranked if r.chunk_type == "function"), None)
|
||||||
|
|
||||||
if function_result:
|
if function_result:
|
||||||
# Function should get boost (original score * 1.1)
|
# Function should get boost (original score * 1.1)
|
||||||
@ -168,7 +172,7 @@ class TestSmartRanking(unittest.TestCase):
|
|||||||
|
|
||||||
self.assertTrue(True)
|
self.assertTrue(True)
|
||||||
|
|
||||||
@patch('pathlib.Path.stat')
|
@patch("pathlib.Path.stat")
|
||||||
def test_04_recency_boost(self, mock_stat):
|
def test_04_recency_boost(self, mock_stat):
|
||||||
"""
|
"""
|
||||||
✅ Test that recently modified files get ranking boosts.
|
✅ Test that recently modified files get ranking boosts.
|
||||||
@ -184,7 +188,7 @@ class TestSmartRanking(unittest.TestCase):
|
|||||||
def mock_stat_side_effect(file_path):
|
def mock_stat_side_effect(file_path):
|
||||||
mock_stat_obj = MagicMock()
|
mock_stat_obj = MagicMock()
|
||||||
|
|
||||||
if 'README' in str(file_path):
|
if "README" in str(file_path):
|
||||||
# Recent file (2 days ago)
|
# Recent file (2 days ago)
|
||||||
recent_time = (now - timedelta(days=2)).timestamp()
|
recent_time = (now - timedelta(days=2)).timestamp()
|
||||||
mock_stat_obj.st_mtime = recent_time
|
mock_stat_obj.st_mtime = recent_time
|
||||||
@ -199,13 +203,13 @@ class TestSmartRanking(unittest.TestCase):
|
|||||||
mock_stat.side_effect = lambda: mock_stat_side_effect("dummy")
|
mock_stat.side_effect = lambda: mock_stat_side_effect("dummy")
|
||||||
|
|
||||||
# Patch the Path constructor to return mocked paths
|
# Patch the Path constructor to return mocked paths
|
||||||
with patch.object(Path, 'stat', side_effect=mock_stat_side_effect):
|
with patch.object(Path, "stat", side_effect=mock_stat_side_effect):
|
||||||
searcher = MagicMock()
|
searcher = MagicMock()
|
||||||
searcher._smart_rerank = CodeSearcher._smart_rerank.__get__(searcher)
|
searcher._smart_rerank = CodeSearcher._smart_rerank.__get__(searcher)
|
||||||
|
|
||||||
ranked = searcher._smart_rerank(self.mock_results.copy())
|
ranked = searcher._smart_rerank(self.mock_results.copy())
|
||||||
|
|
||||||
readme_result = next((r for r in ranked if 'README' in str(r.file_path)), None)
|
readme_result = next((r for r in ranked if "README" in str(r.file_path)), None)
|
||||||
|
|
||||||
if readme_result:
|
if readme_result:
|
||||||
# Recent file should get boost
|
# Recent file should get boost
|
||||||
@ -243,15 +247,19 @@ class TestSmartRanking(unittest.TestCase):
|
|||||||
self.assertEqual(scores, sorted(scores, reverse=True))
|
self.assertEqual(scores, sorted(scores, reverse=True))
|
||||||
|
|
||||||
# 2. README should rank higher than temp files
|
# 2. README should rank higher than temp files
|
||||||
readme_pos = next((i for i, r in enumerate(ranked) if 'README' in str(r.file_path)), None)
|
readme_pos = next(
|
||||||
temp_pos = next((i for i, r in enumerate(ranked) if 'temp' in str(r.file_path)), None)
|
(i for i, r in enumerate(ranked) if "README" in str(r.file_path)), None
|
||||||
|
)
|
||||||
|
temp_pos = next((i for i, r in enumerate(ranked) if "temp" in str(r.file_path)), None)
|
||||||
|
|
||||||
if readme_pos is not None and temp_pos is not None:
|
if readme_pos is not None and temp_pos is not None:
|
||||||
self.assertLess(readme_pos, temp_pos)
|
self.assertLess(readme_pos, temp_pos)
|
||||||
print(f" ✅ README ranks #{readme_pos + 1}, temp file ranks #{temp_pos + 1}")
|
print(f" ✅ README ranks #{readme_pos + 1}, temp file ranks #{temp_pos + 1}")
|
||||||
|
|
||||||
# 3. Function/code should rank well
|
# 3. Function/code should rank well
|
||||||
function_pos = next((i for i, r in enumerate(ranked) if r.chunk_type == 'function'), None)
|
function_pos = next(
|
||||||
|
(i for i, r in enumerate(ranked) if r.chunk_type == "function"), None
|
||||||
|
)
|
||||||
if function_pos is not None:
|
if function_pos is not None:
|
||||||
self.assertLess(function_pos, len(ranked) // 2) # Should be in top half
|
self.assertLess(function_pos, len(ranked) // 2) # Should be in top half
|
||||||
print(f" ✅ Function code ranks #{function_pos + 1}")
|
print(f" ✅ Function code ranks #{function_pos + 1}")
|
||||||
@ -274,7 +282,7 @@ class TestSmartRanking(unittest.TestCase):
|
|||||||
|
|
||||||
# Time the ranking operation
|
# Time the ranking operation
|
||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
ranked = searcher._smart_rerank(self.mock_results.copy())
|
# searcher._smart_rerank(self.mock_results.copy()) # Unused variable removed
|
||||||
end_time = time.time()
|
end_time = time.time()
|
||||||
|
|
||||||
ranking_time = (end_time - start_time) * 1000 # Convert to milliseconds
|
ranking_time = (end_time - start_time) * 1000 # Convert to milliseconds
|
||||||
@ -310,5 +318,5 @@ def run_ranking_tests():
|
|||||||
print(" • All boosts are cumulative for maximum quality")
|
print(" • All boosts are cumulative for maximum quality")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == "__main__":
|
||||||
run_ranking_tests()
|
run_ranking_tests()
|
||||||
@ -8,13 +8,14 @@ and helps identify what's working and what needs attention.
|
|||||||
Run with: python3 tests/troubleshoot.py
|
Run with: python3 tests/troubleshoot.py
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import sys
|
|
||||||
import subprocess
|
import subprocess
|
||||||
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
# Add project to path
|
# Add project to path
|
||||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
"""Run comprehensive troubleshooting checks."""
|
"""Run comprehensive troubleshooting checks."""
|
||||||
|
|
||||||
@ -52,6 +53,7 @@ def main():
|
|||||||
print(" • Start Ollama server: ollama serve")
|
print(" • Start Ollama server: ollama serve")
|
||||||
print(" • Install models: ollama pull qwen3:4b")
|
print(" • Install models: ollama pull qwen3:4b")
|
||||||
|
|
||||||
|
|
||||||
def run_test(test_file):
|
def run_test(test_file):
|
||||||
"""Run a specific test file."""
|
"""Run a specific test file."""
|
||||||
test_path = Path(__file__).parent / test_file
|
test_path = Path(__file__).parent / test_file
|
||||||
@ -62,9 +64,9 @@ def run_test(test_file):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
# Run the test
|
# Run the test
|
||||||
result = subprocess.run([
|
result = subprocess.run(
|
||||||
sys.executable, str(test_path)
|
[sys.executable, str(test_path)], capture_output=True, text=True, timeout=60
|
||||||
], capture_output=True, text=True, timeout=60)
|
)
|
||||||
|
|
||||||
# Show output
|
# Show output
|
||||||
if result.stdout:
|
if result.stdout:
|
||||||
@ -82,5 +84,6 @@ def run_test(test_file):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"❌ Error running {test_file}: {e}")
|
print(f"❌ Error running {test_file}: {e}")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
Loading…
x
Reference in New Issue
Block a user