Skip to content

Commit ae54578

Browse files
authored
Merge branch 'main' into claude/create-openapi-readme-sX2ry
2 parents b504d37 + 93ed79c commit ae54578

39 files changed

Lines changed: 1448 additions & 664 deletions

backend/app/llm/openai/prompts.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,3 +62,15 @@
6262
Output:
6363
Return a JSON object exactly matching the provided schema.
6464
"""
65+
66+
CHAT_PROMPT = """
67+
Role: You are an expert scientific research assistant.
68+
Task: Answer the user's questions based strictly on the provided excerpts from a research paper.
69+
70+
Rules:
71+
1. **Groundedness:** Only use the provided context. If the answer isn't in the context, say:
72+
"I'm sorry, I couldn't find specific information about that in this paper."
73+
2. **Citations:** When possible, refer to specific sections or data mentioned in the snippets.
74+
3. **Format:** Use Markdown for clarity. Use LaTeX for math ($...$ or $$...$$).
75+
4. **Tone:** Academic, precise, and helpful.
76+
"""

backend/app/llm/openai/provider.py

Lines changed: 188 additions & 160 deletions
Original file line numberDiff line numberDiff line change
@@ -1,160 +1,188 @@
1-
import json
2-
from typing import Any, Dict, List, Optional
3-
4-
from openai import AsyncOpenAI
5-
6-
from app.core.config import settings
7-
from app.llm.openai.prompts import KEYWORD_PROMPT, PDF_KEYWORD_PROMPT, SUMMARIZATION_PROMPT
8-
9-
10-
class OpenAIProvider:
11-
"""Wrapper around the OpenAI client."""
12-
13-
_model: str
14-
15-
def __init__(self) -> None:
16-
if settings.OPENAI_API_KEY is None:
17-
raise RuntimeError(
18-
"OPENAI_API_KEY is not set. Please configure it in your environment."
19-
)
20-
21-
self.client = AsyncOpenAI(api_key=settings.OPENAI_API_KEY)
22-
self._model = "gpt-5-nano-2025-08-07"
23-
24-
async def extract_keywords(self, user_text: str) -> List[str]:
25-
"""
26-
Extracts a list of keywords from a given user text using the OpenAI model.
27-
Returns a list of strings. If parsing fails, returns an empty list.
28-
"""
29-
30-
response = await self.client.responses.create(
31-
model=self._model,
32-
reasoning={"effort": "low"},
33-
input=[
34-
{
35-
"role": "developer",
36-
"content": KEYWORD_PROMPT,
37-
},
38-
{
39-
"role": "user",
40-
"content": user_text,
41-
},
42-
],
43-
)
44-
45-
try:
46-
keyword_list = json.loads(response.output_text)
47-
except json.decoder.JSONDecodeError:
48-
keyword_list = []
49-
50-
return keyword_list
51-
52-
async def extract_keywords_from_pdf(
53-
self,
54-
pdf_text: str,
55-
query: Optional[str] = None,
56-
) -> List[str]:
57-
"""
58-
Extracts a list of search queries from the full text of a paper and an optional query.
59-
Returns a list of strings.
60-
"""
61-
user_focus = query or "N/A"
62-
63-
user_content = f"User focus (optional): {user_focus}\n\n" f"Paper text: \n{pdf_text}"
64-
65-
response = await self.client.responses.create(
66-
model=self._model,
67-
reasoning={"effort": "medium"},
68-
input=[
69-
{
70-
"role": "developer",
71-
"content": PDF_KEYWORD_PROMPT,
72-
},
73-
{
74-
"role": "user",
75-
"content": user_content,
76-
},
77-
],
78-
)
79-
80-
try:
81-
keyword_list = json.loads(response.output_text)
82-
except json.decoder.JSONDecodeError:
83-
keyword_list = []
84-
85-
return keyword_list
86-
87-
async def summarise_paper(self, paper_text: str, query: str) -> Dict[str, Any]:
88-
"""
89-
Creates a summary of a scientific paper.
90-
Dynamically adjusts schema to include 'relevance_to_query' only if a query is present.
91-
"""
92-
has_query = query and query.strip()
93-
94-
properties = {
95-
"title": {"type": "string"},
96-
"executive_summary": {"type": "string"},
97-
"methodology_points": {"type": "array", "items": {"type": "string"}},
98-
"results_points": {"type": "array", "items": {"type": "string"}},
99-
"limitations": {"type": "string"},
100-
}
101-
102-
required_fields = [
103-
"title",
104-
"executive_summary",
105-
"methodology_points",
106-
"results_points",
107-
]
108-
109-
if has_query:
110-
properties["relevance_to_query"] = {"type": "string"}
111-
required_fields.append("relevance_to_query")
112-
113-
schema = {
114-
"type": "object",
115-
"additionalProperties": False,
116-
"properties": properties,
117-
"required": required_fields,
118-
}
119-
120-
prompt_content = SUMMARIZATION_PROMPT
121-
if has_query:
122-
prompt_content += f"\n\nUser query: {query}"
123-
124-
response = await self.client.responses.create(
125-
model=self._model,
126-
reasoning={"effort": "medium"},
127-
input=[
128-
{
129-
"role": "developer",
130-
"content": prompt_content,
131-
},
132-
{
133-
"role": "user",
134-
"content": paper_text,
135-
},
136-
],
137-
text={
138-
"format": {
139-
"type": "json_schema",
140-
"name": "paper_summary",
141-
"schema": schema,
142-
"strict": False,
143-
}
144-
},
145-
)
146-
147-
try:
148-
data = json.loads(response.output_text)
149-
except (json.decoder.JSONDecodeError, KeyError):
150-
data = {
151-
"title": "Summary (Parsing Fallback)",
152-
"executive_summary": response.output_text.strip(),
153-
"methodology_points": [],
154-
"results_points": [],
155-
"limitations": "Parsing failed.",
156-
}
157-
if has_query:
158-
data["relevance_to_query"] = "Could not parse specific section."
159-
160-
return data
1+
import json
2+
from typing import Any, Dict, List, Optional, cast
3+
4+
from openai import AsyncOpenAI
5+
6+
from app.core.config import settings
7+
from app.llm.openai.prompts import (
8+
CHAT_PROMPT,
9+
KEYWORD_PROMPT,
10+
PDF_KEYWORD_PROMPT,
11+
SUMMARIZATION_PROMPT,
12+
)
13+
14+
15+
class OpenAIProvider:
16+
"""Wrapper around the OpenAI client."""
17+
18+
_model: str
19+
20+
def __init__(self) -> None:
21+
if settings.OPENAI_API_KEY is None:
22+
raise RuntimeError(
23+
"OPENAI_API_KEY is not set. Please configure it in your environment."
24+
)
25+
26+
self.client = AsyncOpenAI(api_key=settings.OPENAI_API_KEY)
27+
self._model = "gpt-5-nano-2025-08-07"
28+
29+
async def extract_keywords(self, user_text: str) -> List[str]:
30+
"""
31+
Extracts a list of keywords from a given user text using the OpenAI model.
32+
Returns a list of strings. If parsing fails, returns an empty list.
33+
"""
34+
35+
response = await self.client.responses.create(
36+
model=self._model,
37+
reasoning={"effort": "low"},
38+
input=[
39+
{
40+
"role": "developer",
41+
"content": KEYWORD_PROMPT,
42+
},
43+
{
44+
"role": "user",
45+
"content": user_text,
46+
},
47+
],
48+
)
49+
50+
try:
51+
keyword_list = json.loads(response.output_text)
52+
except json.decoder.JSONDecodeError:
53+
keyword_list = []
54+
55+
return keyword_list
56+
57+
async def extract_keywords_from_pdf(
58+
self,
59+
pdf_text: str,
60+
query: Optional[str] = None,
61+
) -> List[str]:
62+
"""
63+
Extracts a list of search queries from the full text of a paper and an optional query.
64+
Returns a list of strings.
65+
"""
66+
user_focus = query or "N/A"
67+
68+
user_content = f"User focus (optional): {user_focus}\n\n" f"Paper text: \n{pdf_text}"
69+
70+
response = await self.client.responses.create(
71+
model=self._model,
72+
reasoning={"effort": "medium"},
73+
input=[
74+
{
75+
"role": "developer",
76+
"content": PDF_KEYWORD_PROMPT,
77+
},
78+
{
79+
"role": "user",
80+
"content": user_content,
81+
},
82+
],
83+
)
84+
85+
try:
86+
keyword_list = json.loads(response.output_text)
87+
except json.decoder.JSONDecodeError:
88+
keyword_list = []
89+
90+
return keyword_list
91+
92+
async def summarise_paper(self, paper_text: str, query: str) -> Dict[str, Any]:
93+
"""
94+
Creates a summary of a scientific paper.
95+
Dynamically adjusts schema to include 'relevance_to_query' only if a query is present.
96+
"""
97+
has_query = query and query.strip()
98+
99+
properties = {
100+
"title": {"type": "string"},
101+
"executive_summary": {"type": "string"},
102+
"methodology_points": {"type": "array", "items": {"type": "string"}},
103+
"results_points": {"type": "array", "items": {"type": "string"}},
104+
"limitations": {"type": "string"},
105+
}
106+
107+
required_fields = [
108+
"title",
109+
"executive_summary",
110+
"methodology_points",
111+
"results_points",
112+
]
113+
114+
if has_query:
115+
properties["relevance_to_query"] = {"type": "string"}
116+
required_fields.append("relevance_to_query")
117+
118+
schema = {
119+
"type": "object",
120+
"additionalProperties": False,
121+
"properties": properties,
122+
"required": required_fields,
123+
}
124+
125+
prompt_content = SUMMARIZATION_PROMPT
126+
if has_query:
127+
prompt_content += f"\n\nUser query: {query}"
128+
129+
response = await self.client.responses.create(
130+
model=self._model,
131+
reasoning={"effort": "medium"},
132+
input=[
133+
{
134+
"role": "developer",
135+
"content": prompt_content,
136+
},
137+
{
138+
"role": "user",
139+
"content": paper_text,
140+
},
141+
],
142+
text={
143+
"format": {
144+
"type": "json_schema",
145+
"name": "paper_summary",
146+
"schema": schema,
147+
"strict": False,
148+
}
149+
},
150+
)
151+
152+
try:
153+
data = json.loads(response.output_text)
154+
except (json.decoder.JSONDecodeError, KeyError):
155+
data = {
156+
"title": "Summary (Parsing Fallback)",
157+
"executive_summary": response.output_text.strip(),
158+
"methodology_points": [],
159+
"results_points": [],
160+
"limitations": "Parsing failed.",
161+
}
162+
if has_query:
163+
data["relevance_to_query"] = "Could not parse specific section."
164+
165+
return data
166+
167+
async def chat_about_paper(
168+
self, paper_text: str, user_query: str, chat_history: List[Dict[str, str]]
169+
) -> str:
170+
"""
171+
Handles a chat turn using the full paper text as context.
172+
"""
173+
174+
input_messages: List[Dict[str, str]] = [
175+
{"role": "developer", "content": CHAT_PROMPT},
176+
{"role": "developer", "content": f"RESEARCH PAPER TEXT:\n\n{paper_text}"},
177+
]
178+
179+
if chat_history:
180+
input_messages.extend(chat_history)
181+
182+
input_messages.append({"role": "user", "content": user_query})
183+
184+
response = await self.client.responses.create(
185+
model=self._model, reasoning={"effort": "medium"}, input=cast(Any, input_messages)
186+
)
187+
188+
return response.output_text.strip()

backend/app/main.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,13 @@
77

88
from app.core.config import settings
99
from app.core.database import init_db
10-
from app.routes import auth_routes, paper_routes, project_routes, search_routes, user_routes
10+
from app.routes import (
11+
auth_routes,
12+
paper_routes,
13+
project_routes,
14+
search_routes,
15+
user_routes,
16+
)
1117

1218
# ---------------------------------------------------------
1319
# Configure Logging

0 commit comments

Comments
 (0)