@@ -101,8 +101,10 @@ fn parse_format(format: &str) -> PyResult<DocumentFormat> {
101101///
102102/// Args:
103103/// mode: Indexing mode - "default", "force", or "incremental".
104- /// summaries: Whether to generate summaries. Default: False.
105- /// description: Whether to generate document description. Default: False.
104+ /// generate_summaries: Whether to generate summaries. Default: True.
105+ /// generate_description: Whether to generate document description. Default: False.
106+ /// include_text: Whether to include node text in the tree. Default: True.
107+ /// generate_ids: Whether to generate node IDs. Default: True.
106108#[ pyclass( name = "IndexOptions" , skip_from_py_object) ]
107109#[ derive( Clone ) ]
108110pub struct PyIndexOptions {
@@ -112,8 +114,14 @@ pub struct PyIndexOptions {
112114#[ pymethods]
113115impl PyIndexOptions {
114116 #[ new]
115- #[ pyo3( signature = ( mode="default" , summaries=false , description=false ) ) ]
116- fn new ( mode : & str , summaries : bool , description : bool ) -> PyResult < Self > {
117+ #[ pyo3( signature = ( mode="default" , generate_summaries=true , generate_description=false , include_text=true , generate_ids=true ) ) ]
118+ fn new (
119+ mode : & str ,
120+ generate_summaries : bool ,
121+ generate_description : bool ,
122+ include_text : bool ,
123+ generate_ids : bool ,
124+ ) -> PyResult < Self > {
117125 let mut opts = IndexOptions :: new ( ) ;
118126 match mode {
119127 "default" => { }
@@ -126,17 +134,26 @@ impl PyIndexOptions {
126134 ) ) )
127135 }
128136 }
129- if summaries {
130- opts = opts. with_summaries ( ) ;
131- }
132- if description {
133- opts = opts. with_description ( ) ;
134- }
137+ opts. generate_summaries = generate_summaries;
138+ opts. generate_description = generate_description;
139+ opts. include_text = include_text;
140+ opts. generate_ids = generate_ids;
135141 Ok ( Self { inner : opts } )
136142 }
137143
138144 fn __repr__ ( & self ) -> String {
139- "IndexOptions(...)" . to_string ( )
145+ format ! (
146+ "IndexOptions(mode='{}', generate_summaries={}, generate_description={}, include_text={}, generate_ids={})" ,
147+ match self . inner. mode {
148+ IndexMode :: Default => "default" ,
149+ IndexMode :: Force => "force" ,
150+ IndexMode :: Incremental => "incremental" ,
151+ } ,
152+ self . inner. generate_summaries,
153+ self . inner. generate_description,
154+ self . inner. include_text,
155+ self . inner. generate_ids,
156+ )
140157 }
141158}
142159
@@ -152,19 +169,19 @@ impl PyIndexOptions {
152169/// from vectorless import IndexContext
153170///
154171/// # Single file
155- /// ctx = IndexContext.from_file ("./document.pdf")
172+ /// ctx = IndexContext.from_path ("./document.pdf")
156173///
157174/// # Multiple files
158- /// ctx = IndexContext.from_files (["./a.pdf", "./b.md"])
175+ /// ctx = IndexContext.from_paths (["./a.pdf", "./b.md"])
159176///
160177/// # Directory
161178/// ctx = IndexContext.from_dir("./docs/")
162179///
163180/// # From text
164- /// ctx = IndexContext.from_text ("# Title\\nContent...", name= "doc")
181+ /// ctx = IndexContext.from_content ("# Title\\nContent...", "markdown").with_name( "doc")
165182///
166183/// # From bytes
167- /// ctx = IndexContext.from_bytes(data, name="doc", format="pdf ")
184+ /// ctx = IndexContext.from_bytes(data, "pdf").with_name("doc ")
168185/// ```
169186#[ pyclass( name = "IndexContext" ) ]
170187pub struct PyIndexContext {
@@ -175,18 +192,15 @@ pub struct PyIndexContext {
175192impl PyIndexContext {
176193 /// Create an IndexContext from a single file path.
177194 #[ staticmethod]
178- #[ pyo3( signature = ( path, name=None ) ) ]
179- fn from_file ( path : String , name : Option < String > ) -> Self {
180- let mut ctx = IndexContext :: from_path ( & path) ;
181- if let Some ( n) = name {
182- ctx = ctx. with_name ( & n) ;
195+ fn from_path ( path : String ) -> Self {
196+ Self {
197+ inner : IndexContext :: from_path ( & path) ,
183198 }
184- Self { inner : ctx }
185199 }
186200
187201 /// Create an IndexContext from multiple file paths.
188202 #[ staticmethod]
189- fn from_files ( paths : Vec < String > ) -> Self {
203+ fn from_paths ( paths : Vec < String > ) -> Self {
190204 Self {
191205 inner : IndexContext :: from_paths ( & paths) ,
192206 }
@@ -202,29 +216,30 @@ impl PyIndexContext {
202216
203217 /// Create an IndexContext from text content.
204218 #[ staticmethod]
205- #[ pyo3( signature = ( content, name= None , format="markdown" ) ) ]
206- fn from_content ( content : String , name : Option < String > , format : & str ) -> PyResult < Self > {
219+ #[ pyo3( signature = ( content, format="markdown" ) ) ]
220+ fn from_content ( content : String , format : & str ) -> PyResult < Self > {
207221 let doc_format = parse_format ( format) ?;
208- let mut ctx = IndexContext :: from_content ( & content, doc_format) ;
209- if let Some ( n) = name {
210- ctx = ctx. with_name ( & n) ;
211- }
222+ let ctx = IndexContext :: from_content ( & content, doc_format) ;
212223 Ok ( Self { inner : ctx } )
213224 }
214225
215226 /// Create an IndexContext from binary data.
216227 #[ staticmethod]
217- #[ pyo3( signature = ( data, name, format) ) ]
218- fn from_bytes ( data : Vec < u8 > , name : String , format : & str ) -> PyResult < Self > {
228+ fn from_bytes ( data : Vec < u8 > , format : & str ) -> PyResult < Self > {
219229 let doc_format = parse_format ( format) ?;
220- let ctx = IndexContext :: from_bytes ( data, doc_format) . with_name ( & name ) ;
230+ let ctx = IndexContext :: from_bytes ( data, doc_format) ;
221231 Ok ( Self { inner : ctx } )
222232 }
223233
234+ /// Set the document name (single-source only).
235+ fn with_name ( & self , name : String ) -> Self {
236+ let ctx = self . inner . clone ( ) . with_name ( & name) ;
237+ Self { inner : ctx }
238+ }
239+
224240 /// Apply indexing options.
225241 fn with_options ( & self , options : & PyIndexOptions ) -> Self {
226- let mut ctx = self . inner . clone ( ) ;
227- ctx = ctx. with_options ( options. inner . clone ( ) ) ;
242+ let ctx = self . inner . clone ( ) . with_options ( options. inner . clone ( ) ) ;
228243 Self { inner : ctx }
229244 }
230245
@@ -250,6 +265,80 @@ impl PyIndexContext {
250265 }
251266}
252267
268+ // ============================================================
269+ // QueryContext
270+ // ============================================================
271+
272+ /// Context for a query operation.
273+ ///
274+ /// ```python
275+ /// from vectorless import QueryContext
276+ ///
277+ /// # Query a single document
278+ /// ctx = QueryContext("What is the total revenue?").with_doc_id(doc_id)
279+ ///
280+ /// # Query multiple documents
281+ /// ctx = QueryContext("What is the architecture?").with_doc_ids(["doc-1", "doc-2"])
282+ ///
283+ /// # Query entire workspace
284+ /// ctx = QueryContext("Explain the algorithm")
285+ /// ```
286+ #[ pyclass( name = "QueryContext" ) ]
287+ pub struct PyQueryContext {
288+ inner : QueryContext ,
289+ }
290+
291+ #[ pymethods]
292+ impl PyQueryContext {
293+ /// Create a new query context (defaults to workspace scope).
294+ #[ new]
295+ fn new ( query : String ) -> Self {
296+ Self {
297+ inner : QueryContext :: new ( & query) ,
298+ }
299+ }
300+
301+ /// Set scope to a single document.
302+ fn with_doc_id ( & self , doc_id : String ) -> Self {
303+ let ctx = self . inner . clone ( ) . with_doc_id ( & doc_id) ;
304+ Self { inner : ctx }
305+ }
306+
307+ /// Set scope to multiple documents.
308+ fn with_doc_ids ( & self , doc_ids : Vec < String > ) -> Self {
309+ let ctx = self . inner . clone ( ) . with_doc_ids ( doc_ids) ;
310+ Self { inner : ctx }
311+ }
312+
313+ /// Set scope to entire workspace.
314+ fn with_workspace ( & self ) -> Self {
315+ let ctx = self . inner . clone ( ) . with_workspace ( ) ;
316+ Self { inner : ctx }
317+ }
318+
319+ /// Set the maximum tokens for the result content.
320+ fn with_max_tokens ( & self , tokens : usize ) -> Self {
321+ let ctx = self . inner . clone ( ) . with_max_tokens ( tokens) ;
322+ Self { inner : ctx }
323+ }
324+
325+ /// Set whether to include the reasoning chain.
326+ fn with_include_reasoning ( & self , include : bool ) -> Self {
327+ let ctx = self . inner . clone ( ) . with_include_reasoning ( include) ;
328+ Self { inner : ctx }
329+ }
330+
331+ /// Set the maximum tree traversal depth.
332+ fn with_depth_limit ( & self , depth : usize ) -> Self {
333+ let ctx = self . inner . clone ( ) . with_depth_limit ( depth) ;
334+ Self { inner : ctx }
335+ }
336+
337+ fn __repr__ ( & self ) -> String {
338+ "QueryContext(...)" . to_string ( )
339+ }
340+ }
341+
253342// ============================================================
254343// QueryResultItem
255344// ============================================================
@@ -798,7 +887,7 @@ async fn run_get_graph(engine: Arc<Engine>) -> PyResult<Option<PyDocumentGraph>>
798887/// `api_key` and `model` are **required**.
799888///
800889/// ```python
801- /// from vectorless import Engine, IndexContext
890+ /// from vectorless import Engine, IndexContext, QueryContext
802891///
803892/// engine = Engine(
804893/// workspace="./data",
@@ -807,11 +896,11 @@ async fn run_get_graph(engine: Arc<Engine>) -> PyResult<Option<PyDocumentGraph>>
807896/// )
808897///
809898/// # Index
810- /// result = await engine.index(IndexContext.from_file ("./report.pdf"))
899+ /// result = await engine.index(IndexContext.from_path ("./report.pdf"))
811900/// doc_id = result.doc_id
812901///
813902/// # Query
814- /// answer = await engine.query(doc_id, "What is the revenue?")
903+ /// answer = await engine.query(QueryContext( "What is the revenue?").with_doc_id(doc_id) )
815904/// print(answer.single().content)
816905/// ```
817906#[ pyclass( name = "Engine" ) ]
@@ -885,7 +974,7 @@ impl PyEngine {
885974 /// Index a document.
886975 ///
887976 /// Args:
888- /// ctx: IndexContext created from from_file, from_files , from_dir, etc.
977+ /// ctx: IndexContext created from from_path, from_paths , from_dir, etc.
889978 ///
890979 /// Returns:
891980 /// IndexResult with doc_id and items.
@@ -901,35 +990,21 @@ impl PyEngine {
901990 /// Query indexed documents.
902991 ///
903992 /// Args:
904- /// doc_id: Document ID (or list of IDs) returned from index().
905- /// question: The question to ask.
993+ /// ctx: QueryContext with query text and scope.
906994 ///
907995 /// Returns:
908996 /// QueryResult with answer and score.
909997 ///
910998 /// Raises:
911999 /// VectorlessError: If query fails.
912- #[ pyo3( signature = ( doc_id, question) ) ]
9131000 fn query < ' py > (
9141001 & self ,
9151002 py : Python < ' py > ,
916- doc_id : & Bound < ' _ , PyAny > ,
917- question : String ,
1003+ ctx : & PyQueryContext ,
9181004 ) -> PyResult < Bound < ' py , PyAny > > {
9191005 let engine = Arc :: clone ( & self . inner ) ;
920-
921- let ctx = if let Ok ( single) = doc_id. extract :: < String > ( ) {
922- QueryContext :: new ( & question) . with_doc_id ( & single)
923- } else if let Ok ( multi) = doc_id. extract :: < Vec < String > > ( ) {
924- QueryContext :: new ( & question) . with_doc_ids ( multi)
925- } else {
926- return Err ( PyErr :: from ( VectorlessError :: new (
927- "doc_id must be a string or list of strings" . to_string ( ) ,
928- "config" ,
929- ) ) ) ;
930- } ;
931-
932- future_into_py ( py, run_query ( engine, ctx) )
1006+ let query_ctx = ctx. inner . clone ( ) ;
1007+ future_into_py ( py, run_query ( engine, query_ctx) )
9331008 }
9341009
9351010 /// List all indexed documents.
@@ -986,18 +1061,19 @@ impl PyEngine {
9861061/// Vectorless - Reasoning-native document intelligence engine.
9871062///
9881063/// ```python
989- /// from vectorless import Engine, IndexContext
1064+ /// from vectorless import Engine, IndexContext, QueryContext
9901065///
9911066/// engine = Engine(workspace="./data", api_key="sk-...", model="gpt-4o")
992- /// result = await engine.index(IndexContext.from_file ("./report.pdf"))
993- /// answer = await engine.query(result.doc_id, "What is the revenue?")
1067+ /// result = await engine.index(IndexContext.from_path ("./report.pdf"))
1068+ /// answer = await engine.query(QueryContext( "What is the revenue?").with_doc_id(result.doc_id) )
9941069/// print(answer.single().content)
9951070/// ```
9961071#[ pymodule]
9971072fn vectorless ( m : & Bound < ' _ , PyModule > ) -> PyResult < ( ) > {
9981073 m. add_class :: < VectorlessError > ( ) ?;
9991074 m. add_class :: < PyIndexOptions > ( ) ?;
10001075 m. add_class :: < PyIndexContext > ( ) ?;
1076+ m. add_class :: < PyQueryContext > ( ) ?;
10011077 m. add_class :: < PyIndexResult > ( ) ?;
10021078 m. add_class :: < PyIndexItem > ( ) ?;
10031079 m. add_class :: < PyQueryResult > ( ) ?;
0 commit comments