1+ """Fireworks Reranker for document compression."""
2+
3+ from __future__ import annotations
4+
5+ from collections .abc import Sequence
6+ from copy import deepcopy
7+ from typing import Any
8+
9+ from langchain_core .callbacks import Callbacks
10+ from langchain_core .documents import BaseDocumentCompressor , Document
11+ from langchain_core .utils import secret_from_env
12+ from openai import OpenAI
13+ from pydantic import BaseModel , ConfigDict , Field , SecretStr , model_validator
14+ from typing_extensions import Self
15+
16+
17+ class FireworksRerank (BaseModel ):
18+ """Document reranker powered by Fireworks.
19+
20+ This reranker uses the Fireworks API to reorder documents based on their
21+ relevance to a query. It implements the `BaseDocumentCompressor` interface
22+ for use with LangChain's contextual compression retrievers.
23+
24+ Setup:
25+ Install `langchain_fireworks` and set environment variable
26+ `FIREWORKS_API_KEY`.
27+
28+ ```bash
29+ pip install -U langchain_fireworks
30+ export FIREWORKS_API_KEY="your-api-key"
31+ ```
32+
33+ Key init args:
34+ model: Name of Fireworks reranking model to use.
35+ Default: "accounts/fireworks/models/reranker"
36+ top_n: Number of documents to return. Default: 3
37+ fireworks_api_key: Fireworks API key.
38+
39+ Example:
40+ ```python
41+ from langchain_fireworks import FireworksRerank
42+ from langchain.retrievers.contextual_compression import ContextualCompressionRetriever
43+
44+ reranker = FireworksRerank(top_n=3)
45+ compression_retriever = ContextualCompressionRetriever(
46+ base_compressor=reranker, base_retriever=base_retriever
47+ )
48+ compressed_docs = compression_retriever.invoke("your query")
49+ ```
50+ """
51+
52+ client : Any = Field (default = None , exclude = True ) # type: ignore[assignment]
53+ """Fireworks API client for reranking."""
54+
55+ top_n : int | None = 3
56+ """Number of documents to return."""
57+
58+ model : str = "accounts/fireworks/models/reranker"
59+ """Model to use for reranking."""
60+
61+ fireworks_api_key : SecretStr = Field (
62+ alias = "api_key" ,
63+ default_factory = secret_from_env (
64+ "FIREWORKS_API_KEY" ,
65+ error_message = (
66+ "You must specify an api key. "
67+ "You can pass it an argument as `api_key=...` or "
68+ "set the environment variable `FIREWORKS_API_KEY`."
69+ ),
70+ ),
71+ )
72+ """Fireworks API key.
73+
74+ Automatically read from env variable `FIREWORKS_API_KEY` if not provided.
75+ """
76+
77+ user_agent : str = "langchain"
78+ """Identifier for the application making the request."""
79+
80+ model_config = ConfigDict (
81+ populate_by_name = True ,
82+ arbitrary_types_allowed = True ,
83+ )
84+
85+ @model_validator (mode = "after" )
86+ def validate_environment (self ) -> Self :
87+ """Validate environment variables and initialize client."""
88+ self .client = OpenAI (
89+ api_key = self .fireworks_api_key .get_secret_value (),
90+ base_url = "https://api.fireworks.ai/inference/v1" ,
91+ )
92+ return self
93+
94+ def rerank (
95+ self ,
96+ documents : Sequence [str | Document | dict ],
97+ query : str ,
98+ * ,
99+ model : str | None = None ,
100+ top_n : int | None = - 1 ,
101+ ) -> list [dict [str , Any ]]:
102+ """Returns an ordered list of documents ordered by their relevance to the query.
103+
104+ Args:
105+ query: The query to use for reranking.
106+ documents: A sequence of documents to rerank.
107+ model: The model to use for re-ranking. Default to self.model.
108+ top_n: The number of results to return. If `None` returns all results.
109+
110+ Returns:
111+ List of dicts containing index and relevance_score.
112+ """
113+ if len (documents ) == 0 : # to avoid empty api call
114+ return []
115+
116+ docs = [
117+ doc .page_content if isinstance (doc , Document ) else doc
118+ for doc in documents
119+ ]
120+
121+ model = model or self .model
122+ top_n = top_n if (top_n is None or top_n > 0 ) else self .top_n
123+
124+ response = self .client .post (
125+ "/rerank" ,
126+ json = {
127+ "query" : query ,
128+ "documents" : docs ,
129+ "model" : model ,
130+ "top_n" : top_n ,
131+ },
132+ )
133+
134+ if not response .is_success :
135+ response .raise_for_status ()
136+
137+ data = response .json ()
138+
139+ if "data" not in data :
140+ raise ValueError (f"Unexpected response format: { data } " )
141+
142+ return [
143+ {"index" : item ["index" ], "relevance_score" : item ["score" ]}
144+ for item in data ["data" ]
145+ ]
146+
147+ def compress_documents (
148+ self ,
149+ documents : Sequence [Document ],
150+ query : str ,
151+ callbacks : Callbacks | None = None ,
152+ ) -> Sequence [Document ]:
153+ """Compress documents using Fireworks' rerank API.
154+
155+ Args:
156+ documents: A sequence of documents to compress.
157+ query: The query to use for compressing the documents.
158+ callbacks: Callbacks to run during the compression process.
159+
160+ Returns:
161+ A sequence of compressed documents.
162+ """
163+ compressed = []
164+ for res in self .rerank (documents , query ):
165+ doc = documents [res ["index" ]]
166+ doc_copy = Document (
167+ doc .page_content , metadata = deepcopy (doc .metadata )
168+ )
169+ doc_copy .metadata ["relevance_score" ] = res ["relevance_score" ]
170+ compressed .append (doc_copy )
171+ return compressed
172+
173+ async def acompress_documents (
174+ self ,
175+ documents : Sequence [Document ],
176+ query : str ,
177+ callbacks : Callbacks | None = None ,
178+ ) -> Sequence [Document ]:
179+ """Async compress documents using Fireworks' rerank API."""
180+ # For now, just call the sync version
181+ # Can be optimized later with async client
182+ return self .compress_documents (documents , query , callbacks )
0 commit comments