Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
223 changes: 223 additions & 0 deletions llm-predict-error-investigation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
# Investigation: `'LLM' object has no attribute 'predict'`

## Error Context

```
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Missing language specifiers on fenced code blocks (MD040).

The traceback block on Line 5 and the Python block on Line 52 both lack a language identifier, triggering markdownlint MD040 warnings.

📝 Proposed fix
-```
+```text
 ERROR: unstract.prompt_service.core.retrievers.keyword_table:
-```
+```python
 BaseLLM -> LLM (has predict()) -> CustomLLM/FunctionCallingLLM -> Provider

Also applies to: 52-52

🧰 Tools
🪛 markdownlint-cli2 (0.21.0)

[warning] 5-5: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@llm-predict-error-investigation.md` at line 5, Add explicit language
specifiers to the two fenced code blocks to satisfy MD040: change the traceback
block that starts with "ERROR:
unstract.prompt_service.core.retrievers.keyword_table:" to use ```text and
change the block containing "BaseLLM -> LLM (has predict()) ->
CustomLLM/FunctionCallingLLM -> Provider" to use ```python so markdownlint
recognizes their languages.

ERROR: unstract.prompt_service.core.retrievers.keyword_table:
'LLM' object has no attribute 'predict'

Traceback:
File "prompt_service/core/retrievers/keyword_table.py", line 48, in retrieve
keyword_index = KeywordTableIndex(...)
File "llama_index/core/indices/keyword_table/base.py", line 92, in __init__
super().__init__(...)
File "llama_index/core/indices/base.py", line 79, in __init__
index_struct = self.build_index_from_nodes(...)
File "llama_index/core/indices/keyword_table/base.py", line 182, in _build_index_from_nodes
response = self._llm.predict(...)
```

## Root Cause

**Class type mismatch** between Unstract's SDK1 `LLM` and llama-index's `LLM`.

The prompt-service retrievers pass `unstract.sdk1.llm.LLM` (a plain Python class wrapping LiteLLM) to llama-index components that expect `llama_index.core.llms.llm.LLM` (a Pydantic model with `predict()`, `chat()`, `complete()`, etc.).

This is **NOT** a llama-index bug, **NOT** provider-specific, and **NOT** a version issue.

## How We Got Here: SDK Migration

### Old SDK (`unstract-sdk`) - Worked Correctly

```python
# unstract-sdk/src/unstract/sdk/llm.py
from llama_index.core.llms import LLM as LlamaIndexLLM

class LLM:
def __init__(self, ...):
self._llm_instance: LlamaIndexLLM = None # Real llama-index LLM
self._initialise()

def _initialise(self):
self._llm_instance = self._get_llm(self._adapter_instance_id)

def _get_llm(self, adapter_instance_id) -> LlamaIndexLLM:
# Returns actual llama-index LLM from adapters via get_llm_instance()
llm_instance = self._llm_adapter_class.get_llm_instance()
return llm_instance
```

The old SDK obtained a real `llama_index.core.llms.LLM` instance from adapters. This instance had `predict()`, `chat()`, `complete()` through llama-index's class hierarchy:

```
BaseLLM -> LLM (has predict()) -> CustomLLM/FunctionCallingLLM -> Provider
```

### New SDK1 (`unstract-sdk1`) - Breaks llama-index Integration

```python
# unstract/sdk1/src/unstract/sdk1/llm.py
import litellm

class LLM: # Plain Python class, NOT llama-index's LLM
def complete(self, prompt, **kwargs):
response = litellm.completion(messages=messages, **completion_kwargs)
return {"response": response_object, ...}

def stream_complete(self, prompt, **kwargs):
for chunk in litellm.completion(messages=messages, stream=True, ...):
yield stream_response

async def acomplete(self, prompt, **kwargs):
response = await litellm.acompletion(messages=messages, ...)
return {"response": response_object}
```

SDK1's `LLM` calls `litellm.completion()` directly. It has **no inheritance** from any llama-index class, and therefore **no** `predict()`, `chat()`, or other llama-index LLM methods.

## Where It Breaks

### keyword_table.py (Confirmed)

```python
# prompt-service/src/unstract/prompt_service/core/retrievers/keyword_table.py:48-52
keyword_index = KeywordTableIndex(
nodes=[node.node for node in all_nodes],
show_progress=True,
llm=self.llm, # unstract.sdk1.llm.LLM, NOT llama_index.core.llms.llm.LLM
)
```

llama-index's `KeywordTableIndex` stores this as `self._llm` and then calls:

```python
# llama_index/core/indices/keyword_table/base.py:239
response = self._llm.predict(self.keyword_extract_template, text=text)
```

Since `unstract.sdk1.llm.LLM` has no `predict()` method -> **AttributeError**.

### The Two LLM Classes Side-by-Side

| Aspect | `llama_index.core.llms.llm.LLM` | `unstract.sdk1.llm.LLM` |
|--------|----------------------------------|--------------------------|
| Base class | `BaseLLM` (Pydantic model) | Plain Python `object` |
| `predict()` | Yes (line 589 of llm.py) | **No** |
| `chat()` | Yes (abstract, implemented by providers) | **No** |
| `complete()` | Yes (abstract, implemented by providers) | Yes, but different signature (returns dict, not `CompletionResponse`) |
| `stream_complete()` | Yes | Yes, but different signature |
| Used by | llama-index components internally | Unstract prompt-service for direct LLM calls |

## All Affected Retrievers

The base retriever stores the SDK1 LLM:

```python
# prompt-service/src/unstract/prompt_service/core/retrievers/base_retriever.py
from unstract.sdk1.llm import LLM

class BaseRetriever:
def __init__(self, ..., llm: LLM | None = None):
self.llm = llm if llm else None
```

All retrievers that pass `self.llm` to llama-index components are affected:

| Retriever | Passes SDK1 LLM to | Status |
|-----------|-------------------|--------|
| `KeywordTableRetriever` | `KeywordTableIndex(llm=self.llm)` | **Confirmed broken** (`'LLM' object has no attribute 'predict'`) |
| `SubquestionRetriever` | `as_query_engine(llm=self.llm)`, `SubQuestionQueryEngine.from_defaults(llm=self.llm)` | **Broken** (unexpected error, needs further investigation) |
| `FusionRetriever` | `QueryFusionRetriever(llm=self.llm)` | **Broken** (unexpected error, needs further investigation) |
| `RouterRetriever` | `LLMSingleSelector.from_defaults(llm=self.llm)`, `RouterQueryEngine.from_defaults(llm=self.llm)`, `as_query_engine(llm=self.llm)` | **Broken** (unexpected error, needs further investigation) |
| `SimpleRetriever` | Does NOT pass LLM to llama-index | **Works fine** |
| `AutomergingRetriever` | - | **Works fine** |
| `RecursiveRetrieval` | - | **Works fine** |

## Online Research: llama-index GitHub Issues

The `predict` method has existed on `llama_index.core.llms.llm.LLM` since v0.10.x. We are on v0.13.2. Related GitHub issues confirm the pattern:

| Issue | Error | Root Cause |
|-------|-------|-----------|
| [#7093](https://github.com/run-llama/llama_index/issues/7093) | `HuggingFaceLLM has no attribute predict` | Wrong parameter name (`llm_predictor=` instead of `llm=`) |
| [#13958](https://github.com/run-llama/llama_index/issues/13958) | `AzureOpenAIMultiModal has no attribute predict` | MultiModal classes don't inherit from `LLM` |
| [#8906](https://github.com/run-llama/llama_index/issues/8906) | `AttributeError in LiteLLM` | Version compatibility issue between litellm and llama-index |

All confirm: the error occurs when a non-`llama_index.core.llms.llm.LLM` object is passed where llama-index expects one.

## Proposed Fix Options

### Option 1: Bridge Adapter (Recommended)

Create a class extending `llama_index.core.llms.CustomLLM` that delegates to SDK1's `LLM.complete()` internally. This makes it compatible with all llama-index components.

```python
from llama_index.core.llms import CustomLLM, CompletionResponse, LLMMetadata

class SDK1LLMBridge(CustomLLM):
"""Bridge between unstract.sdk1.llm.LLM and llama-index's LLM interface."""

def __init__(self, sdk1_llm):
super().__init__()
self._sdk1_llm = sdk1_llm

@property
def metadata(self) -> LLMMetadata:
return LLMMetadata(model_name=self._sdk1_llm.get_model_name())

def complete(self, prompt, **kwargs) -> CompletionResponse:
result = self._sdk1_llm.complete(prompt, **kwargs)
return CompletionResponse(text=result["response"].text)

def stream_complete(self, prompt, **kwargs):
for chunk in self._sdk1_llm.stream_complete(prompt, **kwargs):
yield CompletionResponse(text=chunk.text, delta=chunk.delta)
```

Then in retrievers, wrap before passing to llama-index:

```python
from .llm_bridge import SDK1LLMBridge

llama_llm = SDK1LLMBridge(self.llm)
keyword_index = KeywordTableIndex(nodes=..., llm=llama_llm)
```
Comment on lines +150 to +184
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Option 1 code example does not match the actual implementation.

The example shows a single-layer bridge extending llama_index.core.llms.CustomLLM and names it SDK1LLMBridge. The actual implementation is a two-layer approach:

  • LLMCompat in unstract/sdk1/src/unstract/sdk1/llm.py — emulates llama-index types without importing llama-index.
  • RetrieverLLM in prompt-service/.../core/retrievers/retriever_llm.py — inherits from llama_index.core.llms.llm.LLM (not CustomLLM) and delegates to LLMCompat.

If this document is retained, the code example should be updated to reflect the actual class hierarchy and naming so it doesn't mislead future readers.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@llm-predict-error-investigation.md` around lines 150 - 184, Update the
document's Option 1 example to match the actual two-layer implementation: show
LLMCompat (from unstract/sdk1/src/unstract/sdk1/llm.py) that emulates
llama-index types and RetrieverLLM (from
prompt-service/.../core/retrievers/retriever_llm.py) which inherits from
llama_index.core.llms.llm.LLM and delegates to LLMCompat; replace the
single-class SDK1LLMBridge/CustomLLM example with a short description and class
names matching LLMCompat -> RetrieverLLM delegation and note that RetrieverLLM
inherits from llama_index.core.llms.llm.LLM (not CustomLLM).


### Option 2: Use `llama-index-llms-litellm`

Since SDK1 already has the LiteLLM model name and credentials, construct the official llama-index `LiteLLM` instance from those params:

```python
from llama_index.llms.litellm import LiteLLM

llama_llm = LiteLLM(model=self.llm.get_model_name(), **self.llm.kwargs)
keyword_index = KeywordTableIndex(nodes=..., llm=llama_llm)
```

Requires adding `llama-index-llms-litellm` as a dependency.

### Option 3: Quick Fix for KeywordTable Only

Use `SimpleKeywordTableIndex` instead of `KeywordTableIndex`. The "simple" variant uses regex-based keyword extraction and does NOT call `predict()`.

```python
from llama_index.core.indices.keyword_table import SimpleKeywordTableIndex

keyword_index = SimpleKeywordTableIndex(
nodes=[node.node for node in all_nodes],
show_progress=True,
# No llm parameter needed
)
```

**Limitation**: This only fixes `KeywordTableRetriever`. The other broken retrievers still need a solution.

## Recommendation

**Option 1 (Bridge Adapter)** is recommended because:
- Fixes all affected retrievers with a single adapter class
- No new dependencies needed
- Keeps SDK1's LiteLLM-based architecture intact
- Can be applied at the `BaseRetriever` level so all retrievers benefit

The bridge could be created once in the base retriever or in a shared utility, so each retriever doesn't need to worry about the conversion.
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from unstract.prompt_service.core.retrievers.retriever_llm import RetrieverLLM
from unstract.prompt_service.utils.llm_helper import get_llama_index_llm
from unstract.sdk1.llm import LLM
from unstract.sdk1.vector_db import VectorDB

Expand All @@ -24,6 +26,20 @@ def __init__(
self.doc_id = doc_id
self.top_k = top_k
self.llm = llm if llm else None
self._llama_index_llm: RetrieverLLM | None = None

@property
def llama_index_llm(self) -> RetrieverLLM | None:
"""Return a llama-index compatible LLM, lazily created from SDK1 LLM.

Llama-index components (KeywordTableIndex, SubQuestionQueryEngine,
etc.) expect an instance of ``llama_index.core.llms.llm.LLM``.
SDK1's ``LLM`` wraps litellm directly and is *not* compatible.
This property bridges the gap via ``RetrieverLLM``.
"""
if self._llama_index_llm is None and self.llm is not None:
self._llama_index_llm = get_llama_index_llm(self.llm)
return self._llama_index_llm

@staticmethod
def retrieve() -> set[str]:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ def retrieve(self) -> set[str]:
mode="simple", # Use simple fusion mode (reciprocal rank fusion)
use_async=False,
verbose=True,
llm=self.llm, # LLM generates query variations
llm=self.llama_index_llm,
)

# Retrieve nodes using fusion technique
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ def retrieve(self) -> set[str]:
keyword_index = KeywordTableIndex(
nodes=[node.node for node in all_nodes],
show_progress=True,
llm=self.llm, # Use the provided LLM instead of defaulting to OpenAI
llm=self.llama_index_llm,
)

# Create retriever from keyword index
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
from collections.abc import Sequence
from typing import Any

from llama_index.core.base.llms.types import (
ChatMessage,
ChatResponse,
ChatResponseAsyncGen,
ChatResponseGen,
CompletionResponse,
CompletionResponseAsyncGen,
CompletionResponseGen,
LLMMetadata,
MessageRole,
)
from llama_index.core.llms.llm import LLM as LlamaIndexBaseLLM # noqa: N811

from unstract.sdk1.llm import LLM, LLMCompat


class RetrieverLLM(LlamaIndexBaseLLM):
"""Bridges SDK1's LLMCompat with llama-index's LLM for retriever use.

Llama-index's ``resolve_llm()`` asserts ``isinstance(llm, LLM)``
where ``LLM`` is ``llama_index.core.llms.llm.LLM``. Since SDK1's
``LLMCompat`` is a plain class without llama-index inheritance,
it fails this check.

``RetrieverLLM`` inherits from llama-index's ``LLM`` base class
(passing the isinstance check) and delegates all LLM calls to an
internal ``LLMCompat`` instance.
"""

def __init__(self, llm: LLM, **kwargs: Any) -> None: # noqa: ANN401
"""Initialize with an SDK1 LLM instance."""
super().__init__(**kwargs)
self._compat = LLMCompat(
adapter_id=llm._adapter_id,
adapter_metadata=llm._adapter_metadata,
adapter_instance_id=llm._adapter_instance_id,
tool=llm._tool,
usage_kwargs=llm._usage_kwargs,
capture_metrics=llm._capture_metrics,
)
Comment on lines +33 to +43
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cat -n prompt-service/src/unstract/prompt_service/core/retrievers/retriever_llm.py

Repository: Zipstack/unstract

Length of output: 5083


🏁 Script executed:

# Check what LlamaIndexBaseLLM is and where it comes from
rg -n "class RetrieverLLM" -A 20 prompt-service/src/unstract/prompt_service/core/retrievers/retriever_llm.py

Repository: Zipstack/unstract

Length of output: 1024


🏁 Script executed:

# Check imports to see what LlamaIndexBaseLLM is
rg -n "LlamaIndexBaseLLM\|from.*LLM\|import.*LLM" prompt-service/src/unstract/prompt_service/core/retrievers/retriever_llm.py

Repository: Zipstack/unstract

Length of output: 43


🏁 Script executed:

# Check if _compat is declared elsewhere in the class
rg -n "_compat" prompt-service/src/unstract/prompt_service/core/retrievers/retriever_llm.py

Repository: Zipstack/unstract

Length of output: 447


🏁 Script executed:

# Find llama-index LLM base class to see how it's configured
find . -path "*/site-packages/llama_index*" -name "*.py" -exec grep -l "class LLM" {} \; 2>/dev/null | head -5

Repository: Zipstack/unstract

Length of output: 43


🏁 Script executed:

# Check if there's a pyproject.toml or requirements to see llama-index version
fd -n "pyproject.toml|requirements.txt" prompt-service/ | head -5

Repository: Zipstack/unstract

Length of output: 291


🏁 Script executed:

# Check if _compat attribute assignment works by looking for similar patterns in the codebase
rg -n "self\._[a-z_]+ = " --type=py | grep -v "test" | head -20

Repository: Zipstack/unstract

Length of output: 1970


🏁 Script executed:

# Check if there are other Pydantic models in the codebase that set private attributes
rg -n "PrivateAttr\|model_config\|ConfigDict" --type=py | head -30

Repository: Zipstack/unstract

Length of output: 43


🏁 Script executed:

# Look for how llama-index is used in tests to understand if this pattern works
fd -n "test.*retriever" prompt-service/ | xargs cat 2>/dev/null | head -50

Repository: Zipstack/unstract

Length of output: 291


🏁 Script executed:

# Check the requirements/pyproject to understand llama-index version
find prompt-service -name "pyproject.toml" -o -name "requirements*.txt" | xargs grep -i "llama" 2>/dev/null

Repository: Zipstack/unstract

Length of output: 87


🏁 Script executed:

# Look for how llama-index BaseModel is used elsewhere
rg "LlamaIndexBaseLLM|from llama_index" --type py -A 3 prompt-service/ | head -60

Repository: Zipstack/unstract

Length of output: 6123


🏁 Script executed:

# Check if there are test files for RetrieverLLM
find prompt-service -type f -name "*test*retriever*" -o -name "*retriever*test*" | head -5

Repository: Zipstack/unstract

Length of output: 43


🏁 Script executed:

# Get the actual imports and check llama-index version in any lock files
find . -name "poetry.lock" -o -name "Pipfile.lock" -o -name "uv.lock" | xargs grep -A 5 'llama-index' 2>/dev/null | head -80

Repository: Zipstack/unstract

Length of output: 5650


🏁 Script executed:

# Verify Pydantic handling by checking if llama-index LLM uses ConfigDict
python3 << 'EOF'
try:
    from llama_index.core.llms.llm import LLM
    import inspect
    
    # Check if model_config exists
    if hasattr(LLM, 'model_config'):
        print("LLM has model_config:", LLM.model_config)
    
    # Check base classes
    print("LLM bases:", LLM.__bases__)
    
    # Try to understand if it's a Pydantic model
    if hasattr(LLM, '__fields__'):
        print("Has __fields__ (Pydantic v1)")
    if hasattr(LLM, 'model_fields'):
        print("Has model_fields (Pydantic v2)")
        
    # Check if we can set private attributes
    test_instance = LLM()
    test_instance._test_attr = "test"
    print("Successfully set _test_attr on LLM instance")
    print("_test_attr value:", test_instance._test_attr)
except Exception as e:
    print(f"Error: {type(e).__name__}: {e}")
EOF

Repository: Zipstack/unstract

Length of output: 118


🏁 Script executed:

# Check if the code actually runs by looking for any test files that use RetrieverLLM
find prompt-service -type f -name "*.py" -path "*/test*" -exec grep -l "RetrieverLLM" {} \;

Repository: Zipstack/unstract

Length of output: 43


🏁 Script executed:

# Check if there are integration tests or examples showing RetrieverLLM usage
rg "RetrieverLLM" --type py -B 2 -A 10

Repository: Zipstack/unstract

Length of output: 7975


🏁 Script executed:

# Search for documentation about llama-index LLM Pydantic setup in comments/docstrings
rg -i "pydantic|privateattr|model_config" prompt-service/ --type py -B 2 -A 2

Repository: Zipstack/unstract

Length of output: 43


🏁 Script executed:

# Check the SDK1 LLM class to understand the private attributes being accessed
find . -path "*/unstract/sdk1/llm*" -type f | head -10

Repository: Zipstack/unstract

Length of output: 101


🏁 Script executed:

# Look at what LLMCompat expects
rg "class LLMCompat" -A 30 --type py

Repository: Zipstack/unstract

Length of output: 2505


🏁 Script executed:

# Check if the noqa directives are actually suppressing violations
# Let's look for what ANN401 is
cat -n prompt-service/src/unstract/prompt_service/core/retrievers/retriever_llm.py | grep -E "(noqa|ANN401)" | head -20

Repository: Zipstack/unstract

Length of output: 592


🏁 Script executed:

# Search for Pydantic PrivateAttr usage in the entire codebase to understand the pattern
rg "PrivateAttr" --type py -B 3 -A 3 | head -80

Repository: Zipstack/unstract

Length of output: 43


🏁 Script executed:

# Check the SDK1 LLM class definition to understand the private attributes
cat unstract/sdk1/src/unstract/sdk1/llm.py | head -100

Repository: Zipstack/unstract

Length of output: 2967


🏁 Script executed:

# Look for any documentation or tests that verify RetrieverLLM works correctly
find prompt-service -type f -name "*.py" | xargs grep -l "_compat" 2>/dev/null

Repository: Zipstack/unstract

Length of output: 136


🏁 Script executed:

# Check if there's any test infrastructure that would validate RetrieverLLM works
find . -type f -name "*.py" -path "*/test*" -exec grep -l "llama_index\|RetrieverLLM" {} \; 2>/dev/null | head -10

Repository: Zipstack/unstract

Length of output: 43


🏁 Script executed:

# Check the actual SDK1 LLM class to see if those private attributes are really private
cat unstract/sdk1/src/unstract/sdk1/llm.py | grep -E "self\._adapter_id|self\._adapter_metadata|self\._adapter_instance_id|self\._tool|self\._usage_kwargs|self\._capture_metrics" | head -20

Repository: Zipstack/unstract

Length of output: 1199


🏁 Script executed:

# Check if there are any __init__ definitions in SDK1 LLM that set these private attributes
rg "def __init__" unstract/sdk1/src/unstract/sdk1/llm.py -A 30 | head -60

Repository: Zipstack/unstract

Length of output: 2445


🏁 Script executed:

# Verify: are the noqa directives in RetrieverLLM actually suppressing flake8-annotations violations?
# Check if removing them would trigger ANN401 warnings
python3 << 'EOF'
# ANN401 is from flake8-annotations and means: "Dynamically typed expressions (Any) are disallowed"
# When you use **kwargs: Any, you're using a dynamic type (Any) without specifying what the type should be
# So the noqa: ANN401 is suppressing this warning

# In the code:
# Line 33: def __init__(self, llm: LLM, **kwargs: Any) -> None:  # noqa: ANN401
# The **kwargs: Any is the dynamic type expression being suppressed

# Lines 57, 72, 80, 88, 97, 112, 120, 128: All have **kwargs: Any  # noqa: ANN401
# These are all legitimate uses of the noqa directive

# So the claim about "unused noqa directives" appears to be FALSE
# All 9 noqa: ANN401 directives are suppressing legitimate ANN401 violations

print("Analysis: All noqa: ANN401 directives are legitimate and not unused")
print("They are suppressing the ANN401 violation for '**kwargs: Any' type hints")
EOF

Repository: Zipstack/unstract

Length of output: 201


🏁 Script executed:

# Let's check what the actual Pydantic configuration is in llama-index 0.13.2
# by looking for any Python documentation or checking if it's documented
web_search "llama-index 0.13.2 LLM Pydantic configuration private attributes"

Repository: Zipstack/unstract

Length of output: 111


🏁 Script executed:

# Let me search online for llama-index 0.13.2 LLM implementation
curl -s "https://raw.githubusercontent.com/run-llama/llama_index/main/llama-index-core/llama_index/core/llms/llm.py" | head -150

Repository: Zipstack/unstract

Length of output: 4006


🏁 Script executed:

# Alternative: check if there's any CI configuration or test runner that would validate this works
find . -type f \( -name "pytest.ini" -o -name "tox.ini" -o -name ".github" -o -name "conftest.py" \) | head -5

Repository: Zipstack/unstract

Length of output: 208


🏁 Script executed:

# Let's look for any actual test files or CI config that tests RetrieverLLM
find . -path "*/.github/workflows/*.yml" -o -path "*/tests/*retriever*.py" 2>/dev/null | head -10

Repository: Zipstack/unstract

Length of output: 43


🏁 Script executed:

# Check if there are any runtime validation tests
find prompt-service -type f -name "*.py" | xargs grep -l "test.*retriever\|retriever.*test" 2>/dev/null | head -5

Repository: Zipstack/unstract

Length of output: 43


🏁 Script executed:

# Check if there's any evidence in tests or CI that validates this works
cat prompt-service/src/unstract/prompt_service/tests/conftest.py | head -50

Repository: Zipstack/unstract

Length of output: 817


🏁 Script executed:

# Look for any actual instantiation or usage tests of RetrieverLLM
rg "RetrieverLLM\|get_llama_index_llm" prompt-service --type py -B 2 -A 5 | head -80

Repository: Zipstack/unstract

Length of output: 43


🏁 Script executed:

# Verify the Pydantic configuration in llama-index by checking bridge
curl -s "https://raw.githubusercontent.com/run-llama/llama_index/main/llama-index-core/llama_index/core/bridge/pydantic.py" 2>/dev/null | head -50

Repository: Zipstack/unstract

Length of output: 1032


Declare _compat as a Pydantic PrivateAttr for proper v2 compatibility.

While the current code functions in practice, _compat should be declared as a class-level PrivateAttr() to follow Pydantic v2 conventions. This ensures the attribute is properly managed and future-proofs the code.

🔧 Suggested improvement
+from pydantic import PrivateAttr
+
 class RetrieverLLM(LlamaIndexBaseLLM):
+    _compat: LLMCompat = PrivateAttr()
+
     def __init__(self, llm: LLM, **kwargs: Any) -> None:  # noqa: ANN401
         """Initialize with an SDK1 LLM instance."""
         super().__init__(**kwargs)
         self._compat = LLMCompat(
🧰 Tools
🪛 Ruff (0.15.1)

[warning] 33-33: Unused noqa directive (non-enabled: ANN401)

Remove unused noqa directive

(RUF100)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@prompt-service/src/unstract/prompt_service/core/retrievers/retriever_llm.py`
around lines 33 - 43, Declare _compat as a Pydantic PrivateAttr at the class
level for v2 compatibility: import PrivateAttr from pydantic and add a class
attribute like "_compat: Any = PrivateAttr()" (or similar typed PrivateAttr) on
the retriever class, then keep the existing assignment to self._compat in
__init__ where LLMCompat(...) is created (referencing __init__, LLMCompat, and
the _compat attribute).


@property
def metadata(self) -> LLMMetadata:
return LLMMetadata(
is_chat_model=True,
model_name=self._compat.get_model_name(),
)

# ── Sync ─────────────────────────────────────────────────────────────────

def chat(
self,
messages: Sequence[ChatMessage],
**kwargs: Any, # noqa: ANN401
) -> ChatResponse:
result = self._compat.chat(messages, **kwargs)
return ChatResponse(
message=ChatMessage(
role=MessageRole.ASSISTANT,
content=result.message.content,
),
raw=result.raw,
)

def complete(
self,
prompt: str,
formatted: bool = False,
**kwargs: Any, # noqa: ANN401
) -> CompletionResponse:
result = self._compat.complete(prompt, formatted=formatted, **kwargs)
return CompletionResponse(text=result.text, raw=result.raw)

def stream_chat(
self,
messages: Sequence[ChatMessage],
**kwargs: Any, # noqa: ANN401
) -> ChatResponseGen:
raise NotImplementedError("stream_chat is not supported.")

def stream_complete(
self,
prompt: str,
formatted: bool = False,
**kwargs: Any, # noqa: ANN401
) -> CompletionResponseGen:
raise NotImplementedError("stream_complete is not supported.")

# ── Async ────────────────────────────────────────────────────────────────

async def achat(
self,
messages: Sequence[ChatMessage],
**kwargs: Any, # noqa: ANN401
) -> ChatResponse:
result = await self._compat.achat(messages, **kwargs)
return ChatResponse(
message=ChatMessage(
role=MessageRole.ASSISTANT,
content=result.message.content,
),
raw=result.raw,
)

async def acomplete(
self,
prompt: str,
formatted: bool = False,
**kwargs: Any, # noqa: ANN401
) -> CompletionResponse:
result = await self._compat.acomplete(prompt, formatted=formatted, **kwargs)
return CompletionResponse(text=result.text, raw=result.raw)

async def astream_chat(
self,
messages: Sequence[ChatMessage],
**kwargs: Any, # noqa: ANN401
) -> ChatResponseAsyncGen:
raise NotImplementedError("astream_chat is not supported.")

async def astream_complete(
self,
prompt: str,
formatted: bool = False,
**kwargs: Any, # noqa: ANN401
) -> CompletionResponseAsyncGen:
raise NotImplementedError("astream_complete is not supported.")
Loading