Skip to content

Commit ef00bc1

Browse files
authored
Merge pull request #174 from HolobiomicsLab/fix-main-minor-issues
Fix main minor issues
2 parents de6d438 + 728409b commit ef00bc1

8 files changed

Lines changed: 798 additions & 596 deletions

File tree

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,8 @@ Run a predefined standard question:
139139
python -m app.core.main -q 1
140140
```
141141

142+
The numbers for `-q` index into `standard_questions` in [app/core/questions.py](app/core/questions.py) (module `app.core.questions`).
143+
142144
Run a custom question:
143145

144146
```bash
@@ -151,8 +153,18 @@ Override the endpoint at runtime:
151153
python -m app.core.main -c "Which lab extracts show inhibition above 50% against Leishmania donovani?" --endpoint https://your-endpoint.example/sparql
152154
```
153155

156+
Attach a local input file to your question with `-f` (the file is copied into the session so the `FILE_ANALYZER` tool can use it):
157+
158+
```bash
159+
python -m app.core.main -c "Summarize the annotations in this file" -f path/to/your_file.csv
160+
```
161+
154162
MetaboT saves all result sets to CSV files in a temporary folder and returns the file path. When results are small, they are also displayed inline; for large result sets, only the file path is returned to avoid exceeding the LLM context window.
155163

164+
### LangSmith automated evaluation (benchmark)
165+
166+
This repository includes a LangSmith-based automated evaluation script at `app/core/tests/evaluation.py`. To run it locally you need a LangSmith API key (`LANGCHAIN_API_KEY` or `LANGSMITH_API_KEY`) and an LLM provider key (e.g. `OPENAI_API_KEY`). See [docs/examples/langsmith-evaluation.md](docs/examples/langsmith-evaluation.md).
167+
156168
### Streamlit web app
157169

158170
The repository also includes a Streamlit interface:

app/core/main.py

Lines changed: 108 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import os
2+
import shutil
23
import argparse
3-
from typing import Optional
4+
from typing import Optional, List
45
from dotenv import load_dotenv
56
from langsmith import Client
67
from pathlib import Path
8+
from app.core.session import create_user_session, initialize_session_context
79
from app.core.workflow.langraph_workflow import create_workflow, process_workflow
810
from app.core.utils import IntRange, setup_logger
911
import configparser
@@ -31,6 +33,14 @@
3133
"mistral": "MISTRAL_API_KEY"
3234
}
3335

36+
class SessionFilePreparationError(ValueError):
37+
"""Raised when CLI input files cannot be staged into the session directory."""
38+
39+
def __init__(self, source_path: Path, message: str):
40+
super().__init__(message)
41+
self.source_path = source_path
42+
43+
3444

3545
def get_api_key(provider: str) -> Optional[str]:
3646
"""
@@ -99,7 +109,8 @@ def llm_creation(api_key=None, params_file=None):
99109
Reads the parameters from the configuration file (default is params.ini) and initializes the language models.
100110
101111
Args:
102-
api_key (str, optional): The API key for the OpenAI API.
112+
api_key (str, optional): OpenAI API key; if set, used only for OpenAI-backed config sections.
113+
Other providers always use keys from the environment (see API_KEY_MAPPING).
103114
params_file (str, optional): Path to an alternate configuration file.
104115
105116
Returns:
@@ -129,8 +140,10 @@ def llm_creation(api_key=None, params_file=None):
129140
elif section.startswith("ovh"):
130141
provider = "ovh"
131142

132-
if api_key is None:
133-
api_key = get_api_key(provider)
143+
# CLI `--api-key` is OpenAI-only; do not reuse it for other providers or across iterations.
144+
section_api_key = (
145+
api_key if (api_key is not None and provider == "openai") else get_api_key(provider)
146+
)
134147

135148
model_params = {
136149
"temperature": float(temperature),
@@ -143,12 +156,12 @@ def llm_creation(api_key=None, params_file=None):
143156
base_url = config[section]["base_url"]
144157
if provider == "deepseek":
145158
model_params["openai_api_base"] = base_url
146-
model_params["openai_api_key"] = api_key
159+
model_params["openai_api_key"] = section_api_key
147160
else:
148161
model_params["base_url"] = base_url
149-
model_params["api_key"] = api_key
162+
model_params["api_key"] = section_api_key
150163
else:
151-
model_params["openai_api_key"] = api_key
164+
model_params["openai_api_key"] = section_api_key
152165

153166
llm = ChatOpenAI(**model_params)
154167
models[section] = llm
@@ -198,6 +211,72 @@ def langsmith_setup() -> Optional[Client]:
198211
return None
199212

200213

214+
def _prepare_session_files(session_id: str, file_paths: List[str]) -> Path:
215+
"""
216+
Copy user-supplied local files into the session's input directory so that
217+
the FILE_ANALYZER tool can discover them at runtime.
218+
219+
Args:
220+
session_id: Active session identifier.
221+
file_paths: List of local file paths provided via the CLI.
222+
223+
Returns:
224+
Path to the session input directory.
225+
226+
Raises:
227+
SessionFilePreparationError: If any supplied path cannot be staged safely.
228+
"""
229+
input_dir = create_user_session(session_id, input_dir=True)
230+
staged_destinations: dict[Path, Path] = {}
231+
232+
for raw_path in file_paths:
233+
src = Path(raw_path).expanduser().resolve(strict=False)
234+
if not src.exists():
235+
raise SessionFilePreparationError(src, f"File not found: {src}")
236+
if not src.is_file():
237+
raise SessionFilePreparationError(src, f"Input path is not a file: {src}")
238+
239+
dest = input_dir / src.name
240+
previous_src = staged_destinations.get(dest)
241+
if previous_src is not None:
242+
if previous_src == src:
243+
raise SessionFilePreparationError(
244+
src,
245+
f"Input file was provided more than once: {src}",
246+
)
247+
raise SessionFilePreparationError(
248+
src,
249+
(
250+
f"Cannot stage '{src}' because it would overwrite '{previous_src}' in "
251+
f"the session input directory. Rename one of the files or choose a different path."
252+
),
253+
)
254+
255+
if src == dest.resolve(strict=False):
256+
raise SessionFilePreparationError(
257+
src,
258+
f"Input file is already staged in the session directory: {src}",
259+
)
260+
261+
if dest.exists():
262+
raise SessionFilePreparationError(
263+
src,
264+
f"Cannot stage '{src}' because destination '{dest}' already exists.",
265+
)
266+
267+
try:
268+
shutil.copy2(str(src), str(dest))
269+
except (shutil.SameFileError, OSError) as exc:
270+
raise SessionFilePreparationError(
271+
src,
272+
f"Failed to stage '{src}' into '{dest}': {exc}",
273+
) from exc
274+
275+
staged_destinations[dest] = src
276+
logger.info(f"Copied '{src}' -> '{dest}'")
277+
278+
return input_dir
279+
201280
def main():
202281
"""Main function to run the workflow."""
203282
# Define command line arguments
@@ -207,6 +286,10 @@ def main():
207286
help=f"Choose a standard question number from 1 to {len(standard_questions)}.")
208287
parser.add_argument('-c', '--custom', type=str,
209288
help="Provide a custom question.")
289+
parser.add_argument(
290+
'-f', '--file', type=str, nargs='+',
291+
help="One or more local file paths to make available for the FILE_ANALYZER tool.",
292+
)
210293
parser.add_argument('-e', '--evaluation', action='store_true',
211294
help="Enable evaluation mode")
212295
parser.add_argument('--api-key', type=str,
@@ -224,21 +307,37 @@ def main():
224307
print("You must provide either a standard question number or a custom question.")
225308
return
226309

310+
# Create a user session (mirrors the Streamlit session lifecycle) and
311+
# reconfigure the logger so subsequent CLI logs land in the session file.
312+
session_id = create_user_session()
313+
initialize_session_context(session_id)
314+
global logger
315+
logger = setup_logger(__name__)
316+
317+
if args.file:
318+
try:
319+
_prepare_session_files(session_id, args.file)
320+
except SessionFilePreparationError as exc:
321+
logger.error(str(exc))
322+
print(f"Error: {exc}")
323+
return
324+
227325
# Initialize LangSmith if available
228-
langsmith_client = langsmith_setup()
326+
langsmith_setup()
229327

230328
# Get endpoint URL from arguments or environment
231329
endpoint_url = (
232330
args.endpoint
233331
or os.environ.get("KG_ENDPOINT_URL")
234332
or "https://enpkg.commons-lab.org/graphdb/repositories/ENPKG"
235333
)
236-
models = llm_creation()
334+
models = llm_creation(api_key=args.api_key)
237335

238336
try:
239337
# Create and process workflow
240338
workflow = create_workflow(
241339
models=models,
340+
session_id=session_id,
242341
endpoint_url=endpoint_url,
243342
evaluation=False,
244343
api_key=args.api_key

0 commit comments

Comments
 (0)