pirag 0.1.7__py3-none-any.whl → 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
app/main.py CHANGED
@@ -1,81 +1,49 @@
1
- import argparse, os
2
- from dotenv import load_dotenv
3
1
  from loguru import logger
2
+ from argparse import ArgumentParser, ArgumentDefaultsHelpFormatter
4
3
 
5
- load_dotenv(dotenv_path=os.environ.get('ENV_FILE', '.env'), override=True)
4
+ import app.rag.config as cfn
5
+ import app.rag.api as api
6
+ import app.rag.cli as cli
6
7
 
7
- from app.rag.config import top_parser, common_parser, setup_logger
8
- from app.rag.doctor import help as doctor_help, parser as doctor_parser, route as doctor_route
9
- from app.rag.train import help as train_help, parser as train_parser, route as train_route
10
- from app.rag.ask import help as ask_help, parser as ask_parser, route as ask_route
11
- from app.rag.test import help as test_help, parser as test_parser, route as test_route
8
+ # Command definitions
9
+ commands = {
10
+ "serve" : ("Start the RAG server", "Run a FastAPI-based RAG server", api.serve),
11
+ "chat" : ("Chat with the RAG system", "Run an interactive chat with the RAG system", cli.chat),
12
+ "train" : ("Train the RAG system", "Run a pipeline to train the RAG system", cli.train),
13
+ "test" : ("Test the RAG system", "Run a pipeline to test the RAG system", cli.test),
14
+ "doctor" : ("Diagnose the RAG system", "Run a pipeline to diagnose the RAG system", cli.doctor),
15
+ }
12
16
 
13
17
  # Main parser
14
- parser = argparse.ArgumentParser(
15
- formatter_class = argparse.ArgumentDefaultsHelpFormatter,
16
- description = 'Pilot of On-Premise RAG',
17
- parents = [top_parser],
18
+ parser = ArgumentParser(
19
+ formatter_class = ArgumentDefaultsHelpFormatter,
20
+ description = """
21
+ Pilot of On-Premise RAG.
22
+ """,
23
+ parents = [cfn.top_parser, cfn.common_parser],
18
24
  add_help = False,
19
25
  )
20
26
 
21
- # Commands
22
- subparsers = parser.add_subparsers(
23
- title = 'commands',
24
- dest = 'command',
25
- )
26
-
27
- subparsers.add_parser(
28
- 'doctor',
29
- help = doctor_help,
30
- description = doctor_parser.description,
31
- parents = [top_parser, common_parser, doctor_parser],
32
- add_help = False,
33
- )
34
-
35
- subparsers.add_parser(
36
- 'train',
37
- help = train_help,
38
- description = train_parser.description,
39
- parents = [top_parser, common_parser, train_parser],
40
- add_help = False,
41
- )
42
-
43
- subparsers.add_parser(
44
- 'test',
45
- help = test_help,
46
- description = test_parser.description,
47
- parents = [top_parser, common_parser, test_parser],
48
- add_help = False,
49
- )
50
-
51
- subparsers.add_parser(
52
- 'ask',
53
- help = ask_help,
54
- description = ask_parser.description,
55
- parents = [top_parser, common_parser, ask_parser],
56
- add_help = False,
57
- )
27
+ # Add command parsers
28
+ subparsers = parser.add_subparsers(title="commands", dest="command")
29
+ for name, (help, description, _) in commands.items():
30
+ subparsers.add_parser(
31
+ name = name,
32
+ help = help,
33
+ description = description,
34
+ parents = [cfn.common_parser],
35
+ add_help = False,
36
+ )
58
37
 
59
38
  def main():
60
39
  args = parser.parse_args()
61
- setup_logger(
62
- log_level = args.log_level,
63
- log_dir = args.log_dir,
64
- )
65
- command_message = f"with command: {args.command}" if args.command else ""
66
- logger.info(f"RAG Started {command_message}")
40
+ cfn.setup_logger(cfn.LOG_LEVEL, cfn.LOG_SAVE, cfn.LOG_DIR)
67
41
  logger.debug(f"Parsed arguments: {args}")
68
42
 
69
- if args.command == 'doctor':
70
- doctor_route(args)
71
- elif args.command == 'ask':
72
- ask_route(args)
73
- elif args.command == 'train':
74
- train_route(args)
75
- elif args.command == 'test':
76
- test_route(args)
43
+ if func := commands.get(args.command):
44
+ func[-1]()
77
45
  else:
78
46
  parser.print_help()
79
47
 
80
- if __name__ == '__main__':
48
+ if __name__ == "__main__":
81
49
  main()
app/rag/api.py ADDED
@@ -0,0 +1,50 @@
1
+ import uvicorn
2
+ from fastapi import FastAPI, Request, Depends, HTTPException, Query
3
+ from fastapi.middleware.cors import CORSMiddleware
4
+
5
+ from loguru import logger
6
+ import app.rag.config as cfn
7
+ from app.rag.v1.router import router as core_router
8
+
9
+ # Initialize FastAPI app
10
+ api = FastAPI(
11
+ title = "RAG API",
12
+ description = "API for Retrieval-Augmented Generation",
13
+ version = cfn.__version__,
14
+ )
15
+
16
+ # Add CORS middleware
17
+ api.add_middleware(
18
+ CORSMiddleware,
19
+ allow_origins=["*"], # Adjust in production
20
+ allow_credentials=True,
21
+ allow_methods=["*"],
22
+ allow_headers=["*"],
23
+ )
24
+
25
+
26
+ @api.get("/")
27
+ async def _():
28
+ return {"message": "RAG API is running"}
29
+
30
+
31
+ @api.get("/livez")
32
+ async def _():
33
+ return {"status": "ok"}
34
+
35
+
36
+ @api.get("/readyz")
37
+ async def _():
38
+ return {"status": "ok"}
39
+
40
+ api.include_router(router=core_router, prefix="/v1")
41
+
42
+ def serve():
43
+ print("Serving the RAG API...")
44
+ print(cfn.API_HOST, cfn.API_PORT, cfn.API_RELOAD)
45
+ uvicorn.run(
46
+ app = "app.rag.api:api",
47
+ host = cfn.API_HOST,
48
+ port = cfn.API_PORT,
49
+ reload = cfn.API_RELOAD,
50
+ )
app/rag/cli.py ADDED
@@ -0,0 +1,33 @@
1
+ import app.rag.config as cfn
2
+ from loguru import logger
3
+
4
+ from app.rag.llm.service import doctor as doctor_llm
5
+ from app.rag.embedding.service import doctor as doctor_embedding
6
+ from app.rag.vector_store.service import doctor as doctor_vector_store
7
+
8
+ def chat():
9
+ print("Chatting with the RAG system...")
10
+
11
+
12
+ def train():
13
+ print("Training the RAG system...")
14
+
15
+
16
+ def test():
17
+ print("Testing the RAG system...")
18
+
19
+
20
+ def doctor():
21
+ logger.info("💚 Doctoring the RAG system...")
22
+
23
+ # -- LLM Server
24
+ logger.info("Checking the LLM server (OpenAI-compatible)...")
25
+ doctor_llm()
26
+
27
+ # -- Embedding Server
28
+ logger.info("Checking the embedding server (OpenAI-compatible)...")
29
+ doctor_embedding()
30
+
31
+ # -- Vector Store
32
+ logger.info("Checking the vector store server (Milvus)...")
33
+ doctor_vector_store()
app/rag/config.py CHANGED
@@ -1,35 +1,82 @@
1
- import argparse, os, sys
2
- from pathlib import Path
1
+ import argparse, sys, pathlib
3
2
  from loguru import logger
3
+ from dynaconf import Dynaconf
4
+ from importlib.metadata import version, PackageNotFoundError
5
+ try:
6
+ __version__ = version("pirag")
7
+ except PackageNotFoundError:
8
+ __version__ = "0.0.0"
9
+
10
+
11
+ # -- Load configuration
12
+ settings = Dynaconf(
13
+ settings_files = ["settings.yaml"],
14
+ envvar_prefix = False,
15
+ load_dotenv = False,
16
+ )
17
+
18
+
19
+ # -- Loging
20
+ LOG_LEVEL: str = settings.get("LOG.LEVEL", "INFO").upper()
21
+ if LOG_LEVEL not in ["INFO", "DEBUG", "WARNING", "ERROR", "CRITICAL"]:
22
+ raise ValueError(f"Invalid log level: {LOG_LEVEL}. Must be one of: INFO, DEBUG, WARNING, ERROR, CRITICAL")
23
+
24
+ LOG_SAVE: bool = settings.get("LOG.SAVE", False)
25
+ LOG_DIR: str = settings.get("LOG.DIR", ".pirag/logs")
4
26
 
5
- # Logger format constants
6
27
  LOG_TIME_FORMAT = "{time:YYYY-MM-DD HH:mm:ss.SSS!UTC}Z"
7
28
  LOG_FILE_FORMAT = f"{LOG_TIME_FORMAT} | {{level: <8}} | {{name}}:{{function}}:{{line}} - {{message}}"
8
29
  LOG_CONSOLE_FORMAT_FULL = f"<green>{LOG_TIME_FORMAT}</green> | <level>{{level: <8}}</level> | <cyan>{{name}}</cyan>:<cyan>{{function}}</cyan>:<cyan>{{line}}</cyan> - <level>{{message}}</level>\n"
9
30
  LOG_CONSOLE_FORMAT_SIMPLE = f"<green>{LOG_TIME_FORMAT}</green> | <level>{{level: <8}}</level> | <level>{{message}}</level>\n"
10
31
 
11
- def setup_logger(log_level: str, log_dir: str):
32
+
33
+ # -- Serving API
34
+ API_HOST: str = settings.get("API.HOST", "0.0.0.0")
35
+ API_PORT: int = settings.get("API.PORT", 8000)
36
+ API_RELOAD: bool = settings.get("API.RELOAD", True)
37
+
38
+
39
+ # -- LLM Server
40
+ LLM_BASE_URL: str = settings.get("LLM.BASE_URL", "http://localhost:11434")
41
+ LLM_API_KEY: str = settings.get("LLM.API_KEY", "llm_api_key")
42
+ LLM_MODEL: str = settings.get("LLM.MODEL", "gemma3:4b")
43
+
44
+
45
+ # -- Embedding Server
46
+ EMBEDDING_BASE_URL: str = settings.get("EMBEDDING.BASE_URL", "http://localhost:11434")
47
+ EMBEDDING_API_KEY: str = settings.get("EMBEDDING.API_KEY", "embedding_api_key")
48
+ EMBEDDING_MODEL: str = settings.get("EMBEDDING.MODEL", "nomic-embed-text:latest")
49
+ EMBEDDING_DIMENSION: int = settings.get("EMBEDDING.DIMENSION", 768)
50
+
51
+
52
+ # -- Data Warehouse
53
+ MINIO_BASE_URL: str = settings.get("MINIO.BASE_URL", "http://localhost:9000")
54
+ MINIO_ACCESS_KEY: str = settings.get("MINIO.ACCESS_KEY", "minioadmin")
55
+ MINIO_SECRET_KEY: str = settings.get("MINIO.SECRET_KEY", "minioadmin")
56
+ MINIO_BUCKET: str = settings.get("MINIO.BUCKET", "pirag")
57
+ MINIO_REGION: str = settings.get("MINIO.REGION", "us-east-1")
58
+
59
+
60
+ # -- Vector Store
61
+ MILVUS_BASE_URL: str = settings.get("MILVUS.BASE_URL", "http://localhost:19530")
62
+ MILVUS_USER: str = settings.get("MILVUS.USER", "milvus")
63
+ MILVUS_PASSWORD: str = settings.get("MILVUS.PASSWORD", "milvus")
64
+ MILVUS_DATABASE: str = settings.get("MILVUS.DATABASE", "milvus_database")
65
+ MILVUS_COLLECTION: str = settings.get("MILVUS.COLLECTION", "milvus_collection")
66
+ MILVUS_METRIC_TYPE: str = settings.get("MILVUS.METRIC_TYPE", "IP")
67
+
68
+
69
+ # -- Monitoring
70
+ LANGFUSE_BASE_URL: str = settings.get("LANGFUSE.BASE_URL", "http://localhost:8000")
71
+ LANGFUSE_API_KEY: str = settings.get("LANGFUSE.API_KEY", "langfuse_api_key")
72
+ LANGFUSE_PROJECT_ID: str = settings.get("LANGFUSE.PROJECT_ID", "langfuse_project_id")
73
+
74
+
75
+ def setup_logger(log_level: str, log_save: bool, log_dir: str):
12
76
  """Configure logger with specified level and outputs"""
13
-
14
- log_dir = Path(log_dir)
15
- log_dir.mkdir(exist_ok=True, parents=True)
16
-
77
+
17
78
  logger.remove()
18
-
19
- # File handler
20
- logger.add(
21
- sink = log_dir / "{time:YYYYMMDD-HHmmss!UTC}Z.log",
22
- level = log_level,
23
- rotation = "100 MB",
24
- retention = 0,
25
- format = LOG_FILE_FORMAT,
26
- serialize = False,
27
- enqueue = True,
28
- backtrace = True,
29
- diagnose = True,
30
- catch = True
31
- )
32
-
79
+
33
80
  # Console handler
34
81
  logger.add(
35
82
  sink = sys.stderr,
@@ -38,114 +85,40 @@ def setup_logger(log_level: str, log_dir: str):
38
85
  colorize = True
39
86
  )
40
87
 
41
-
42
- class EnvDefault(argparse.Action):
43
- """Custom argparse action that uses environment variables as defaults.
44
-
45
- This action extends the standard argparse.Action to support reading default values
46
- from environment variables. If the specified environment variable exists, its value
47
- will be used as the default value for the argument.
48
-
49
- For boolean flags (store_true/store_false), the environment variable is interpreted
50
- as a boolean value where 'true', '1', 'yes', or 'on' (case-insensitive) are
51
- considered True.
52
-
53
- Args:
54
- envvar (str): Name of the environment variable to use as default
55
- required (bool, optional): Whether the argument is required. Defaults to True.
56
- Note: If a default value is found in environment variables, required is set to False.
57
- default (Any, optional): Default value if environment variable is not set. Defaults to None.
58
- **kwargs: Additional arguments passed to argparse.Action
59
-
60
- Example:
61
- ```python
62
- parser.add_argument(
63
- '--log-level',
64
- envvar='LOG_LEVEL',
65
- help='Logging level',
66
- default='INFO',
67
- action=EnvDefault
88
+ if log_save:
89
+ log_dir = pathlib.Path(log_dir)
90
+ log_dir.mkdir(exist_ok=True, parents=True)
91
+
92
+ # File handler
93
+ logger.add(
94
+ sink = log_dir / "{time:YYYYMMDD-HHmmss!UTC}Z.log",
95
+ level = log_level,
96
+ rotation = "100 MB",
97
+ retention = 0,
98
+ format = LOG_FILE_FORMAT,
99
+ serialize = False,
100
+ enqueue = True,
101
+ backtrace = True,
102
+ diagnose = True,
103
+ catch = True
68
104
  )
69
- ```
70
-
71
- Note:
72
- The help text is automatically updated to include the environment variable name.
73
- """
74
- def __init__(self, envvar, required=True, default=None, **kwargs):
75
- if envvar and envvar in os.environ:
76
- env_value = os.environ[envvar]
77
- # Convert string environment variable to boolean
78
- if kwargs.get('nargs') is None and kwargs.get('const') is not None: # store_true/store_false case
79
- default = env_value.lower() in ('true', '1', 'yes', 'on')
80
- else:
81
- default = env_value
82
- logger.debug(f"Using {envvar}={default} from environment")
83
-
84
- if envvar:
85
- kwargs["help"] += f" (envvar: {envvar})"
86
-
87
- if required and default:
88
- required = False
89
-
90
- super(EnvDefault, self).__init__(default=default, required=required, **kwargs)
91
- self.envvar = envvar
92
-
93
- def __call__(self, parser, namespace, values, option_string=None):
94
- setattr(namespace, self.dest, values if values is not None else self.default)
95
-
96
-
97
- # Top-level parser with common options
98
- top_parser = argparse.ArgumentParser(add_help=False)
99
-
100
- top_parser.add_argument(
101
- '-h', '--help',
102
- help = 'Show help message and exit',
103
- default = argparse.SUPPRESS,
104
- action = 'help',
105
- )
106
105
 
107
- top_parser.add_argument(
108
- '--env-file',
109
- envvar = 'ENV_FILE',
110
- help = 'Path to environment file',
111
- default = '.env',
112
- type = str,
113
- action = EnvDefault,
114
- )
115
-
116
- top_parser.add_argument(
117
- '--log-level',
118
- envvar = 'LOG_LEVEL',
119
- help = 'Logging level',
120
- default = 'INFO',
121
- type = lambda x: x.upper(),
122
- choices = ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'],
123
- required = False,
124
- action = EnvDefault,
125
- )
126
106
 
107
+ # Top-level parser
108
+ top_parser = argparse.ArgumentParser(add_help=False)
127
109
  top_parser.add_argument(
128
- '--log-dir',
129
- envvar = 'LOG_DIR',
130
- help = 'Path to log directory',
131
- default = '.pirag/logs',
132
- type = str,
133
- required = False,
134
- action = EnvDefault,
110
+ "-v", "--version",
111
+ help = "Show the `pirag` application's version and exit",
112
+ action = "version",
113
+ version = f"{__version__}",
135
114
  )
136
115
 
137
- top_parser.add_argument(
138
- '--log-save',
139
- envvar = 'LOG_SAVE',
140
- help = 'Save log to file. If this flag is set, the log will be saved to the file specified in the `--log-path`.',
141
- default = False,
142
- const = True,
143
- nargs = 0,
144
- type = bool,
145
- required = False,
146
- action = EnvDefault,
147
- )
148
116
 
149
- common_parser = argparse.ArgumentParser(
150
- add_help = False,
117
+ # Common parser
118
+ common_parser = argparse.ArgumentParser(add_help=False)
119
+ common_parser.add_argument(
120
+ "-h", "--help",
121
+ help = "Show help message and exit",
122
+ default = argparse.SUPPRESS,
123
+ action = "help",
151
124
  )
@@ -0,0 +1,70 @@
1
+ import requests
2
+ from langchain_openai.embeddings import OpenAIEmbeddings
3
+
4
+ import app.rag.config as cfn
5
+ from app.rag.utils import connection_check
6
+
7
+
8
+ class EmbeddingClient:
9
+ def __init__(self, base_url: str, api_key: str, model: str):
10
+ self.base_url = base_url
11
+ self.api_key = api_key
12
+ self.model = model
13
+ self._is_connected = True
14
+ self._client = None
15
+
16
+ if self.check_connection():
17
+ try:
18
+ self._client = OpenAIEmbeddings(
19
+ base_url = base_url,
20
+ api_key = api_key,
21
+ model = model
22
+ )
23
+ except Exception as e:
24
+ self._is_connected = False
25
+
26
+ def check_connection(self) -> bool:
27
+ """Check if the embedding server is accessible"""
28
+ try:
29
+ requests.head(url=self.base_url, timeout=5)
30
+ except requests.exceptions.ConnectionError:
31
+ self._is_connected = False
32
+ return False
33
+ self._is_connected = True
34
+ return True
35
+
36
+ @connection_check
37
+ def generate(self, prompt: str) -> str:
38
+ """Generate text from prompt"""
39
+ if not self._is_connected or self._client is None:
40
+ return ""
41
+ return self._client.embed_query(prompt)
42
+
43
+ @connection_check
44
+ def list_models(self) -> list:
45
+ """List available models"""
46
+ if not self._is_connected:
47
+ return []
48
+ try:
49
+ response = requests.get(
50
+ f"{self.base_url}/models",
51
+ headers={"Authorization": f"Bearer {self.api_key}"}
52
+ )
53
+ if response.status_code == 200:
54
+ return [model['id'] for model in response.json()['data']]
55
+ return []
56
+ except Exception:
57
+ return []
58
+
59
+ @connection_check
60
+ def has_model(self, model: str) -> bool:
61
+ """Check if model exists"""
62
+ if not self._is_connected:
63
+ return False
64
+ return model in self.list_models()
65
+
66
+ client = EmbeddingClient(
67
+ base_url = cfn.EMBEDDING_BASE_URL,
68
+ api_key = cfn.EMBEDDING_API_KEY,
69
+ model = cfn.EMBEDDING_MODEL,
70
+ )
@@ -0,0 +1,26 @@
1
+ from loguru import logger
2
+
3
+ import app.rag.config as cfn
4
+ from .client import client
5
+
6
+ def doctor():
7
+ # Check connection
8
+ is_connected = client.check_connection()
9
+ if not is_connected:
10
+ logger.error(f"- ❌ FAILED: Embedding connection ({cfn.EMBEDDING_BASE_URL})")
11
+ else:
12
+ logger.info(f"- ✅ PASSED: Embedding connection ({cfn.EMBEDDING_BASE_URL})")
13
+
14
+ # Check model availability
15
+ try:
16
+ if not is_connected:
17
+ logger.warning(f"- ⏭️ SKIPPED: Embedding model (Server is not accessible)")
18
+ else:
19
+ # List models
20
+ models = client.list_models()
21
+ if cfn.EMBEDDING_MODEL not in models:
22
+ logger.error(f"- ❌ FAILED: Embedding model not found ({cfn.EMBEDDING_MODEL})")
23
+ else:
24
+ logger.info(f"- ✅ PASSED: Embedding model found (Model `{cfn.EMBEDDING_MODEL}` exists)")
25
+ except Exception as e:
26
+ logger.error(f"- ❌ FAILED: Embedding model check ({str(e)})")
app/rag/llm/client.py ADDED
@@ -0,0 +1,70 @@
1
+ import requests
2
+ from langchain_openai.llms import OpenAI
3
+
4
+ import app.rag.config as cfn
5
+ from app.rag.utils import connection_check
6
+
7
+
8
+ class LLMClient:
9
+ def __init__(self, base_url: str, api_key: str, model: str):
10
+ self.base_url = base_url
11
+ self.api_key = api_key
12
+ self.model = model
13
+ self._is_connected = True
14
+ self._client = None
15
+
16
+ if self.check_connection():
17
+ try:
18
+ self._client = OpenAI(
19
+ base_url = base_url,
20
+ api_key = api_key,
21
+ model = model
22
+ )
23
+ except Exception as e:
24
+ self._is_connected = False
25
+
26
+ def check_connection(self) -> bool:
27
+ """Check if the LLM server is accessible"""
28
+ try:
29
+ requests.head(url=self.base_url, timeout=5)
30
+ except requests.exceptions.ConnectionError:
31
+ self._is_connected = False
32
+ return False
33
+ self._is_connected = True
34
+ return True
35
+
36
+ @connection_check
37
+ def generate(self, prompt: str) -> str:
38
+ """Generate text from prompt"""
39
+ if not self._is_connected or self._client is None:
40
+ return ""
41
+ return self._client.invoke(prompt)
42
+
43
+ @connection_check
44
+ def list_models(self) -> list:
45
+ """List available models"""
46
+ if not self._is_connected:
47
+ return []
48
+ try:
49
+ response = requests.get(
50
+ f"{self.base_url}/models",
51
+ headers={"Authorization": f"Bearer {self.api_key}"}
52
+ )
53
+ if response.status_code == 200:
54
+ return [model['id'] for model in response.json()['data']]
55
+ return []
56
+ except Exception:
57
+ return []
58
+
59
+ @connection_check
60
+ def has_model(self, model: str) -> bool:
61
+ """Check if model exists"""
62
+ if not self._is_connected:
63
+ return False
64
+ return model in self.list_models()
65
+
66
+ client = LLMClient(
67
+ base_url = cfn.LLM_BASE_URL,
68
+ api_key = cfn.LLM_API_KEY,
69
+ model = cfn.LLM_MODEL,
70
+ )
app/rag/llm/service.py ADDED
@@ -0,0 +1,26 @@
1
+ from loguru import logger
2
+
3
+ import app.rag.config as cfn
4
+ from .client import client
5
+
6
+ def doctor():
7
+ # Check connection
8
+ is_connected = client.check_connection()
9
+ if not is_connected:
10
+ logger.error(f"- ❌ FAILED: LLM connection ({cfn.LLM_BASE_URL})")
11
+ else:
12
+ logger.info(f"- ✅ PASSED: LLM connection ({cfn.LLM_BASE_URL})")
13
+
14
+ # Check model availability
15
+ try:
16
+ if not is_connected:
17
+ logger.warning(f"- ⏭️ SKIPPED: LLM model (Server is not accessible)")
18
+ else:
19
+ # List models
20
+ models = client.list_models()
21
+ if cfn.LLM_MODEL not in models:
22
+ logger.error(f"- ❌ FAILED: LLM model not found ({cfn.LLM_MODEL})")
23
+ else:
24
+ logger.info(f"- ✅ PASSED: LLM model found (Model `{cfn.LLM_MODEL}` exists)")
25
+ except Exception as e:
26
+ logger.error(f"- ❌ FAILED: LLM model check ({str(e)})")
app/rag/utils.py ADDED
@@ -0,0 +1,15 @@
1
+ import requests
2
+ from functools import wraps
3
+
4
+ def connection_check(func):
5
+ """Check if the server is accessible. `base_url` and `_is_connected` must be provided."""
6
+ @wraps(func)
7
+ def wrapper(self, *args, **kwargs):
8
+ try:
9
+ requests.head(url=self.base_url, timeout=5)
10
+ self._is_connected = True
11
+ return func(self, *args, **kwargs)
12
+ except requests.exceptions.ConnectionError:
13
+ self._is_connected = False
14
+ return []
15
+ return wrapper
app/rag/v1/router.py ADDED
@@ -0,0 +1,7 @@
1
+ from fastapi import APIRouter
2
+
3
+ router = APIRouter()
4
+
5
+ @router.get("/")
6
+ async def root():
7
+ return {"message": "RAG API is running"}
@@ -0,0 +1,81 @@
1
+ import requests
2
+ from pymilvus import MilvusClient
3
+ from pymilvus.exceptions import MilvusException
4
+
5
+ import app.rag.config as cfn
6
+ from app.rag.utils import connection_check
7
+
8
+
9
+ class VectorStoreClient(MilvusClient):
10
+ def __init__(self, base_url: str, user: str, password: str, database: str, collection: str, metric_type: str):
11
+ self.base_url = base_url
12
+ self.user = user
13
+ self.password = password
14
+ self.database = database
15
+ self.collection = collection
16
+ self.metric_type = metric_type
17
+ self._is_connected = True
18
+ self._is_database_exists = True
19
+
20
+ if self.check_connection():
21
+ try:
22
+ super().__init__(
23
+ uri = base_url,
24
+ db_name = database,
25
+ collection = collection,
26
+ user = user,
27
+ password = password,
28
+ metric_type = metric_type,
29
+ )
30
+ except MilvusException as e:
31
+ if e.code == 800: # database not found
32
+ self._is_database_exists = False
33
+ else:
34
+ raise
35
+
36
+ def check_connection(self) -> bool:
37
+ """Check if the Milvus server is accessible"""
38
+ try:
39
+ requests.head(url=self.base_url, timeout=5)
40
+ except requests.exceptions.ConnectionError:
41
+ self._is_connected = False
42
+ return False
43
+ self._is_connected = True
44
+ return True
45
+
46
+ @connection_check
47
+ def get_databases(self) -> list:
48
+ """Get list of available databases"""
49
+ if not self._is_database_exists:
50
+ return []
51
+ return self.list_databases()
52
+
53
+ @connection_check
54
+ def get_collections(self) -> list:
55
+ """Get list of available collections"""
56
+ if not self._is_database_exists:
57
+ return []
58
+ return self.list_collections()
59
+
60
+ @connection_check
61
+ def has_database(self, database: str) -> bool:
62
+ """Check if database exists"""
63
+ if not self._is_database_exists:
64
+ return False
65
+ return database in self.get_databases()
66
+
67
+ @connection_check
68
+ def has_collection(self, collection: str) -> bool:
69
+ """Check if collection exists"""
70
+ if not self._is_database_exists:
71
+ return False
72
+ return collection in self.get_collections()
73
+
74
+ client = VectorStoreClient(
75
+ base_url = cfn.MILVUS_BASE_URL,
76
+ user = cfn.MILVUS_USER,
77
+ password = cfn.MILVUS_PASSWORD,
78
+ database = cfn.MILVUS_DATABASE,
79
+ collection = cfn.MILVUS_COLLECTION,
80
+ metric_type = cfn.MILVUS_METRIC_TYPE,
81
+ )
@@ -0,0 +1,43 @@
1
+ from loguru import logger
2
+
3
+ import app.rag.config as cfn
4
+ from .client import client
5
+
6
+ def doctor():
7
+ # Check connection
8
+ is_connected = client.check_connection()
9
+ if not is_connected:
10
+ logger.error(f"- ❌ FAILED: Vector store connection ({cfn.MILVUS_BASE_URL})")
11
+ else:
12
+ logger.info(f"- ✅ PASSED: Vector store connection ({cfn.MILVUS_BASE_URL})")
13
+
14
+ # Check databases
15
+ databases = None
16
+ try:
17
+ databases = client.get_databases()
18
+ if not is_connected:
19
+ logger.warning("- ⏭️ SKIPPED: Vector store databases (Server is not accessible)")
20
+ else:
21
+ if not client.has_database(cfn.MILVUS_DATABASE):
22
+ logger.error(f"- ❌ FAILED: Vector store databases (Database '{cfn.MILVUS_DATABASE}' not found)")
23
+ else:
24
+ logger.info(f"- ✅ PASSED: Vector store databases (Database '{cfn.MILVUS_DATABASE}' exists)")
25
+ except Exception as e:
26
+ logger.error(f"- ❌ FAILED: Database check ({str(e)})")
27
+
28
+ # Check collections
29
+ try:
30
+ collections = client.get_collections()
31
+ if not is_connected:
32
+ logger.warning("- ⏭️ SKIPPED: Vector store collections (Server is not accessible)")
33
+ elif len(databases) == 0:
34
+ logger.warning("- ⏭️ SKIPPED: Vector store collections (No database available)")
35
+ elif len(collections) == 0:
36
+ logger.error("- ❌ FAILED: Vector store collections (No collections available)")
37
+ else:
38
+ if not client.has_collection(cfn.MILVUS_COLLECTION):
39
+ logger.error(f"- ❌ FAILED: Vector store collections (Collection '{cfn.MILVUS_COLLECTION}' not found)")
40
+ else:
41
+ logger.info(f"- ✅ PASSED: Vector store collections (Collection '{cfn.MILVUS_COLLECTION}' exists)")
42
+ except Exception as e:
43
+ logger.error(f"- ❌ FAILED: Collection check ({str(e)})")
app/requirements.txt CHANGED
@@ -1,10 +1,9 @@
1
- # Core dependencies
2
- python-dotenv < 1.2
1
+ # App
2
+ dynaconf < 3.3
3
3
  loguru < 0.8
4
-
5
- # Development
6
4
  pytest < 8.4
7
- black < 25.2
5
+ fastapi < 0.116
6
+ uvicorn < 0.35
8
7
 
9
8
  # RAG
10
9
  ragas < 0.3
app/setup.py CHANGED
@@ -1,6 +1,5 @@
1
+ import os, tomli
1
2
  from setuptools import setup, find_packages
2
- import os, sys
3
- import tomli
4
3
 
5
4
  # Load requirements
6
5
  with open(os.path.join(os.path.dirname(__file__), 'requirements.txt'), 'r') as f:
@@ -21,7 +20,7 @@ setup(
21
20
  install_requires = requirements,
22
21
  entry_points = {
23
22
  "console_scripts": [
24
- f"{APP_NAME}=main:main",
23
+ f"{APP_NAME}=app.cli:main",
25
24
  ],
26
25
  },
27
26
  )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pirag
3
- Version: 0.1.7
3
+ Version: 0.2.0
4
4
  Summary: CLI Projects of On-Premise RAG. You can use your own LLM and vector DB. Or just add remote LLM servers and vector DB.
5
5
  Author-email: semir4in <semir4in@gmail.com>, jyje <jyjeon@outlook.com>
6
6
  Project-URL: Homepage, https://github.com/jyje/pilot-onpremise-rag
@@ -9,10 +9,11 @@ Project-URL: Issue, https://github.com/jyje/pilot-onpremise-rag/issues
9
9
  Requires-Python: >=3.9
10
10
  Description-Content-Type: text/markdown
11
11
  License-File: LICENSE
12
- Requires-Dist: python-dotenv<1.2
12
+ Requires-Dist: dynaconf<3.3
13
13
  Requires-Dist: loguru<0.8
14
14
  Requires-Dist: pytest<8.4
15
- Requires-Dist: black<25.2
15
+ Requires-Dist: fastapi<0.116
16
+ Requires-Dist: uvicorn<0.35
16
17
  Requires-Dist: ragas<0.3
17
18
  Requires-Dist: pymilvus<2.6
18
19
  Dynamic: license-file
@@ -0,0 +1,24 @@
1
+ app/main.py,sha256=4yUp70bM0mn1AwL00-ttPO-wUn8nUgehRstFGQIytSc,1573
2
+ app/requirements.txt,sha256=SVy0-4AaepRwHLga-DaO_HiegjR1IUORWbGHMu3GanE,112
3
+ app/setup.py,sha256=nwUhqn3SLcm6L-WN0saHFisF-K9BpMR3qktszjBJcA4,736
4
+ app/rag/api.py,sha256=68HuxoYMkViFnr4iZKFLt6Y8fCkntrUXcnL3weSWEfw,1101
5
+ app/rag/cli.py,sha256=J4IBNOwi1B_qws3HP5OAHV82X1b56zPJcmR1w-Lkem8,822
6
+ app/rag/config.py,sha256=kHgsy4z8HGXq-zlwTYcgjcvxkly79mb8rTw6DY0zTB8,4369
7
+ app/rag/utils.py,sha256=svN1mEWf9mGrJ67PTwn8KNP2b6OuE08qkdslK4T11bM,510
8
+ app/rag/agent/client.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
+ app/rag/embedding/client.py,sha256=tu5LgZx1QuadYtViUQkLdFJAC99S2iB01jY0wOO5PCc,2159
10
+ app/rag/embedding/service.py,sha256=XC5SYIbwp8eTFu3zY5OYKlpELGpeRp-G2OPYmJWsko4,994
11
+ app/rag/llm/client.py,sha256=pFUsTgsBkPS0m5P0Btv9pgHHR9Os6NxEzM43GQSXwiA,2092
12
+ app/rag/llm/service.py,sha256=HBEnVyek-dz8pwv8wTHXG8d7oBT1uScMcr7omYVoSv0,928
13
+ app/rag/test/client.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
+ app/rag/train/client.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
+ app/rag/v1/router.py,sha256=mE-c-c_vNVN_V7M0-Z8OSGlzWTOa419sfxSV35Qs7DU,133
16
+ app/rag/v1/service.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
+ app/rag/vector_store/client.py,sha256=nFDS3ARTZhojXcFoo6AvCpK_qeTaEYt8WihllPD6ZSM,2613
18
+ app/rag/vector_store/service.py,sha256=xuamcVIHDgPC8XsbsPTRaE3L7hfLTmM1eS9ZOXLdw9c,1919
19
+ pirag-0.2.0.dist-info/licenses/LICENSE,sha256=gBUmwRyDQYI4Q4Ju5S88urvB-B-nqsXN45n8oQ3DZ3Y,1079
20
+ pirag-0.2.0.dist-info/METADATA,sha256=5rnLQ9cQMjNfeDwgxq-WDFoCpnfEiV_hJfYZwmcTBDQ,5563
21
+ pirag-0.2.0.dist-info/WHEEL,sha256=0CuiUZ_p9E4cD6NyLD6UG80LBXYyiSYZOKDm5lp32xk,91
22
+ pirag-0.2.0.dist-info/entry_points.txt,sha256=1tHs5rP66AVq5SMEWRRIWRf_XqJo2Gb1TJl9-Kw_MSo,40
23
+ pirag-0.2.0.dist-info/top_level.txt,sha256=io9g7LCbfmTG1SFKgEOGXmCFB9uMP2H5lerm0HiHWQE,4
24
+ pirag-0.2.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (79.0.0)
2
+ Generator: setuptools (80.3.1)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
app/rag/agent.py DELETED
@@ -1,64 +0,0 @@
1
- from langchain_openai.llms import OpenAI
2
- from langchain_openai.embeddings import OpenAIEmbeddings
3
- from pymilvus import MilvusClient
4
-
5
- class Agent():
6
- def __init__(
7
- self,
8
- llm_base_url: str,
9
- llm_api_key: str,
10
- llm_model: str,
11
- embedding_base_url: str,
12
- embedding_api_key: str,
13
- embedding_model: str,
14
- milvus_host: str,
15
- milvus_database: str,
16
- milvus_collection: str,
17
- ):
18
- self.llm_base_url = llm_base_url
19
- self.llm_api_key = llm_api_key
20
- self.llm_model = llm_model
21
- self.embedding_base_url = embedding_base_url
22
- self.embedding_api_key = embedding_api_key
23
- self.embedding_model = embedding_model
24
- self.milvus_host = milvus_host
25
- self.milvus_database = milvus_database
26
- self.milvus_collection = milvus_collection
27
-
28
- self._llm_client: OpenAI = None
29
- self._embedding_client: OpenAIEmbeddings = None
30
- self._milvus_client: MilvusClient = None
31
-
32
- def retrieve_knowledge_base(self, query: str) -> str:
33
- llm_client = self._get_llm_client()
34
- embedding_client = self._get_embedding_client()
35
- milvus_client = self._get_milvus_client()
36
-
37
-
38
- def _get_llm_client(self) -> OpenAI:
39
- if self._llm_client is None:
40
- self._llm_client = OpenAI(
41
- base_url=self.llm_base_url,
42
- api_key=self.llm_api_key,
43
- model=self.llm_model,
44
- )
45
- return self._llm_client
46
-
47
-
48
- def _get_embedding_client(self) -> OpenAIEmbeddings:
49
- if self._embedding_client is None:
50
- self._embedding_client = OpenAIEmbeddings(
51
- base_url=self.embedding_base_url,
52
- api_key=self.embedding_api_key,
53
- model=self.embedding_model,
54
- )
55
- return self._embedding_client
56
-
57
- def _get_milvus_client(self) -> MilvusClient:
58
- if self._milvus_client is None:
59
- self._milvus_client = MilvusClient(
60
- host=self.milvus_host,
61
- database=self.milvus_database,
62
- collection=self.milvus_collection,
63
- )
64
- return self._milvus_client
app/rag/ask/__init__.py DELETED
@@ -1,2 +0,0 @@
1
- from .config import parser, help
2
- from .router import route
app/rag/ask/config.py DELETED
@@ -1,9 +0,0 @@
1
- import argparse
2
-
3
- help = "Ask a question to the RAG system."
4
-
5
- parser = argparse.ArgumentParser(
6
- description = f"{help} Queries the knowledge base with natural language questions, retrieves relevant context, and generates accurate, contextually-aware responses based on the retrieved information",
7
- add_help = False,
8
- formatter_class = argparse.ArgumentDefaultsHelpFormatter,
9
- )
app/rag/ask/router.py DELETED
@@ -1,4 +0,0 @@
1
- from argparse import Namespace
2
-
3
- def route(args: Namespace):
4
- print(args)
@@ -1,2 +0,0 @@
1
- from .config import parser, help
2
- from .router import route
app/rag/doctor/config.py DELETED
@@ -1,24 +0,0 @@
1
- import argparse
2
-
3
- from app.rag.config import top_parser, common_parser
4
-
5
- help = "Diagnose the RAG system."
6
-
7
- parser = argparse.ArgumentParser(
8
- description = f"{help} Performs comprehensive health checks on all components, validates configurations, and reports issues to ensure optimal system operation",
9
- add_help = False,
10
- formatter_class = argparse.ArgumentDefaultsHelpFormatter,
11
- )
12
-
13
- subparsers = parser.add_subparsers(
14
- title = "subcommands",
15
- dest = "subcommand",
16
- )
17
-
18
- subparsers.add_parser(
19
- "check",
20
- help = "Check RAG System. Scan all components of the system and diagnose status",
21
- description = "Check RAG System. Scan all components of the system and diagnose status",
22
- parents = [top_parser, common_parser],
23
- add_help = False,
24
- )
app/rag/doctor/router.py DELETED
@@ -1,4 +0,0 @@
1
- from argparse import Namespace
2
-
3
- def route(args: Namespace):
4
- print(args)
app/rag/test/__init__.py DELETED
@@ -1,2 +0,0 @@
1
- from .config import parser, help
2
- from .router import route
app/rag/test/config.py DELETED
@@ -1,9 +0,0 @@
1
- import argparse
2
-
3
- help = "Test the RAG system."
4
-
5
- parser = argparse.ArgumentParser(
6
- description = f"{help} Evaluates system performance by running predefined test cases, measuring accuracy, relevance, and latency metrics to validate retrieval and generation capabilities",
7
- add_help = False,
8
- formatter_class = argparse.ArgumentDefaultsHelpFormatter,
9
- )
app/rag/test/router.py DELETED
@@ -1,4 +0,0 @@
1
- from argparse import Namespace
2
-
3
- def route(args: Namespace):
4
- print(args)
app/rag/train/__init__.py DELETED
@@ -1,2 +0,0 @@
1
- from .config import parser, help
2
- from .router import route
app/rag/train/config.py DELETED
@@ -1,20 +0,0 @@
1
- import argparse
2
-
3
- help = "Train the RAG system."
4
-
5
- parser = argparse.ArgumentParser(
6
- description = f"{help} Processes documents, extracts knowledge, generates embeddings, and builds a searchable vector database for efficient semantic retrieval and contextual responses",
7
- add_help = False,
8
- formatter_class = argparse.ArgumentDefaultsHelpFormatter,
9
- )
10
-
11
- subparsers = parser.add_subparsers(
12
- title = "subcommands",
13
- dest = "subcommand",
14
- )
15
-
16
- subparsers.add_parser(
17
- "check",
18
- help = "Check RAG System. Scan all components of the system and diagnose status",
19
- description = "Check RAG System. Scan all components of the system and diagnose status",
20
- )
app/rag/train/router.py DELETED
@@ -1,4 +0,0 @@
1
- from argparse import Namespace
2
-
3
- def route(args: Namespace):
4
- print(args)
@@ -1,27 +0,0 @@
1
- app/main.py,sha256=yPDjSCvnbrJBCHKSKmf6-2DhjLOhDd7WalfuMvfuC_s,2235
2
- app/requirements.txt,sha256=JFEWoQHN7hwDg0bDrWlN0vxZVR3oe8dYgv7mkHrQg1g,128
3
- app/setup.py,sha256=j9qcfapv_jOW3jORYGfHzKDAcxNRItHcHA34_mMDGj4,744
4
- app/rag/agent.py,sha256=u1ovyqALsmOMHDCW-hdlkMU72XdlTMiFJ-WRbLHHrYQ,2190
5
- app/rag/config.py,sha256=Gm4fAHZrlTiRmzRjyzx73TL0J3lxjhjlsFyuGG-8f0o,4914
6
- app/rag/ask/__init__.py,sha256=41lK4nmvWT2ZZ3G_FMtOiG-0JFJdDVT4zoxkoes8TaE,59
7
- app/rag/ask/config.py,sha256=tRHOBrEAmTMBaBmYPKPsRJmSaVyKT8-TBSDJQXEndxc,386
8
- app/rag/ask/router.py,sha256=5nuaaHzWVr2ltYqOuQ30ujmZ-GFyx-22OLVODILd5iE,76
9
- app/rag/ask/service.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
- app/rag/doctor/__init__.py,sha256=41lK4nmvWT2ZZ3G_FMtOiG-0JFJdDVT4zoxkoes8TaE,59
11
- app/rag/doctor/config.py,sha256=CW0scwLkRykKLp15szu6Q9i3sG7n70ZJ0eYOtkjuS9Q,765
12
- app/rag/doctor/router.py,sha256=5nuaaHzWVr2ltYqOuQ30ujmZ-GFyx-22OLVODILd5iE,76
13
- app/rag/doctor/service.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
- app/rag/test/__init__.py,sha256=41lK4nmvWT2ZZ3G_FMtOiG-0JFJdDVT4zoxkoes8TaE,59
15
- app/rag/test/config.py,sha256=tk-IM1VspaUPgwWKSwYWagkdYXfOjIAjU3-c0rxMBoc,361
16
- app/rag/test/router.py,sha256=5nuaaHzWVr2ltYqOuQ30ujmZ-GFyx-22OLVODILd5iE,76
17
- app/rag/test/service.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
18
- app/rag/train/__init__.py,sha256=41lK4nmvWT2ZZ3G_FMtOiG-0JFJdDVT4zoxkoes8TaE,59
19
- app/rag/train/config.py,sha256=PO4iRMmZXViFHOMBbOOhnFHqBu8hRfbI61zGCuksKhI,668
20
- app/rag/train/router.py,sha256=5nuaaHzWVr2ltYqOuQ30ujmZ-GFyx-22OLVODILd5iE,76
21
- app/rag/train/service.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
22
- pirag-0.1.7.dist-info/licenses/LICENSE,sha256=gBUmwRyDQYI4Q4Ju5S88urvB-B-nqsXN45n8oQ3DZ3Y,1079
23
- pirag-0.1.7.dist-info/METADATA,sha256=bR5iK9Ontup0kC3ilEntHswPjCRHHYzYRChATc-Cy38,5537
24
- pirag-0.1.7.dist-info/WHEEL,sha256=pxyMxgL8-pra_rKaQ4drOZAegBVuX-G_4nRHjjgWbmo,91
25
- pirag-0.1.7.dist-info/entry_points.txt,sha256=1tHs5rP66AVq5SMEWRRIWRf_XqJo2Gb1TJl9-Kw_MSo,40
26
- pirag-0.1.7.dist-info/top_level.txt,sha256=io9g7LCbfmTG1SFKgEOGXmCFB9uMP2H5lerm0HiHWQE,4
27
- pirag-0.1.7.dist-info/RECORD,,
File without changes
File without changes
File without changes
File without changes