rag-python 0.3.0__py3-none-any.whl → 0.3.1__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.
rag_python/__init__.py CHANGED
@@ -2,18 +2,29 @@
2
2
 
3
3
  Quick start::
4
4
 
5
- from rag_python import RAG
5
+ pip install rag-python
6
+ export OPENAI_API_KEY=sk-...
7
+
8
+ # CLI
9
+ rag-python ingest ./docs --reindex
10
+ rag-python query "What is our leave policy?"
11
+ rag-python docs quickstart
6
12
 
7
- rag = RAG(llm_model="gpt-4o-mini")
13
+ # Python
14
+ from rag_python import RAG
15
+ rag = RAG()
8
16
  rag.ingest(["./docs"], reindex=True)
9
17
  print(rag.query("What is our leave policy?").text)
18
+
19
+ Documentation: https://github.com/RaghavOG/rag-python/tree/main/docs
10
20
  """
11
21
 
12
- __version__ = "0.3.0"
22
+ __version__ = "0.3.1"
13
23
 
14
24
  from .client import RAG, RAGAnswer
15
- from .rag_pipeline import ingest, query, RAGResponse
25
+ from .rag_pipeline import ingest, query, query_stream, RAGResponse, RAGStream
16
26
  from .providers import make_llm_provider, make_embedding_provider
27
+ from .log import configure_logging, get_logger
17
28
  from .options import (
18
29
  ChunkingConfig,
19
30
  DocumentConfig,
@@ -33,7 +44,11 @@ __all__ = [
33
44
  "QueryConfig",
34
45
  "ingest",
35
46
  "query",
47
+ "query_stream",
48
+ "RAGStream",
36
49
  "RAGResponse",
50
+ "configure_logging",
51
+ "get_logger",
37
52
  "make_llm_provider",
38
53
  "make_embedding_provider",
39
54
  ]
rag_python/cli.py CHANGED
@@ -1,10 +1,14 @@
1
1
  """rag-python command-line interface."""
2
+ from __future__ import annotations
3
+
2
4
  import argparse
3
5
  import json
6
+ import sys
4
7
  from dataclasses import replace
5
8
 
6
9
  from . import __version__
7
10
  from .client import RAG
11
+ from .help_text import CLI_EPILOG, list_topics, print_topic, print_topic_list
8
12
 
9
13
 
10
14
  def _build_rag(args: argparse.Namespace) -> RAG:
@@ -42,21 +46,44 @@ def _add_provider_args(parser: argparse.ArgumentParser) -> None:
42
46
  "--llm-provider",
43
47
  default="openai",
44
48
  choices=["openai", "azure_openai", "anthropic", "gemini", "ollama"],
49
+ metavar="PROVIDER",
50
+ help="LLM backend (default: openai). See: rag-python docs providers",
51
+ )
52
+ parser.add_argument(
53
+ "--llm-model",
54
+ default=None,
55
+ metavar="MODEL",
56
+ help="LLM model or Azure deployment name (default: from env LLM_MODEL)",
45
57
  )
46
- parser.add_argument("--llm-model", default=None)
47
58
  parser.add_argument(
48
59
  "--embedding-provider",
49
60
  default="openai",
50
61
  choices=["openai", "azure_openai", "ollama", "local"],
62
+ metavar="PROVIDER",
63
+ help="Embedding backend (default: openai). Use local for offline embeddings",
51
64
  )
52
- parser.add_argument("--embedding-model", default=None)
53
- parser.add_argument("--ollama-base-url", default=None)
54
- parser.add_argument("--azure-endpoint", default=None)
55
- parser.add_argument("--azure-api-key", default=None)
56
- parser.add_argument("--azure-api-version", default=None)
57
- parser.add_argument("--openai-api-key", default=None)
58
- parser.add_argument("--anthropic-api-key", default=None)
59
- parser.add_argument("--gemini-api-key", default=None)
65
+ parser.add_argument(
66
+ "--embedding-model",
67
+ default=None,
68
+ metavar="MODEL",
69
+ help="Embedding model name (default: from env EMBEDDING_MODEL)",
70
+ )
71
+ parser.add_argument(
72
+ "--ollama-base-url",
73
+ default=None,
74
+ metavar="URL",
75
+ help="Ollama server URL (default: http://localhost:11434 or OLLAMA_BASE_URL)",
76
+ )
77
+ parser.add_argument("--azure-endpoint", default=None, help="Azure OpenAI endpoint URL")
78
+ parser.add_argument("--azure-api-key", default=None, help="Azure OpenAI API key")
79
+ parser.add_argument(
80
+ "--azure-api-version",
81
+ default=None,
82
+ help="Azure API version (default: 2023-09-01-preview)",
83
+ )
84
+ parser.add_argument("--openai-api-key", default=None, help="OpenAI API key (overrides env)")
85
+ parser.add_argument("--anthropic-api-key", default=None, help="Anthropic API key")
86
+ parser.add_argument("--gemini-api-key", default=None, help="Gemini API key")
60
87
 
61
88
 
62
89
  def _add_search_args(parser: argparse.ArgumentParser) -> None:
@@ -64,37 +91,135 @@ def _add_search_args(parser: argparse.ArgumentParser) -> None:
64
91
  "--retriever",
65
92
  choices=["vector", "multi_query", "hybrid"],
66
93
  default=None,
67
- help="Retrieval strategy (default: multi_query; hybrid needs pip install rag-python[hybrid])",
94
+ metavar="MODE",
95
+ help=(
96
+ "Retrieval mode: vector (single query), multi_query (default, with rewriting), "
97
+ "or hybrid (BM25+vector; requires pip install rag-python[hybrid])"
98
+ ),
68
99
  )
69
100
  parser.add_argument(
70
101
  "--metadata-filter",
71
102
  type=_parse_metadata_filter,
72
103
  default=None,
73
- help='Chroma metadata filter as JSON, e.g. \'{"filename": "policy.pdf"}\'',
104
+ metavar="JSON",
105
+ help='Filter chunks by metadata, e.g. \'{"filename": "policy.pdf"}\'',
74
106
  )
75
107
 
76
108
 
77
- def main() -> None:
109
+ def _make_parser() -> argparse.ArgumentParser:
78
110
  parser = argparse.ArgumentParser(
79
111
  prog="rag-python",
80
- description="rag-python — modular RAG with query rewriting, reranking, guardrails, and multi-LLM support.",
112
+ description=(
113
+ "Production-grade RAG for Python — ingest documents, ask questions, "
114
+ "get grounded answers with multi-LLM support."
115
+ ),
116
+ epilog=CLI_EPILOG,
117
+ formatter_class=argparse.RawDescriptionHelpFormatter,
81
118
  )
82
- parser.add_argument("--version", action="version", version=f"rag-python {__version__}")
83
- sub = parser.add_subparsers(dest="command", required=True)
119
+ parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
120
+ sub = parser.add_subparsers(dest="command", required=True, metavar="COMMAND")
84
121
 
85
- ing = sub.add_parser("ingest", help="Ingest files/folders into the vector store")
86
- ing.add_argument("paths", nargs="+", help="Files or folders to ingest")
87
- ing.add_argument("--reindex", action="store_true", help="Clear vector store and re-ingest")
122
+ ing = sub.add_parser(
123
+ "ingest",
124
+ help="Load files into the vector store (chunk + embed)",
125
+ description=(
126
+ "Ingest one or more files or directories into the ChromaDB vector store.\n"
127
+ "Supported formats: .txt .md .pdf .docx .csv .json .html"
128
+ ),
129
+ formatter_class=argparse.RawDescriptionHelpFormatter,
130
+ epilog=(
131
+ "examples:\n"
132
+ " rag-python ingest ./data --reindex\n"
133
+ " rag-python ingest policy.pdf handbook/ --embedding-provider local"
134
+ ),
135
+ )
136
+ ing.add_argument(
137
+ "paths",
138
+ nargs="+",
139
+ metavar="PATH",
140
+ help="File or directory paths to ingest",
141
+ )
142
+ ing.add_argument(
143
+ "--reindex",
144
+ action="store_true",
145
+ help="Delete existing vectors before ingesting (fresh index)",
146
+ )
88
147
  _add_provider_args(ing)
89
148
 
90
- q = sub.add_parser("query", help="Ask a question against ingested documents")
91
- q.add_argument("question", nargs="+", help="Question text")
92
- q.add_argument("--no-multi-query", action="store_true", help="Use vector retriever only")
93
- q.add_argument("-v", "--verbose", action="store_true")
149
+ q = sub.add_parser(
150
+ "query",
151
+ help="Ask a question against ingested documents",
152
+ description=(
153
+ "Run the full RAG pipeline: retrieve relevant chunks, generate an answer, "
154
+ "optionally stream tokens and show sources."
155
+ ),
156
+ formatter_class=argparse.RawDescriptionHelpFormatter,
157
+ epilog=(
158
+ "examples:\n"
159
+ ' rag-python query "How many days of annual leave?"\n'
160
+ " rag-python query \"PTO policy\" --stream -v\n"
161
+ ' rag-python query "benefits" --retriever hybrid --metadata-filter \'{"filename": "hr.pdf"}\''
162
+ ),
163
+ )
164
+ q.add_argument(
165
+ "question",
166
+ nargs="+",
167
+ metavar="QUESTION",
168
+ help="Question text (multiple words are joined)",
169
+ )
170
+ q.add_argument(
171
+ "--no-multi-query",
172
+ action="store_true",
173
+ help="Use single-query vector retrieval (same as --retriever vector)",
174
+ )
175
+ q.add_argument(
176
+ "--stream",
177
+ action="store_true",
178
+ help="Stream answer tokens to stdout as they are generated",
179
+ )
180
+ q.add_argument(
181
+ "-v",
182
+ "--verbose",
183
+ action="store_true",
184
+ help="After the answer, print evaluation scores and top source paths",
185
+ )
94
186
  _add_provider_args(q)
95
187
  _add_search_args(q)
96
188
 
97
- args = parser.parse_args()
189
+ docs = sub.add_parser(
190
+ "docs",
191
+ help="Show user documentation in the terminal",
192
+ description="Print built-in help topics. Full docs: https://github.com/RaghavOG/rag-python/tree/main/docs",
193
+ formatter_class=argparse.RawDescriptionHelpFormatter,
194
+ epilog="topics: " + ", ".join(list_topics()),
195
+ )
196
+ docs.add_argument(
197
+ "topic",
198
+ nargs="?",
199
+ default="quickstart",
200
+ choices=list_topics(),
201
+ metavar="TOPIC",
202
+ help="Documentation topic (default: quickstart)",
203
+ )
204
+ docs.add_argument(
205
+ "--list",
206
+ action="store_true",
207
+ help="List all available documentation topics",
208
+ )
209
+
210
+ return parser
211
+
212
+
213
+ def main(argv: list[str] | None = None) -> None:
214
+ parser = _make_parser()
215
+ args = parser.parse_args(argv)
216
+
217
+ if args.command == "docs":
218
+ if args.list:
219
+ print_topic_list()
220
+ else:
221
+ print_topic(args.topic)
222
+ return
98
223
 
99
224
  if args.command == "ingest":
100
225
  rag = _build_rag(args)
@@ -113,6 +238,20 @@ def main() -> None:
113
238
  retriever=retriever or rag.config.search.retriever,
114
239
  metadata_filter=args.metadata_filter or rag.config.search.metadata_filter,
115
240
  )
241
+ if args.stream:
242
+ stream = rag.query_stream(question, search=search)
243
+ for token in stream:
244
+ print(token, end="", flush=True)
245
+ print()
246
+ result = stream.result
247
+ if args.verbose:
248
+ print("\n--- evaluation ---")
249
+ print(result.evaluation)
250
+ print("\n--- sources ---")
251
+ for s in result.sources[:5]:
252
+ print(s.get("metadata", {}).get("source", ""), "score:", s.get("score"))
253
+ return
254
+
116
255
  ans = rag.query(question, search=search)
117
256
  print(ans.text)
118
257
  if args.verbose:
@@ -124,4 +263,4 @@ def main() -> None:
124
263
 
125
264
 
126
265
  if __name__ == "__main__":
127
- main()
266
+ main(sys.argv[1:])
rag_python/client.py CHANGED
@@ -30,7 +30,7 @@ from .options import (
30
30
  SearchConfig,
31
31
  )
32
32
  from .providers import make_llm_provider, make_embedding_provider
33
- from .rag_pipeline import ingest as _ingest, query as _query, RAGResponse
33
+ from .rag_pipeline import ingest as _ingest, query as _query, query_stream as _query_stream, RAGResponse, RAGStream
34
34
  from .vector_store import set_persist_dir
35
35
 
36
36
 
@@ -191,3 +191,21 @@ class RAG:
191
191
  evaluation=resp.evaluation,
192
192
  retried=resp.retried,
193
193
  )
194
+
195
+ def query_stream(
196
+ self,
197
+ question: str,
198
+ *,
199
+ search: SearchConfig | None = None,
200
+ query_config: QueryConfig | None = None,
201
+ ) -> RAGStream:
202
+ """Stream answer tokens; call ``stream.result`` after iterating."""
203
+ return _query_stream(
204
+ question,
205
+ search=search or self.config.search,
206
+ query_config=query_config or self.config.query,
207
+ llm_model=self.llm_model,
208
+ embedding_model=self.embedding_model,
209
+ llm=self.llm,
210
+ embedder=self.embedder,
211
+ )
rag_python/generation.py CHANGED
@@ -1,6 +1,9 @@
1
1
  """LLM generation with context (RAG)."""
2
+ from collections.abc import Iterator
3
+
2
4
  from .config import LLM_MODEL
3
5
  from .providers import LLMProvider, make_llm_provider
6
+ from .providers.streaming import stream_generate
4
7
 
5
8
 
6
9
  RAG_SYSTEM = (
@@ -10,6 +13,11 @@ RAG_SYSTEM = (
10
13
  )
11
14
 
12
15
 
16
+ def _build_user_prompt(query: str, context_chunks: list[str]) -> str:
17
+ context = "\n\n---\n\n".join(context_chunks)
18
+ return f"Context:\n{context}\n\nQuestion: {query}\n\nAnswer:"
19
+
20
+
13
21
  def generate(
14
22
  query: str,
15
23
  context_chunks: list[str],
@@ -20,12 +28,11 @@ def generate(
20
28
  ) -> str:
21
29
  """Generate answer from query and retrieved context."""
22
30
  llm = llm or make_llm_provider("openai")
23
- context = "\n\n---\n\n".join(context_chunks)
24
31
  sys = system_prompt or RAG_SYSTEM
25
32
  try:
26
33
  return llm.generate(
27
34
  system=sys,
28
- user=f"Context:\n{context}\n\nQuestion: {query}\n\nAnswer:",
35
+ user=_build_user_prompt(query, context_chunks),
29
36
  model=model or LLM_MODEL,
30
37
  temperature=0.2,
31
38
  max_tokens=1024,
@@ -33,3 +40,26 @@ def generate(
33
40
  except Exception as e:
34
41
  return f"[Generation error: {e}]"
35
42
 
43
+
44
+ def generate_stream(
45
+ query: str,
46
+ context_chunks: list[str],
47
+ *,
48
+ model: str | None = None,
49
+ system_prompt: str | None = None,
50
+ llm: LLMProvider | None = None,
51
+ ) -> Iterator[str]:
52
+ """Stream answer tokens from query and retrieved context."""
53
+ llm = llm or make_llm_provider("openai")
54
+ sys = system_prompt or RAG_SYSTEM
55
+ try:
56
+ yield from stream_generate(
57
+ llm,
58
+ system=sys,
59
+ user=_build_user_prompt(query, context_chunks),
60
+ model=model or LLM_MODEL,
61
+ temperature=0.2,
62
+ max_tokens=1024,
63
+ )
64
+ except Exception as e:
65
+ yield f"[Generation error: {e}]"
@@ -0,0 +1,229 @@
1
+ """User-facing documentation printed by ``rag-python docs``."""
2
+ from __future__ import annotations
3
+
4
+ DOCS_BASE_URL = "https://github.com/RaghavOG/rag-python/tree/main/docs"
5
+
6
+ CLI_EPILOG = """
7
+ examples:
8
+ rag-python ingest ./data --reindex
9
+ rag-python query "How many days of annual leave?"
10
+ rag-python query "leave policy" --stream -v
11
+ rag-python query "benefits" --retriever hybrid
12
+ rag-python docs quickstart
13
+ rag-python docs --list
14
+
15
+ online docs:
16
+ {base_url}
17
+ """.format(base_url=DOCS_BASE_URL).strip()
18
+
19
+ TOPICS: dict[str, str] = {
20
+ "quickstart": """
21
+ rag-python — Quick start
22
+ ========================
23
+
24
+ 1. Install
25
+ pip install rag-python
26
+
27
+ 2. Set your API key (OpenAI default)
28
+ export OPENAI_API_KEY=sk-...
29
+
30
+ 3. Ingest documents (TXT, MD, PDF, DOCX, CSV, JSON, HTML)
31
+ rag-python ingest ./my-docs --reindex
32
+
33
+ 4. Ask a question
34
+ rag-python query "What is our leave policy?"
35
+ rag-python query "annual leave" --stream -v
36
+
37
+ Python API
38
+ ----------
39
+ from rag_python import RAG
40
+
41
+ rag = RAG(llm_model="gpt-4o-mini")
42
+ rag.ingest(["./my-docs"], reindex=True)
43
+ print(rag.query("What is our leave policy?").text)
44
+
45
+ More: rag-python docs install | cli | config | providers | features
46
+ Online: {base_url}
47
+ """.format(base_url=DOCS_BASE_URL).strip(),
48
+ "install": """
49
+ rag-python — Install & optional extras
50
+ ======================================
51
+
52
+ Base install (OpenAI + Chroma + document loaders):
53
+ pip install rag-python
54
+
55
+ Optional extras:
56
+ pip install rag-python[local] Offline embeddings (sentence-transformers)
57
+ pip install rag-python[hybrid] BM25 + vector hybrid search
58
+ pip install rag-python[rerank] Cross-encoder reranking
59
+ pip install rag-python[anthropic] Claude LLM
60
+ pip install rag-python[gemini] Gemini LLM
61
+ pip install rag-python[all] All optional features
62
+
63
+ From source:
64
+ git clone https://github.com/RaghavOG/rag-python.git
65
+ cd rag-python
66
+ pip install -e ".[dev,all]"
67
+
68
+ Verify:
69
+ rag-python --version
70
+ pytest
71
+ """.strip(),
72
+ "cli": """
73
+ rag-python — CLI reference
74
+ ==========================
75
+
76
+ USAGE
77
+ rag-python ingest PATH [PATH ...] [options]
78
+ rag-python query QUESTION [options]
79
+ rag-python docs [TOPIC]
80
+ rag-python --version
81
+
82
+ INGEST
83
+ paths Files or directories to ingest
84
+ --reindex Clear vector store before ingesting
85
+ --llm-provider openai | azure_openai | anthropic | gemini | ollama
86
+ --embedding-provider openai | azure_openai | ollama | local
87
+ --llm-model Model or deployment name
88
+ --embedding-model Embedding model name
89
+ --openai-api-key Override OPENAI_API_KEY
90
+ --ollama-base-url Ollama URL (default http://localhost:11434)
91
+
92
+ QUERY
93
+ question Natural-language question (words joined if multiple)
94
+ --retriever vector | multi_query | hybrid (default: multi_query)
95
+ --no-multi-query Shortcut for --retriever vector
96
+ --metadata-filter JSON Chroma filter, e.g. '{"filename": "policy.pdf"}'
97
+ --stream Stream answer tokens to stdout
98
+ -v, --verbose Show evaluation scores and source paths
99
+
100
+ ENVIRONMENT
101
+ OPENAI_API_KEY Required for default OpenAI provider
102
+ RAG_PYTHON_DATA_DIR Document storage (default ./data)
103
+ RAG_PYTHON_CHROMA_DIR Vector DB path (default ./chroma_db)
104
+
105
+ Examples:
106
+ rag-python ingest ./policies ./handbook.pdf --reindex
107
+ rag-python query "How many vacation days?" -v
108
+ rag-python query "PTO policy" --retriever hybrid --stream
109
+ rag-python query "benefits" --metadata-filter '{"filename": "hr.pdf"}'
110
+ """.strip(),
111
+ "config": """
112
+ rag-python — Configuration
113
+ ==========================
114
+
115
+ Environment variables (.env supported via python-dotenv):
116
+
117
+ OPENAI_API_KEY OpenAI LLM + embeddings
118
+ ANTHROPIC_API_KEY Claude (LLM only)
119
+ GEMINI_API_KEY Gemini (LLM only)
120
+ AZURE_OPENAI_ENDPOINT Azure OpenAI
121
+ AZURE_OPENAI_API_KEY Azure OpenAI
122
+ OPENAI_API_VERSION Azure API version
123
+ OLLAMA_BASE_URL Local Ollama server
124
+ LOCAL_EMBEDDING_MODEL Model for embedding_provider=local
125
+ RAG_PYTHON_DATA_DIR Default ./data
126
+ RAG_PYTHON_CHROMA_DIR Default ./chroma_db
127
+
128
+ CHUNK_SIZE, CHUNK_OVERLAP, CHUNK_STRATEGY
129
+ TOP_K_RETRIEVE, TOP_K_RERANK, MULTI_QUERY_N
130
+ GUARDRAILS_ENABLED, MAX_RETRIES, RERANKER_MODEL
131
+
132
+ Python RAGConfig:
133
+ from rag_python import RAG, RAGConfig, ChunkingConfig, SearchConfig
134
+
135
+ rag = RAG(
136
+ config=RAGConfig(
137
+ chunking=ChunkingConfig(strategy="recursive", chunk_size=512),
138
+ search=SearchConfig(
139
+ retriever="hybrid",
140
+ top_k_retrieve=20,
141
+ metadata_filter={"filename": "policy.pdf"},
142
+ ),
143
+ ),
144
+ )
145
+
146
+ Shorthand on RAG():
147
+ chunk_strategy, chunk_size, retriever, metadata_filter,
148
+ top_k_retrieve, document_extensions, ...
149
+
150
+ Logging:
151
+ import rag_python
152
+ rag_python.configure_logging()
153
+ """.strip(),
154
+ "providers": """
155
+ rag-python — Providers
156
+ ======================
157
+
158
+ LLM (generation, rewriting, guardrails):
159
+ openai OPENAI_API_KEY (default)
160
+ azure_openai AZURE_OPENAI_ENDPOINT + AZURE_OPENAI_API_KEY
161
+ anthropic ANTHROPIC_API_KEY + pip install rag-python[anthropic]
162
+ gemini GEMINI_API_KEY + pip install rag-python[gemini]
163
+ ollama Local Ollama — set --llm-model to your model name
164
+
165
+ Embeddings (retrieval):
166
+ openai OPENAI_API_KEY (default)
167
+ azure_openai Azure deployment for embeddings
168
+ ollama Local embedding model via Ollama
169
+ local Offline sentence-transformers + pip install rag-python[local]
170
+
171
+ Common combos:
172
+ OpenAI end-to-end:
173
+ RAG(llm_provider="openai", embedding_provider="openai")
174
+
175
+ Claude + OpenAI embeddings:
176
+ RAG(llm_provider="anthropic", llm_model="claude-opus-4-6",
177
+ embedding_provider="openai")
178
+
179
+ Fully local embeddings:
180
+ RAG(llm_provider="ollama", llm_model="llama3.1",
181
+ embedding_provider="local", embedding_model="all-MiniLM-L6-v2")
182
+ """.strip(),
183
+ "features": """
184
+ rag-python — Features
185
+ =====================
186
+
187
+ Ingest pipeline
188
+ Loaders: .txt .md .pdf .docx .csv .json .html
189
+ Cleaning, chunking (recursive | structure_aware | semantic)
190
+ Embeddings → ChromaDB vector store
191
+
192
+ Query pipeline
193
+ Query rewriting + multi-query retrieval
194
+ Hybrid search: BM25 + vector (pip install rag-python[hybrid])
195
+ Cross-encoder reranking (pip install rag-python[rerank])
196
+ Metadata filters on retrieval (source, filename, ...)
197
+ Streaming answers: rag.query_stream()
198
+ Guardrails: prompt injection + hallucination checks
199
+ Evaluation + self-correction retry loop
200
+
201
+ CLI
202
+ rag-python ingest | query | docs
203
+ rag-python --version
204
+
205
+ Docs
206
+ rag-python docs [topic]
207
+ https://github.com/RaghavOG/rag-python/tree/main/docs
208
+ """.strip(),
209
+ }
210
+
211
+
212
+ def list_topics() -> list[str]:
213
+ return sorted(TOPICS.keys())
214
+
215
+
216
+ def print_topic(topic: str) -> None:
217
+ key = topic.lower().strip()
218
+ if key not in TOPICS:
219
+ available = ", ".join(list_topics())
220
+ raise SystemExit(f"Unknown topic '{topic}'. Available: {available}\nUse: rag-python docs --list")
221
+ print(TOPICS[key])
222
+
223
+
224
+ def print_topic_list() -> None:
225
+ print("rag-python documentation topics:\n")
226
+ for name in list_topics():
227
+ print(f" {name}")
228
+ print("\nUsage: rag-python docs <topic>")
229
+ print(f"Online: {DOCS_BASE_URL}")
rag_python/log.py ADDED
@@ -0,0 +1,21 @@
1
+ """Structured logging helpers for rag-python."""
2
+ from __future__ import annotations
3
+
4
+ import logging
5
+
6
+ PACKAGE_LOGGER = "rag_python"
7
+
8
+
9
+ def get_logger(name: str | None = None) -> logging.Logger:
10
+ """Return a namespaced logger (default: ``rag_python``)."""
11
+ if name:
12
+ return logging.getLogger(f"{PACKAGE_LOGGER}.{name}")
13
+ return logging.getLogger(PACKAGE_LOGGER)
14
+
15
+
16
+ def configure_logging(level: int = logging.INFO) -> None:
17
+ """Enable default console logging for the package (optional convenience)."""
18
+ logging.basicConfig(
19
+ level=level,
20
+ format="%(asctime)s %(levelname)s [%(name)s] %(message)s",
21
+ )
@@ -1,5 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from collections.abc import Iterator
4
+
3
5
 
4
6
  class AnthropicProvider:
5
7
  def __init__(self, *, api_key: str | None = None) -> None:
@@ -36,6 +38,27 @@ class AnthropicProvider:
36
38
  parts.append(text)
37
39
  return ("\n".join(parts)).strip()
38
40
 
41
+ def generate_stream(
42
+ self,
43
+ *,
44
+ user: str,
45
+ system: str | None = None,
46
+ model: str | None = None,
47
+ temperature: float = 0.2,
48
+ max_tokens: int = 1024,
49
+ ) -> Iterator[str]:
50
+ if not model:
51
+ raise RuntimeError("AnthropicProvider requires `model=...` (e.g. claude-...)")
52
+ with self._client.messages.stream(
53
+ model=model,
54
+ max_tokens=max_tokens,
55
+ temperature=temperature,
56
+ system=system or "",
57
+ messages=[{"role": "user", "content": user}],
58
+ ) as stream:
59
+ for text in stream.text_stream:
60
+ yield text
61
+
39
62
  def embed(self, texts: list[str], *, model: str | None = None) -> list[list[float]]:
40
63
  raise RuntimeError("Anthropic does not provide embeddings in this package. Use OpenAI/Ollama or local embeddings.")
41
64