ming commited on
Commit
eca870b
·
1 Parent(s): 6d85a91

Fix URL normalization and improve error handling in Ollama service

Browse files

- Add _normalize_base function to handle URL protocol and formatting
- Replace 0.0.0.0 with localhost for client requests
- Improve error handling with specific exception types
- Add better logging with URL information
- Use urljoin for safer URL construction

Files changed (1) hide show
  1. app/services/summarizer.py +95 -77
app/services/summarizer.py CHANGED
@@ -2,120 +2,138 @@
2
  Ollama service integration for text summarization.
3
  """
4
  import time
5
- from typing import Dict, Any, Optional
 
 
6
  import httpx
 
7
  from app.core.config import settings
8
  from app.core.logging import get_logger
9
 
10
  logger = get_logger(__name__)
11
 
12
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
  class OllamaService:
14
  """Service for interacting with Ollama API."""
15
-
16
  def __init__(self):
17
- self.base_url = settings.ollama_host
18
  self.model = settings.ollama_model
19
  self.timeout = settings.ollama_timeout
20
-
 
 
 
21
  async def summarize_text(
22
- self,
23
- text: str,
24
- max_tokens: int = 256,
25
- prompt: str = "Summarize the following text concisely:"
26
  ) -> Dict[str, Any]:
27
  """
28
  Summarize text using Ollama.
29
-
30
- Args:
31
- text: Text to summarize
32
- max_tokens: Maximum tokens for summary
33
- prompt: Custom prompt for summarization
34
-
35
- Returns:
36
- Dictionary containing summary and metadata
37
-
38
- Raises:
39
- httpx.HTTPError: If Ollama API call fails
40
  """
41
  start_time = time.time()
42
-
43
- # Calculate dynamic timeout based on text length
44
- # Base timeout + additional time for longer texts
45
  text_length = len(text)
46
- dynamic_timeout = self.timeout + max(0, (text_length - 1000) // 1000 * 5) # +5s per 1000 chars over 1000
47
-
48
- # Cap the timeout at 2 minutes to prevent extremely long waits
49
- dynamic_timeout = min(dynamic_timeout, 120)
50
-
51
- logger.info(f"Processing text of {text_length} characters with timeout of {dynamic_timeout}s")
52
-
53
- # Prepare the full prompt
54
  full_prompt = f"{prompt}\n\n{text}"
55
-
56
- # Prepare request payload
57
  payload = {
58
  "model": self.model,
59
  "prompt": full_prompt,
60
  "stream": False,
61
  "options": {
62
  "num_predict": max_tokens,
63
- "temperature": 0.3, # Lower temperature for more consistent summaries
64
- }
65
  }
66
-
 
 
 
67
  try:
68
- # Debug logging
69
- full_url = f"{self.base_url}/api/generate"
70
- logger.info(f"Making request to: {full_url}")
71
- logger.info(f"Base URL: {self.base_url}")
72
-
73
  async with httpx.AsyncClient(timeout=dynamic_timeout) as client:
74
- response = await client.post(
75
- full_url,
76
- json=payload
77
- )
78
- response.raise_for_status()
79
-
80
- result = response.json()
81
-
82
- # Calculate processing time
83
- latency_ms = (time.time() - start_time) * 1000
84
-
85
- return {
86
- "summary": result.get("response", "").strip(),
87
- "model": self.model,
88
- "tokens_used": result.get("eval_count", 0),
89
- "latency_ms": round(latency_ms, 2)
90
- }
91
-
92
  except httpx.TimeoutException:
93
- logger.error(f"Timeout calling Ollama API after {dynamic_timeout}s for text of {text_length} characters")
94
- # Re-raise the TimeoutException so the API layer can handle it properly
 
 
 
 
 
 
95
  raise
96
- except httpx.HTTPError as e:
97
- logger.error(f"HTTP error calling Ollama API: {e}")
 
 
 
 
98
  raise
99
  except Exception as e:
100
- logger.error(f"Unexpected error calling Ollama API: {e}")
101
- raise httpx.HTTPError(f"Ollama API error: {str(e)}")
102
-
 
103
  async def check_health(self) -> bool:
104
  """
105
- Check if Ollama service is available.
106
-
107
- Returns:
108
- True if Ollama is reachable, False otherwise
109
  """
 
 
 
110
  try:
111
- # Debug logging for health check
112
- health_url = f"{self.base_url}/api/tags"
113
- logger.info(f"Health check URL: {health_url}")
114
- logger.info(f"Base URL for health check: {self.base_url}")
115
-
116
- async with httpx.AsyncClient(timeout=5) as client:
117
- response = await client.get(health_url)
118
- return response.status_code == 200
 
 
 
 
 
 
119
  except Exception as e:
120
  logger.warning(f"Ollama health check failed: {e}")
121
  return False
 
2
  Ollama service integration for text summarization.
3
  """
4
  import time
5
+ from typing import Dict, Any
6
+ from urllib.parse import urljoin
7
+
8
  import httpx
9
+
10
  from app.core.config import settings
11
  from app.core.logging import get_logger
12
 
13
  logger = get_logger(__name__)
14
 
15
 
16
+ def _normalize_base(url: str) -> str:
17
+ """
18
+ Ensure a usable base URL:
19
+ - add http:// if scheme missing
20
+ - replace 0.0.0.0 (bind addr) with localhost for client requests
21
+ - ensure trailing slash for safe urljoin
22
+ """
23
+ v = (url or "").strip()
24
+ if not v:
25
+ v = "http://localhost:11434"
26
+ if not (v.startswith("http://") or v.startswith("https://")):
27
+ v = "http://" + v
28
+ if "://0.0.0.0:" in v:
29
+ v = v.replace("://0.0.0.0:", "://localhost:")
30
+ if not v.endswith("/"):
31
+ v += "/"
32
+ return v
33
+
34
+
35
  class OllamaService:
36
  """Service for interacting with Ollama API."""
37
+
38
  def __init__(self):
39
+ self.base_url = _normalize_base(settings.ollama_host)
40
  self.model = settings.ollama_model
41
  self.timeout = settings.ollama_timeout
42
+
43
+ logger.info(f"Ollama base URL (normalized): {self.base_url}")
44
+ logger.info(f"Ollama model: {self.model}")
45
+
46
  async def summarize_text(
47
+ self,
48
+ text: str,
49
+ max_tokens: int = 256,
50
+ prompt: str = "Summarize the following text concisely:",
51
  ) -> Dict[str, Any]:
52
  """
53
  Summarize text using Ollama.
54
+ Raises httpx.HTTPError (and subclasses) on failure.
 
 
 
 
 
 
 
 
 
 
55
  """
56
  start_time = time.time()
57
+
58
+ # Dynamic timeout: base + 5s per extra 1000 chars (cap 120s)
 
59
  text_length = len(text)
60
+ dynamic_timeout = min(self.timeout + max(0, (text_length - 1000) // 1000 * 5), 120)
61
+
62
+ logger.info(f"Processing text of {text_length} chars with timeout {dynamic_timeout}s")
63
+
 
 
 
 
64
  full_prompt = f"{prompt}\n\n{text}"
65
+
 
66
  payload = {
67
  "model": self.model,
68
  "prompt": full_prompt,
69
  "stream": False,
70
  "options": {
71
  "num_predict": max_tokens,
72
+ "temperature": 0.3,
73
+ },
74
  }
75
+
76
+ generate_url = urljoin(self.base_url, "api/generate")
77
+ logger.info(f"POST {generate_url}")
78
+
79
  try:
 
 
 
 
 
80
  async with httpx.AsyncClient(timeout=dynamic_timeout) as client:
81
+ resp = await client.post(generate_url, json=payload)
82
+ resp.raise_for_status()
83
+ data = resp.json()
84
+
85
+ latency_ms = (time.time() - start_time) * 1000.0
86
+ return {
87
+ "summary": (data.get("response") or "").strip(),
88
+ "model": self.model,
89
+ "tokens_used": data.get("eval_count", 0),
90
+ "latency_ms": round(latency_ms, 2),
91
+ }
92
+
 
 
 
 
 
 
93
  except httpx.TimeoutException:
94
+ logger.error(
95
+ f"Timeout calling Ollama after {dynamic_timeout}s "
96
+ f"(chars={text_length}, url={generate_url})"
97
+ )
98
+ raise
99
+ except httpx.RequestError as e:
100
+ # Network / connection errors (DNS, refused, TLS, etc.)
101
+ logger.error(f"Request error calling Ollama at {generate_url}: {e}")
102
  raise
103
+ except httpx.HTTPStatusError as e:
104
+ # Non-2xx responses
105
+ body = e.response.text if e.response is not None else ""
106
+ logger.error(
107
+ f"HTTP {e.response.status_code if e.response else '??'} from Ollama at {generate_url}: {body[:400]}"
108
+ )
109
  raise
110
  except Exception as e:
111
+ logger.error(f"Unexpected error calling Ollama at {generate_url}: {e}")
112
+ # Present a consistent error type to callers
113
+ raise httpx.HTTPError(f"Ollama API error: {e}") from e
114
+
115
  async def check_health(self) -> bool:
116
  """
117
+ Verify Ollama is reachable and (optionally) that the model exists.
 
 
 
118
  """
119
+ tags_url = urljoin(self.base_url, "api/tags")
120
+ logger.info(f"GET {tags_url} (health)")
121
+
122
  try:
123
+ async with httpx.AsyncClient(timeout=5.0) as client:
124
+ resp = await client.get(tags_url)
125
+ resp.raise_for_status()
126
+ tags = resp.json()
127
+
128
+ # If you want to *require* the model to exist, uncomment below:
129
+ # available = {m.get("name") for m in tags.get("models", []) if isinstance(m, dict)}
130
+ # if self.model and self.model not in available:
131
+ # logger.warning(f"Model '{self.model}' not found in Ollama tags: {available}")
132
+ # # Still return True for connectivity; or return False to fail hard
133
+ # return True
134
+
135
+ return True
136
+
137
  except Exception as e:
138
  logger.warning(f"Ollama health check failed: {e}")
139
  return False