Skip to content

Commit 511c368

Browse files
authored
Merge pull request #55 from vectorlessflow/dev
Dev
2 parents 0727287 + eab706d commit 511c368

4 files changed

Lines changed: 176 additions & 73 deletions

File tree

examples/indexing/main.py

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
import asyncio
1010
import os
11-
from vectorless import Engine, IndexContext, IndexOptions
11+
from vectorless import Engine, IndexContext, IndexOptions, QueryContext
1212

1313
# os is used only for removing the sample file
1414

@@ -45,8 +45,8 @@ async def main():
4545
- **LLM Navigation**: Queries are resolved by traversing the tree.
4646
- **No Vectors**: No embeddings, no similarity search, no vector DB.
4747
""",
48-
name="architecture",
49-
)
48+
"markdown",
49+
).with_name("architecture")
5050
)
5151
doc_id = result.doc_id
5252
print(f" Indexed: {doc_id}")
@@ -74,7 +74,7 @@ async def main():
7474
Projected Q1 revenue is $13.5M based on current pipeline.
7575
""")
7676

77-
result = await engine.index(IndexContext.from_file(sample_path))
77+
result = await engine.index(IndexContext.from_path(sample_path))
7878
file_doc_id = result.doc_id
7979
print(f" Indexed: {file_doc_id}\n")
8080
os.remove(sample_path)
@@ -84,14 +84,18 @@ async def main():
8484
result = await engine.index(
8585
IndexContext.from_content(
8686
"# API Reference\n\n## GET /users\n\nList all users.\n\n## POST /users\n\nCreate a user.",
87-
name="api_ref",
88-
).with_options(IndexOptions(summaries=True, description=True)),
87+
"markdown",
88+
)
89+
.with_name("api_ref")
90+
.with_options(IndexOptions(generate_summaries=True, generate_description=True)),
8991
)
9092
print(f" Indexed: {result.doc_id}\n")
9193

9294
# --- 5. Query ---
9395
print("--- Query ---")
94-
answer = await engine.query(file_doc_id, "What was the total revenue?")
96+
answer = await engine.query(
97+
QueryContext("What was the total revenue?").with_doc_id(file_doc_id)
98+
)
9599
item = answer.single()
96100
if item:
97101
print(f" Score: {item.score:.2f}")

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "maturin"
44

55
[project]
66
name = "vectorless"
7-
version = "0.1.1"
7+
version = "0.1.2"
88
description = "Hierarchical document intelligence without vectors"
99
readme = "README.md"
1010
requires-python = ">=3.9"

python/src/lib.rs

Lines changed: 134 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -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)]
108110
pub struct PyIndexOptions {
@@ -112,8 +114,14 @@ pub struct PyIndexOptions {
112114
#[pymethods]
113115
impl 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")]
170187
pub struct PyIndexContext {
@@ -175,18 +192,15 @@ pub struct PyIndexContext {
175192
impl 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]
9971072
fn 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

Comments
 (0)