jaegglic commited on
Commit
fbe1045
·
1 Parent(s): 4fe60f4

Added possibility to test connection

Browse files
vianu/spock/__main__.py CHANGED
@@ -2,19 +2,34 @@ from argparse import Namespace
2
  import asyncio
3
  from datetime import datetime
4
  import logging
 
5
  import sys
6
- from typing import List, Tuple
 
 
7
 
8
  from vianu import LOG_FMT
9
- from vianu.spock.settings import SCRAPING_SOURCES, LOG_LEVEL
10
  from vianu.spock.src.cli import parse_args
11
  from vianu.spock.src.base import Setup, Document, SpoCK, FileHandler
12
  from vianu.spock.src import scraping as scp
13
  from vianu.spock.src import ner
14
 
15
-
16
  logging.basicConfig(format=LOG_FMT, level=LOG_LEVEL)
17
  logger = logging.getLogger(__name__)
 
 
 
 
 
 
 
 
 
 
 
 
 
18
 
19
 
20
  async def _orchestrator(
@@ -73,7 +88,7 @@ async def _orchestrator(
73
  await ner_queue.put(None)
74
 
75
 
76
- def setup_asyncio_framework(args_: Namespace) -> Tuple[asyncio.Queue, List[asyncio.Task], List[asyncio.Task], asyncio.Task]:
77
  """Set up the asyncio framework for the SpoCK application."""
78
  # Set up arguments
79
  if args_.source is None:
@@ -86,7 +101,7 @@ def setup_asyncio_framework(args_: Namespace) -> Tuple[asyncio.Queue, List[async
86
 
87
  # Start tasks
88
  scp_tasks = scp.create_tasks(args_=args_, queue_in=src_queue, queue_out=scp_queue)
89
- ner_tasks = ner.create_tasks(args_=args_, queue_in=scp_queue, queue_out=ner_queue)
90
  orc_task = asyncio.create_task(
91
  _orchestrator(
92
  args_=args_,
@@ -125,10 +140,22 @@ async def main(args_: Namespace | None = None, save: bool = True) -> None:
125
  args_= parse_args(sys.argv[1:])
126
 
127
  logging.basicConfig(level=args_.log_level.upper(), format=LOG_FMT)
128
- logger.info(f'starting SpoCK (args_={args_})')
 
 
 
 
 
 
 
 
 
 
 
 
129
 
130
  # Set up async structure (scraping queue/tasks, NER queue/tasks, orchestrator task)
131
- ner_queue, _, _, _ = setup_asyncio_framework(args_)
132
 
133
  # Set up collector task and wait for it to finish
134
  # NOTE: if collector task is finished, the orchestrator is also finished (because of the sentinel in `ner_queue`)
 
2
  import asyncio
3
  from datetime import datetime
4
  import logging
5
+ import os
6
  import sys
7
+ from typing import List, Tuple, Dict, Any
8
+
9
+ from dotenv import load_dotenv
10
 
11
  from vianu import LOG_FMT
12
+ from vianu.spock.settings import SCRAPING_SOURCES, LOG_LEVEL, MODEL_TEST_QUESTION
13
  from vianu.spock.src.cli import parse_args
14
  from vianu.spock.src.base import Setup, Document, SpoCK, FileHandler
15
  from vianu.spock.src import scraping as scp
16
  from vianu.spock.src import ner
17
 
 
18
  logging.basicConfig(format=LOG_FMT, level=LOG_LEVEL)
19
  logger = logging.getLogger(__name__)
20
+ load_dotenv()
21
+
22
+
23
+ def get_model_config() -> Dict[str, Dict[str, Any]]:
24
+ """Get model configuration."""
25
+ return {
26
+ 'openai': {
27
+ 'api_key': os.getenv('OPENAI_API_KEY'),
28
+ },
29
+ 'llama': {
30
+ 'base_url': os.getenv('OLLAMA_BASE_URL'),
31
+ },
32
+ }
33
 
34
 
35
  async def _orchestrator(
 
88
  await ner_queue.put(None)
89
 
90
 
91
+ def setup_asyncio_framework(args_: Namespace, model_config: Dict[str, Any]) -> Tuple[asyncio.Queue, List[asyncio.Task], List[asyncio.Task], asyncio.Task]:
92
  """Set up the asyncio framework for the SpoCK application."""
93
  # Set up arguments
94
  if args_.source is None:
 
101
 
102
  # Start tasks
103
  scp_tasks = scp.create_tasks(args_=args_, queue_in=src_queue, queue_out=scp_queue)
104
+ ner_tasks = ner.create_tasks(args_=args_, queue_in=scp_queue, queue_out=ner_queue, model_config=model_config)
105
  orc_task = asyncio.create_task(
106
  _orchestrator(
107
  args_=args_,
 
140
  args_= parse_args(sys.argv[1:])
141
 
142
  logging.basicConfig(level=args_.log_level.upper(), format=LOG_FMT)
143
+ logger.info(f'starting SpoCK (args_={args_})')
144
+
145
+ # Test availability of NER model
146
+ model = args_.model
147
+ model_config = get_model_config()
148
+ try:
149
+ _ner = ner.NERFactory.create(model=model, config=model_config)
150
+ test_task = asyncio.create_task(_ner.test_model_endpoint())
151
+ test_answer = await test_task
152
+ logger.debug(f"test model endpoint of model='{model}': '{MODEL_TEST_QUESTION}' was answered with '{test_answer}'")
153
+ except Exception as e:
154
+ logger.error(f'could not reach model endpoint: {e}')
155
+ raise e
156
 
157
  # Set up async structure (scraping queue/tasks, NER queue/tasks, orchestrator task)
158
+ ner_queue, _, _, _ = setup_asyncio_framework(args_=args_, model_config=model_config)
159
 
160
  # Set up collector task and wait for it to finish
161
  # NOTE: if collector task is finished, the orchestrator is also finished (because of the sentinel in `ner_queue`)
vianu/spock/app/app.py CHANGED
@@ -10,26 +10,30 @@ from dotenv import load_dotenv
10
  import gradio as gr
11
 
12
  from vianu.spock.settings import LOG_LEVEL, N_SCP_TASKS, N_NER_TASKS
13
- from vianu.spock.settings import LARGE_LANGUAGE_MODELS, SCRAPING_SOURCES, MAX_DOCS_SRC
14
  from vianu.spock.settings import GRADIO_APP_NAME, GRADIO_SERVER_PORT, GRADIO_MAX_JOBS, GRADIO_UPDATE_INTERVAL
15
- from vianu.spock.settings import OLLAMA_BASE_URL_ENV_NAME, OPENAI_API_KEY_ENV_NAME
16
  from vianu.spock.src.base import Setup, SpoCK, SpoCKList, QueueItem # noqa: F401
17
  from vianu import BaseApp
18
- from vianu.spock.__main__ import setup_asyncio_framework
19
  import vianu.spock.app.formatter as fmt
 
20
 
21
  logger = logging.getLogger(__name__)
22
  load_dotenv()
23
 
24
  # App settings
25
  _ASSETS_PATH = Path(__file__).parents[1] / "assets"
26
- _UI_SETTINGS_LLM_CHOICES = [(name, value) for name, value in zip(['Ollama', 'OpenAI'], LARGE_LANGUAGE_MODELS)]
27
- _UI_SETTINGS_SOURCE_CHOICES = [(name, value) for name, value in zip(['PubMed', 'EMA', 'MHRA', 'FDA'], SCRAPING_SOURCES)]
28
  if not len(_UI_SETTINGS_LLM_CHOICES) == len(LARGE_LANGUAGE_MODELS):
29
  raise ValueError('LARGE_LANGUAGE_MODELS and _UI_SETTINGS_LLM_CHOICES must have the same length')
 
 
30
  if not len(_UI_SETTINGS_SOURCE_CHOICES) == len(SCRAPING_SOURCES):
31
  raise ValueError('SCRAPING_SOURCES and _UI_SETTINGS_SOURCE_CHOICES must have the same length')
32
 
 
 
33
 
34
  @dataclass
35
  class LocalState:
@@ -55,6 +59,12 @@ class SessionState:
55
  # Data
56
  is_running: bool = False
57
  spocks: SpoCKList = field(default_factory=list)
 
 
 
 
 
 
58
  _index_running_spock: int | None = None
59
  _index_active_spock: int | None = None
60
 
@@ -113,25 +123,11 @@ class App(BaseApp):
113
  with gr.Column(scale=1):
114
  with gr.Accordion(label='LLM Endpoint'):
115
  self._components['settings.llm_radio'] = gr.Radio(
116
- label='Model', show_label=False, choices=_UI_SETTINGS_LLM_CHOICES, value='llama', interactive=True
117
  )
118
 
119
- # 'llama' specific settings
120
- with gr.Group(visible=True) as self._components['settings.ollama_group']:
121
- value = os.environ.get("OLLAMA_BASE_URL")
122
- placeholder = 'base_url of ollama endpoint' if value is None else None
123
- gr.Markdown('---')
124
- self._components['settings.ollama_base_url'] = gr.Textbox(
125
- label='base_url',
126
- show_label=False,
127
- info='base_url',
128
- placeholder=placeholder,
129
- value=value,
130
- interactive=True,
131
- )
132
-
133
  # 'openai' specific settings
134
- with gr.Group(visible=False) as self._components['settings.openai_group']:
135
  value = os.environ.get("OPENAI_API_KEY")
136
  placeholder = 'api_key of openai endpoint' if value is None else None
137
  logger.debug(f'openai api_key={value}')
@@ -146,6 +142,23 @@ class App(BaseApp):
146
  type='password',
147
  )
148
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
149
  with gr.Accordion(label='Sources', open=True):
150
  self._components['settings.source'] = gr.CheckboxGroup(
151
  label='Sources', show_label=False, choices=_UI_SETTINGS_SOURCE_CHOICES, value=SCRAPING_SOURCES, interactive=True
@@ -193,27 +206,48 @@ class App(BaseApp):
193
  # Helpers
194
  # --------------------------------------------------------------------------
195
  @staticmethod
196
- def _show_llm_settings(llm: str) -> Tuple[dict[str, Any], dict[str, Any]]:
 
197
  logger.debug(f'show {llm} model settings')
 
198
  if llm == 'llama':
199
- return gr.update(visible=True), gr.update(visible=False)
200
  elif llm == 'openai':
201
- return gr.update(visible=False), gr.update(visible=True)
202
  else:
203
- return gr.update(visible=False), gr.update(visible=False)
204
 
205
  @staticmethod
206
- def _set_ollama_base_url(base_url: str) -> None:
207
- """Setup ollama base_url as environment variable."""
208
- logger.debug(f'set ollama base_url environment variable ({OLLAMA_BASE_URL_ENV_NAME}={base_url})')
209
- os.environ[OLLAMA_BASE_URL_ENV_NAME] = base_url
 
 
 
210
 
211
  @staticmethod
212
- def _set_openai_api_key(api_key: str) -> None:
213
- """Setup openai api_key as environment variable."""
214
- log_key = '*****' if api_key else 'None'
215
- logger.debug(f'set openai api key (api_key={log_key})')
216
- os.environ[OPENAI_API_KEY_ENV_NAME] = api_key
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
217
 
218
  @staticmethod
219
  def _feed_cards_to_ui(local_state: dict, session_state: SessionState) -> List[dict[str, Any]]:
@@ -243,15 +277,20 @@ class App(BaseApp):
243
  logger.debug(f'feeding details to UI (len(data)={len(active_spock.data)})')
244
  return fmt.get_details_html(active_spock.data)
245
 
246
- @staticmethod
247
- def _check_llm_settings(llm: str) -> None:
248
- """Check if the LLM settings are set."""
249
- if llm == 'llama':
250
- if os.environ.get(OLLAMA_BASE_URL_ENV_NAME) is None:
251
- raise gr.Error('Ollama base_url is not set (submit value with Enter)')
252
- elif llm == 'openai':
253
- if os.environ.get(OPENAI_API_KEY_ENV_NAME) is None:
254
- raise gr.Error('OpenAI api_key is not set (submit value with Enter)')
 
 
 
 
 
255
 
256
  @staticmethod
257
  def _toggle_button(session_state: SessionState) -> Tuple[SessionState, dict[str, Any], dict[str, Any], dict[str, Any]]:
@@ -348,7 +387,7 @@ class App(BaseApp):
348
 
349
  # Setup asyncio tasks as in `vianu.spock.__main__`
350
  args_ = session_state.get_running_spock().setup.to_namespace()
351
- ner_queue, scp_tasks, ner_tasks, orc_task = setup_asyncio_framework(args_=args_)
352
  session_state.ner_queue = ner_queue
353
  session_state.scp_tasks = scp_tasks
354
  session_state.ner_tasks = ner_tasks
@@ -442,25 +481,42 @@ class App(BaseApp):
442
  """Choose LLM model show the correspoding settings."""
443
  self._components['settings.llm_radio'].change(
444
  fn=self._show_llm_settings,
445
- inputs=self._components['settings.llm_radio'],
 
 
 
446
  outputs=[
447
  self._components['settings.ollama_group'],
448
  self._components['settings.openai_group'],
 
449
  ],
450
  )
 
 
 
 
 
 
 
 
451
 
452
  def _event_settings_ollama(self):
453
  """Callback of the ollama settings."""
454
- self._components['settings.ollama_base_url'].submit(
455
  fn=self._set_ollama_base_url,
456
- inputs=self._components['settings.ollama_base_url'],
 
457
  )
458
 
459
- def _event_settings_openai(self):
460
- """Callback of the openai settings."""
461
- self._components['settings.openai_api_key'].submit(
462
- fn=self._set_openai_api_key,
463
- inputs=self._components['settings.openai_api_key'],
 
 
 
 
464
  )
465
 
466
  def _event_start_spock(self) -> None:
@@ -471,7 +527,11 @@ class App(BaseApp):
471
  gr.on(
472
  triggers=[search_term.submit, start_button.click],
473
  fn=self._check_llm_settings,
474
- inputs=self._components['settings.llm_radio'],
 
 
 
 
475
  ).success(
476
  fn=self._toggle_button,
477
  inputs=self._session_state,
@@ -568,6 +628,7 @@ class App(BaseApp):
568
  self._event_choose_llm()
569
  self._event_settings_ollama()
570
  self._event_settings_openai()
 
571
 
572
  # Start/Stop events
573
  self._event_start_spock()
 
10
  import gradio as gr
11
 
12
  from vianu.spock.settings import LOG_LEVEL, N_SCP_TASKS, N_NER_TASKS
13
+ from vianu.spock.settings import LARGE_LANGUAGE_MODELS, SCRAPING_SOURCES, MAX_DOCS_SRC, MODEL_TEST_QUESTION
14
  from vianu.spock.settings import GRADIO_APP_NAME, GRADIO_SERVER_PORT, GRADIO_MAX_JOBS, GRADIO_UPDATE_INTERVAL
 
15
  from vianu.spock.src.base import Setup, SpoCK, SpoCKList, QueueItem # noqa: F401
16
  from vianu import BaseApp
17
+ from vianu.spock.__main__ import get_model_config, setup_asyncio_framework
18
  import vianu.spock.app.formatter as fmt
19
+ from vianu.spock.src.ner import NERFactory
20
 
21
  logger = logging.getLogger(__name__)
22
  load_dotenv()
23
 
24
  # App settings
25
  _ASSETS_PATH = Path(__file__).parents[1] / "assets"
26
+
27
+ _UI_SETTINGS_LLM_CHOICES = [(name, value) for name, value in zip(['OpenAI', 'Ollama'], LARGE_LANGUAGE_MODELS)]
28
  if not len(_UI_SETTINGS_LLM_CHOICES) == len(LARGE_LANGUAGE_MODELS):
29
  raise ValueError('LARGE_LANGUAGE_MODELS and _UI_SETTINGS_LLM_CHOICES must have the same length')
30
+
31
+ _UI_SETTINGS_SOURCE_CHOICES = [(name, value) for name, value in zip(['PubMed', 'EMA', 'MHRA', 'FDA'], SCRAPING_SOURCES)]
32
  if not len(_UI_SETTINGS_SOURCE_CHOICES) == len(SCRAPING_SOURCES):
33
  raise ValueError('SCRAPING_SOURCES and _UI_SETTINGS_SOURCE_CHOICES must have the same length')
34
 
35
+ _MODEL_CONFIG = get_model_config()
36
+
37
 
38
  @dataclass
39
  class LocalState:
 
59
  # Data
60
  is_running: bool = False
61
  spocks: SpoCKList = field(default_factory=list)
62
+
63
+ # Model config
64
+ model_config: Dict[str, Dict[str, Any]] = field(default_factory=lambda: _MODEL_CONFIG)
65
+ connection_is_valid: bool = False
66
+
67
+ # Indexes
68
  _index_running_spock: int | None = None
69
  _index_active_spock: int | None = None
70
 
 
123
  with gr.Column(scale=1):
124
  with gr.Accordion(label='LLM Endpoint'):
125
  self._components['settings.llm_radio'] = gr.Radio(
126
+ label='Model', show_label=False, choices=_UI_SETTINGS_LLM_CHOICES, value='openai', interactive=True
127
  )
128
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
129
  # 'openai' specific settings
130
+ with gr.Group(visible=True) as self._components['settings.openai_group']:
131
  value = os.environ.get("OPENAI_API_KEY")
132
  placeholder = 'api_key of openai endpoint' if value is None else None
133
  logger.debug(f'openai api_key={value}')
 
142
  type='password',
143
  )
144
 
145
+ # 'llama' specific settings
146
+ with gr.Group(visible=False) as self._components['settings.ollama_group']:
147
+ value = os.environ.get("OLLAMA_BASE_URL")
148
+ placeholder = 'base_url of ollama endpoint' if value is None else None
149
+ gr.Markdown('---')
150
+ self._components['settings.ollama_base_url'] = gr.Textbox(
151
+ label='base_url',
152
+ show_label=False,
153
+ info='base_url',
154
+ placeholder=placeholder,
155
+ value=value,
156
+ interactive=True,
157
+ )
158
+
159
+ self._components['settings.test_connection_button'] = gr.Button(value='Test connection', interactive=True)
160
+
161
+
162
  with gr.Accordion(label='Sources', open=True):
163
  self._components['settings.source'] = gr.CheckboxGroup(
164
  label='Sources', show_label=False, choices=_UI_SETTINGS_SOURCE_CHOICES, value=SCRAPING_SOURCES, interactive=True
 
206
  # Helpers
207
  # --------------------------------------------------------------------------
208
  @staticmethod
209
+ def _show_llm_settings(llm: str, session_state: SessionState) -> Tuple[dict[str, Any], dict[str, Any], SessionState]:
210
+ """Show the settings for the selected LLM model."""
211
  logger.debug(f'show {llm} model settings')
212
+ session_state.connection_is_valid = False
213
  if llm == 'llama':
214
+ return gr.update(visible=True), gr.update(visible=False), session_state
215
  elif llm == 'openai':
216
+ return gr.update(visible=False), gr.update(visible=True), session_state
217
  else:
218
+ return gr.update(visible=False), gr.update(visible=False), session_state
219
 
220
  @staticmethod
221
+ def _set_openai_api_key(api_key: str, session_state: SessionState) -> SessionState:
222
+ """Setup openai api_key"""
223
+ log_key = '*****' if api_key else 'None'
224
+ logger.debug(f'set openai api_key {log_key}')
225
+ session_state.model_config['openai'] = {'api_key': api_key}
226
+ session_state.connection_is_valid = False
227
+ return session_state
228
 
229
  @staticmethod
230
+ def _set_ollama_base_url(base_url: str, session_state: SessionState) -> SessionState:
231
+ """Setup ollama base_url"""
232
+ logger.debug(f'set ollama base_url={base_url}')
233
+ session_state.model_config['llama'] = {'base_url': base_url}
234
+ session_state.connection_is_valid = False
235
+ return session_state
236
+
237
+ @staticmethod
238
+ async def _test_connection(llm: str, session_state: SessionState) -> SessionState:
239
+ logger.debug(f'test connection to model={llm}')
240
+ try:
241
+ ner = NERFactory.create(model=llm, config=session_state.model_config)
242
+ test_task = asyncio.create_task(ner.test_model_endpoint())
243
+ test_answer = await test_task
244
+ gr.Info(f"connection to model={llm} is valid: '{MODEL_TEST_QUESTION}' was answered with '{test_answer}'")
245
+ session_state.connection_is_valid = True
246
+ except Exception as e:
247
+ session_state.connection_is_valid = False
248
+ raise gr.Error(f'connection to model={llm} failed: {e}')
249
+ return session_state
250
+
251
 
252
  @staticmethod
253
  def _feed_cards_to_ui(local_state: dict, session_state: SessionState) -> List[dict[str, Any]]:
 
277
  logger.debug(f'feeding details to UI (len(data)={len(active_spock.data)})')
278
  return fmt.get_details_html(active_spock.data)
279
 
280
+ async def _check_llm_settings(self, llm: str, session_state: SessionState) -> SessionState:
281
+ """Check if the LLM settings are set correcly."""
282
+ # Check if the settings are set
283
+ if llm == 'openai':
284
+ if session_state.model_config.get(llm) is None or session_state.model_config[llm].get('api_key') is None:
285
+ raise gr.Error('OpenAI api_key is not set')
286
+ elif llm == 'llama':
287
+ if session_state.model_config.get(llm) is None or session_state.model_config[llm].get('base_url') is None:
288
+ raise gr.Error('Ollama base_url is not set')
289
+
290
+ # Check connection to the model
291
+ if not session_state.connection_is_valid:
292
+ session_state = await self._test_connection(llm=llm, session_state=session_state)
293
+ return session_state
294
 
295
  @staticmethod
296
  def _toggle_button(session_state: SessionState) -> Tuple[SessionState, dict[str, Any], dict[str, Any], dict[str, Any]]:
 
387
 
388
  # Setup asyncio tasks as in `vianu.spock.__main__`
389
  args_ = session_state.get_running_spock().setup.to_namespace()
390
+ ner_queue, scp_tasks, ner_tasks, orc_task = setup_asyncio_framework(args_=args_, model_config=session_state.model_config)
391
  session_state.ner_queue = ner_queue
392
  session_state.scp_tasks = scp_tasks
393
  session_state.ner_tasks = ner_tasks
 
481
  """Choose LLM model show the correspoding settings."""
482
  self._components['settings.llm_radio'].change(
483
  fn=self._show_llm_settings,
484
+ inputs=[
485
+ self._components['settings.llm_radio'],
486
+ self._session_state,
487
+ ],
488
  outputs=[
489
  self._components['settings.ollama_group'],
490
  self._components['settings.openai_group'],
491
+ self._session_state,
492
  ],
493
  )
494
+
495
+ def _event_settings_openai(self):
496
+ """Callback of the openai settings."""
497
+ self._components['settings.openai_api_key'].change(
498
+ fn=self._set_openai_api_key,
499
+ inputs=[self._components['settings.openai_api_key'], self._session_state],
500
+ outputs=self._session_state,
501
+ )
502
 
503
  def _event_settings_ollama(self):
504
  """Callback of the ollama settings."""
505
+ self._components['settings.ollama_base_url'].change(
506
  fn=self._set_ollama_base_url,
507
+ inputs=[self._components['settings.ollama_base_url'], self._session_state],
508
+ outputs=self._session_state,
509
  )
510
 
511
+ def _event_test_connection(self):
512
+ """Test the connection to the LLM model."""
513
+ self._components['settings.test_connection_button'].click(
514
+ fn=self._test_connection,
515
+ inputs=[
516
+ self._components['settings.llm_radio'],
517
+ self._session_state,
518
+ ],
519
+ outputs=self._session_state,
520
  )
521
 
522
  def _event_start_spock(self) -> None:
 
527
  gr.on(
528
  triggers=[search_term.submit, start_button.click],
529
  fn=self._check_llm_settings,
530
+ inputs=[
531
+ self._components['settings.llm_radio'],
532
+ self._session_state,
533
+ ],
534
+ outputs=self._session_state,
535
  ).success(
536
  fn=self._toggle_button,
537
  inputs=self._session_state,
 
628
  self._event_choose_llm()
629
  self._event_settings_ollama()
630
  self._event_settings_openai()
631
+ self._event_test_connection()
632
 
633
  # Start/Stop events
634
  self._event_start_spock()
vianu/spock/app/formatter.py CHANGED
@@ -11,6 +11,7 @@ JOBS_CONTAINER_CARD_TEMPLATE = """
11
  <div class="card" onclick="cardClickHandler(this)">
12
  <div class="title">{title} {status}</div>
13
  <div class="info">Date: {date}</div>
 
14
  <div class="info">Sources: {sources}</div>
15
  <div class="info">#docs: {n_doc} | #adr: {n_adr}</div>
16
  </div>
@@ -78,13 +79,14 @@ def _get_status_html(status: str) -> str:
78
 
79
  def get_job_card_html(card_nmbr: int, spock: SpoCK):
80
  """Get the HTML for the job card."""
81
- job = spock.setup
82
  data = spock.data
83
 
84
  title = spock.setup.term
85
  status = _get_status_html(spock.status)
86
- sources = ", ".join(job.source)
87
- date = job.submission.strftime(DATE_FORMAT)
 
88
  n_doc = len(data)
89
  n_adr = sum([len(d.adverse_reactions) for d in data])
90
  return JOBS_CONTAINER_CARD_TEMPLATE.format(
@@ -92,6 +94,7 @@ def get_job_card_html(card_nmbr: int, spock: SpoCK):
92
  title=title,
93
  status=status,
94
  date=date,
 
95
  sources=sources,
96
  n_doc=n_doc,
97
  n_adr=n_adr,
 
11
  <div class="card" onclick="cardClickHandler(this)">
12
  <div class="title">{title} {status}</div>
13
  <div class="info">Date: {date}</div>
14
+ <div class="info">Model: {model}</div>
15
  <div class="info">Sources: {sources}</div>
16
  <div class="info">#docs: {n_doc} | #adr: {n_adr}</div>
17
  </div>
 
79
 
80
  def get_job_card_html(card_nmbr: int, spock: SpoCK):
81
  """Get the HTML for the job card."""
82
+ setup = spock.setup
83
  data = spock.data
84
 
85
  title = spock.setup.term
86
  status = _get_status_html(spock.status)
87
+ date = setup.submission.strftime(DATE_FORMAT)
88
+ model = setup.model
89
+ sources = ", ".join(setup.source)
90
  n_doc = len(data)
91
  n_adr = sum([len(d.adverse_reactions) for d in data])
92
  return JOBS_CONTAINER_CARD_TEMPLATE.format(
 
94
  title=title,
95
  status=status,
96
  date=date,
97
+ model=model,
98
  sources=sources,
99
  n_doc=n_doc,
100
  n_adr=n_adr,
vianu/spock/settings.py CHANGED
@@ -23,10 +23,11 @@ PUBMED_DB = 'pubmed'
23
  PUBMED_BATCH_SIZE = 20
24
 
25
  # NER settings
26
- LARGE_LANGUAGE_MODELS = ['llama', 'openai']
27
  MAX_TOKENS = 128.000
28
  LLAMA_MODEL='llama3.2'
29
  OPENAI_MODEL='gpt-4o'
30
  N_NER_TASKS = 2
31
  OLLAMA_BASE_URL_ENV_NAME = "OLLAMA_BASE_URL"
32
  OPENAI_API_KEY_ENV_NAME = "OPENAI_API_KEY"
 
 
23
  PUBMED_BATCH_SIZE = 20
24
 
25
  # NER settings
26
+ LARGE_LANGUAGE_MODELS = ['openai', 'llama']
27
  MAX_TOKENS = 128.000
28
  LLAMA_MODEL='llama3.2'
29
  OPENAI_MODEL='gpt-4o'
30
  N_NER_TASKS = 2
31
  OLLAMA_BASE_URL_ENV_NAME = "OLLAMA_BASE_URL"
32
  OPENAI_API_KEY_ENV_NAME = "OPENAI_API_KEY"
33
+ MODEL_TEST_QUESTION = 'Are you available?'
vianu/spock/src/cli.py CHANGED
@@ -27,7 +27,7 @@ def parse_args(args_: Sequence) -> argparse.Namespace:
27
 
28
  # Add NER group
29
  ner_gp = parser.add_argument_group('ner')
30
- ner_gp.add_argument('--model', '-m', type=str, choices=LARGE_LANGUAGE_MODELS, default='llama', help='NER model')
31
  ner_gp.add_argument('--n-ner-tasks', metavar='', type=int, default=N_NER_TASKS, help='number of async ner tasks')
32
 
33
  return parser.parse_args(args_)
 
27
 
28
  # Add NER group
29
  ner_gp = parser.add_argument_group('ner')
30
+ ner_gp.add_argument('--model', '-m', type=str, choices=LARGE_LANGUAGE_MODELS, default='openai', help='NER model')
31
  ner_gp.add_argument('--n-ner-tasks', metavar='', type=int, default=N_NER_TASKS, help='number of async ner tasks')
32
 
33
  return parser.parse_args(args_)
vianu/spock/src/ner.py CHANGED
@@ -7,14 +7,12 @@ import os
7
  import re
8
  from typing import List
9
 
10
- from dotenv import load_dotenv
11
  from openai import AsyncOpenAI
12
 
13
  from vianu.spock.src.base import NamedEntity, QueueItem # noqa: F401
14
- from vianu.spock.settings import N_CHAR_DOC_ID, LLAMA_MODEL, OPENAI_MODEL
15
 
16
  logger = logging.getLogger(__name__)
17
- load_dotenv()
18
 
19
  NAMED_ENTITY_PROMPT = """
20
  You are an expert in Natural Language Processing. Your task is to identify named entities (NER) in a given text.
@@ -89,12 +87,27 @@ class NER(ABC):
89
  "content": text,
90
  },
91
  ]
 
 
 
 
 
 
 
 
 
 
 
 
92
 
93
 
94
  @abstractmethod
95
- async def _get_ner_model_answer(self, text: str) -> str:
96
  raise NotImplementedError('OpenAINER._get_ner_model_answer is not implemented yet')
97
 
 
 
 
98
 
99
  async def apply(self, queue_in: asyncio.Queue, queue_out: asyncio.Queue) -> None:
100
  """Apply NER to a text received from input queue and put the results in an output queue."""
@@ -114,7 +127,7 @@ class NER(ABC):
114
  self.logger.debug(f'starting ner for item.id_={id_} (doc.id_={doc.id_[:N_CHAR_DOC_ID]})')
115
  try:
116
  text = doc.text
117
- content = await self._get_ner_model_answer(text=text)
118
  except Exception as e:
119
  self.logger.error(f'error during ner for item.id_={item.id_} (doc.id_={doc.id_[:N_CHAR_DOC_ID]}): {e}')
120
  queue_in.task_done()
@@ -147,16 +160,38 @@ class NER(ABC):
147
  # Put the document in the output queue
148
  await queue_out.put(item)
149
  queue_in.task_done()
150
- self.logger.info(f'finished NER task for item.id_={id_} (doc.id_={doc.id_[:N_CHAR_DOC_ID]})')
151
 
152
 
153
- class OllamaNER(NER):
154
 
155
- def __init__(self, base_url: str, model: str):
156
  super().__init__()
157
- self._base_url = base_url
158
  self._model = model
 
 
 
 
 
 
 
 
159
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
160
 
161
  def _get_http_data(self, text: str, stream: bool = False) -> dict:
162
  messages = self._get_messages(text=text)
@@ -166,9 +201,8 @@ class OllamaNER(NER):
166
  "stream": stream,
167
  }
168
  return data
169
-
170
 
171
- async def _get_ner_model_answer(self, text: str) -> str:
172
  data = self._get_http_data(text=text)
173
  async with aiohttp.ClientSession() as session:
174
  url = f'{self._base_url}/api/chat/'
@@ -177,44 +211,57 @@ class OllamaNER(NER):
177
  resp_json = await response.json()
178
  content = resp_json['message']['content']
179
  return content
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
180
 
181
 
182
- class OpenAINER(NER):
 
183
 
184
- def __init__(self, api_key: str, model: str):
185
- super().__init__()
186
- self._model = model
187
- self._client = AsyncOpenAI(api_key=api_key)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
188
 
189
-
190
- async def _get_ner_model_answer(self, text: str) -> str:
191
- messages = self._get_messages(text=text)
192
- chat_completion = await self._client.chat.completions.create(
193
- messages=messages,
194
- model=self._model,
195
- )
196
- return chat_completion.choices[0].message.content
197
 
198
 
199
- def create_tasks(args_: Namespace, queue_in: asyncio.Queue, queue_out: asyncio.Queue) -> List[asyncio.Task]:
200
  """Create asyncio NER tasks."""
201
  n_ner_tasks = args_.n_ner_tasks
202
  model = args_.model
203
-
204
- if model == 'llama':
205
- base_url = os.environ.get('OLLAMA_BASE_URL')
206
- if base_url is None:
207
- raise EnvironmentError("The ollama endpoint must be set by the OLLAMA_ENDPOINT environment variable")
208
- ner = OllamaNER(base_url=base_url, model=LLAMA_MODEL)
209
-
210
- elif model == 'openai':
211
- api_key = os.environ.get('OPENAI_API_KEY')
212
- if api_key is None:
213
- raise EnvironmentError("The api_key for the OpenAI client must be set by the OPENAI_API_KEY environment variable")
214
- ner = OpenAINER(api_key=api_key, model=OPENAI_MODEL)
215
-
216
- else:
217
- raise ValueError(f"unknown ner model '{args_.model}'")
218
 
219
  logger.info(f'setting up {n_ner_tasks} NER task(s)')
220
  tasks = [asyncio.create_task(ner.apply(queue_in=queue_in, queue_out=queue_out)) for _ in range(n_ner_tasks)]
 
7
  import re
8
  from typing import List
9
 
 
10
  from openai import AsyncOpenAI
11
 
12
  from vianu.spock.src.base import NamedEntity, QueueItem # noqa: F401
13
+ from vianu.spock.settings import N_CHAR_DOC_ID, LLAMA_MODEL, OPENAI_MODEL, MODEL_TEST_QUESTION
14
 
15
  logger = logging.getLogger(__name__)
 
16
 
17
  NAMED_ENTITY_PROMPT = """
18
  You are an expert in Natural Language Processing. Your task is to identify named entities (NER) in a given text.
 
87
  "content": text,
88
  },
89
  ]
90
+
91
+ @staticmethod
92
+ def _get_test_messages(text: str) -> List[dict]:
93
+ return [
94
+ {
95
+ "role": "system",
96
+ "content": "Answer very briefly."},
97
+ {
98
+ "role": "user",
99
+ "content": text,
100
+ },
101
+ ]
102
 
103
 
104
  @abstractmethod
105
+ async def _get_model_answer(self, text: str) -> str:
106
  raise NotImplementedError('OpenAINER._get_ner_model_answer is not implemented yet')
107
 
108
+ @abstractmethod
109
+ async def test_model_endpoint(self) -> str:
110
+ pass
111
 
112
  async def apply(self, queue_in: asyncio.Queue, queue_out: asyncio.Queue) -> None:
113
  """Apply NER to a text received from input queue and put the results in an output queue."""
 
127
  self.logger.debug(f'starting ner for item.id_={id_} (doc.id_={doc.id_[:N_CHAR_DOC_ID]})')
128
  try:
129
  text = doc.text
130
+ content = await self._get_model_answer(text=text)
131
  except Exception as e:
132
  self.logger.error(f'error during ner for item.id_={item.id_} (doc.id_={doc.id_[:N_CHAR_DOC_ID]}): {e}')
133
  queue_in.task_done()
 
160
  # Put the document in the output queue
161
  await queue_out.put(item)
162
  queue_in.task_done()
163
+ self.logger.info(f'finished NER task for item.id_={id_} (doc.id_={doc.id_[:N_CHAR_DOC_ID]})')
164
 
165
 
166
+ class OpenAINER(NER):
167
 
168
+ def __init__(self, model: str, api_key: str):
169
  super().__init__()
 
170
  self._model = model
171
+ self._client = AsyncOpenAI(api_key=api_key)
172
+
173
+ async def _chat_completion(self, messages: List[dict]) -> str:
174
+ chat_completion = await self._client.chat.completions.create(
175
+ messages=messages,
176
+ model=self._model,
177
+ )
178
+ return chat_completion.choices[0].message.content
179
 
180
+ async def _get_model_answer(self, text: str) -> str:
181
+ messages = self._get_messages(text=text)
182
+ return await self._chat_completion(messages=messages)
183
+
184
+ async def test_model_endpoint(self) -> str:
185
+ messages = self._get_test_messages(text=MODEL_TEST_QUESTION)
186
+ return await self._chat_completion(messages=messages)
187
+
188
+
189
+ class OllamaNER(NER):
190
+
191
+ def __init__(self, model: str, base_url: str):
192
+ super().__init__()
193
+ self._model = model
194
+ self._base_url = base_url
195
 
196
  def _get_http_data(self, text: str, stream: bool = False) -> dict:
197
  messages = self._get_messages(text=text)
 
201
  "stream": stream,
202
  }
203
  return data
 
204
 
205
+ async def _get_model_answer(self, text: str) -> str:
206
  data = self._get_http_data(text=text)
207
  async with aiohttp.ClientSession() as session:
208
  url = f'{self._base_url}/api/chat/'
 
211
  resp_json = await response.json()
212
  content = resp_json['message']['content']
213
  return content
214
+
215
+ async def test_model_endpoint(self) -> str:
216
+ messages = self._get_test_messages(text=MODEL_TEST_QUESTION)
217
+ data = {
218
+ 'model': self._model,
219
+ "messages": messages,
220
+ 'stream': False,
221
+ }
222
+ async with aiohttp.ClientSession() as session:
223
+ url = f'{self._base_url}/api/chat/'
224
+ async with session.post(url, json=data) as response:
225
+ response.raise_for_status()
226
+ resp_json = await response.json()
227
+ content = resp_json['message']['content']
228
+ return content
229
 
230
 
231
+ class NERFactory:
232
+ """Factory for NER models."""
233
 
234
+ @staticmethod
235
+ def create(model: str, config: dict) -> NER:
236
+ """Create a NER model from the `model` keyword."""
237
+
238
+ # create a :class:`OllamaNER` instance
239
+ if model == 'openai':
240
+ api_key = config[model].get('api_key')
241
+ if api_key is None:
242
+ api_key = os.environ.get('OPENAI_API_KEY')
243
+ if api_key is None:
244
+ raise ValueError("The api_key for the OpenAI client is missing (set it by the OPENAI_API_KEY environment variable or in the settings)")
245
+ ner = OpenAINER(model=OPENAI_MODEL, api_key=api_key)
246
+
247
+ elif model == 'llama':
248
+ base_url = config[model].get('base_url')
249
+ if base_url is None:
250
+ base_url = os.environ.get('OLLAMA_BASE_URL')
251
+ if base_url is None:
252
+ raise ValueError("The base_url for the ollama endpoint is missing")
253
+ ner = OllamaNER(model=LLAMA_MODEL, base_url=base_url)
254
+ else:
255
+ raise ValueError(f"unknown ner model '{model}'")
256
 
257
+ return ner
 
 
 
 
 
 
 
258
 
259
 
260
+ def create_tasks(args_: Namespace, queue_in: asyncio.Queue, queue_out: asyncio.Queue, model_config: dict) -> List[asyncio.Task]:
261
  """Create asyncio NER tasks."""
262
  n_ner_tasks = args_.n_ner_tasks
263
  model = args_.model
264
+ ner = NERFactory.create(model=model, config=model_config)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
265
 
266
  logger.info(f'setting up {n_ner_tasks} NER task(s)')
267
  tasks = [asyncio.create_task(ner.apply(queue_in=queue_in, queue_out=queue_out)) for _ in range(n_ner_tasks)]
vianu/spock/src/scraping.py CHANGED
@@ -9,7 +9,7 @@ The module contains three main classes:
9
  from abc import ABC, abstractmethod
10
  from argparse import Namespace
11
  import asyncio
12
- from dataclasses import dataclass
13
  from datetime import datetime
14
  from io import BytesIO
15
  import logging
@@ -284,9 +284,9 @@ class PubmedScraper(Scraper):
284
  @dataclass
285
  class SearchResults:
286
  """Class for storing the search results from different databases."""
287
- count: int | None
288
- n_pages: int | None
289
- items: List[Tag]
290
 
291
 
292
  class EMAScraper(Scraper):
 
9
  from abc import ABC, abstractmethod
10
  from argparse import Namespace
11
  import asyncio
12
+ from dataclasses import dataclass, field
13
  from datetime import datetime
14
  from io import BytesIO
15
  import logging
 
284
  @dataclass
285
  class SearchResults:
286
  """Class for storing the search results from different databases."""
287
+ count: int | None = None
288
+ n_pages: int | None = None
289
+ items: List[Tag] = field(default_factory=list)
290
 
291
 
292
  class EMAScraper(Scraper):