valeriow commited on
Commit
ccd3906
·
verified ·
1 Parent(s): 80b7c70

Upload 2 files

Browse files

feat: first version

Files changed (2) hide show
  1. app.py +353 -0
  2. requirements.txt +7 -0
app.py ADDED
@@ -0,0 +1,353 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import dspy
2
+ import pandas as pd
3
+ import requests
4
+ from ddgs import DDGS
5
+ import wikipedia
6
+ import json
7
+ from typing import List, Optional
8
+ from pydantic import BaseModel
9
+ from dataclasses import dataclass
10
+ import gradio as gr
11
+ from google.colab import userdata
12
+ import os
13
+
14
+ # Define a threshold for confidence
15
+ threshold_perc_confianca = 0.95
16
+
17
+ # Definir estruturas de dados padronizadas
18
+ @dataclass
19
+ class SearchResult:
20
+ """Individual search result structure."""
21
+ title: str
22
+ snippet: str
23
+ url: str
24
+ relevance_score: Optional[float] = None
25
+
26
+ class ToolResponse(BaseModel):
27
+ """Standardized response format for all tools."""
28
+ success: bool
29
+ tool_name: str
30
+ query: str
31
+ results_count: int
32
+ results: List[dict]
33
+ error_message: Optional[str] = None
34
+ metadata: dict = {}
35
+
36
+ def search_web(query: str, max_results: int = 5) -> str:
37
+ """Search the web using DuckDuckGo API (free). Returns JSON format."""
38
+ response = ToolResponse(
39
+ success=False,
40
+ tool_name="search_web",
41
+ query=query,
42
+ results_count=0,
43
+ results=[],
44
+ metadata={"max_results": max_results}
45
+ )
46
+
47
+ try:
48
+ with DDGS() as ddgs:
49
+ results = list(ddgs.text(query, max_results=max_results))
50
+
51
+ if not results:
52
+ response.error_message = f"No results found for '{query}'"
53
+ return response.model_dump_json(indent=2)
54
+
55
+ # Format results for the agent
56
+ formatted_results = []
57
+ for i, result in enumerate(results[:max_results], 1):
58
+ formatted_results.append({
59
+ "rank": i,
60
+ "title": result.get('title', 'No title'),
61
+ "snippet": result.get('body', 'No description'),
62
+ "url": result.get('href', 'No URL'),
63
+
64
+ "source": "duckduckgo"
65
+ })
66
+
67
+ response.success = True
68
+ response.results_count = len(formatted_results)
69
+ response.results = formatted_results
70
+
71
+ except Exception as e:
72
+ response.error_message = f"Error searching for '{query}': {str(e)}"
73
+
74
+ return response.model_dump_json(indent=2)
75
+
76
+ def search_wikipedia(query: str, sentences: int = 3) -> str:
77
+ """Search Wikipedia for information. Returns JSON format."""
78
+ response = ToolResponse(
79
+ success=False,
80
+ tool_name="search_wikipedia",
81
+ query=query,
82
+ results_count=0,
83
+ results=[],
84
+ metadata={"sentences": sentences, "language": "pt"}
85
+ )
86
+
87
+ try:
88
+ # Set language to Portuguese for Brazilian context
89
+ wikipedia.set_lang("pt")
90
+
91
+ # Search for pages
92
+ search_results = wikipedia.search(query, results=3)
93
+
94
+ if not search_results:
95
+ response.error_message = f"No Wikipedia results found for '{query}'"
96
+ return response.model_dump_json(indent=2)
97
+
98
+ # Get summary of first result
99
+ page_title = search_results[0]
100
+ summary = wikipedia.summary(page_title, sentences=sentences)
101
+ page_url = wikipedia.page(page_title).url
102
+
103
+ response.success = True
104
+ response.results_count = 1
105
+ response.results = [{
106
+ "rank": 1,
107
+ "title": page_title,
108
+ "snippet": summary,
109
+ "url": page_url,
110
+
111
+ "source": "wikipedia",
112
+ "alternative_titles": search_results[1:] if len(search_results) > 1 else []
113
+ }]
114
+
115
+ except wikipedia.exceptions.DisambiguationError as e:
116
+ # Handle disambiguation
117
+ try:
118
+ page_title = e.options[0]
119
+ summary = wikipedia.summary(page_title, sentences=sentences)
120
+ page_url = wikipedia.page(page_title).url
121
+
122
+ response.success = True
123
+ response.results_count = 1
124
+ response.results = [{
125
+ "rank": 1,
126
+ "title": page_title,
127
+ "snippet": summary,
128
+ "url": page_url,
129
+
130
+ "source": "wikipedia",
131
+ "disambiguation_options": e.options[:5]
132
+ }]
133
+ response.metadata["disambiguation_resolved"] = True
134
+
135
+ except Exception as inner_e:
136
+ response.error_message = f"Disambiguation error for '{query}': {str(inner_e)}"
137
+ response.metadata["disambiguation_options"] = e.options[:5]
138
+
139
+ except Exception as e:
140
+ response.error_message = f"Error searching Wikipedia for '{query}': {str(e)}"
141
+
142
+ return response.model_dump_json(indent=2)
143
+
144
+ def search_news_verification(claim: str) -> str:
145
+ """Search for fact-checking and verification information. Returns JSON format."""
146
+ response = ToolResponse(
147
+ success=False,
148
+ tool_name="search_news_verification",
149
+ query=claim,
150
+ results_count=0,
151
+ results=[],
152
+ metadata={"search_type": "fact_check", "target_sites": ["snopes", "factcheck", "boatos.org", "e-farsas"]}
153
+ )
154
+
155
+ try:
156
+ # Search for fact-checking sites specifically
157
+ fact_check_query = f"{claim} fact check verificação OR snopes OR factcheck OR boatos.org OR e-farsas"
158
+
159
+ with DDGS() as ddgs:
160
+ results = list(ddgs.text(fact_check_query, max_results=3))
161
+
162
+ if not results:
163
+ response.error_message = "No fact-checking results found for the claim"
164
+ return response.model_dump_json(indent=2)
165
+
166
+ formatted_results = []
167
+ for i, result in enumerate(results, 1):
168
+ # Determine if this is from a fact-checking site
169
+ url = result.get('href', '').lower()
170
+ is_fact_checker = any(site in url for site in ['snopes', 'factcheck', 'boatos', 'e-farsas', 'politifact'])
171
+
172
+ formatted_results.append({
173
+ "rank": i,
174
+ "title": result.get('title', 'No title'),
175
+ "snippet": result.get('body', 'No description'),
176
+ "url": result.get('href', 'No URL'),
177
+
178
+ "source": "duckduckgo",
179
+ "is_fact_checker": is_fact_checker,
180
+ "verification_type": "fact_check"
181
+ })
182
+
183
+ response.success = True
184
+ response.results_count = len(formatted_results)
185
+ response.results = formatted_results
186
+
187
+ except Exception as e:
188
+ response.error_message = f"Error searching for verification: {str(e)}"
189
+
190
+ return response.model_dump_json(indent=2)
191
+
192
+ def search_credible_sources(topic: str) -> str:
193
+ """Search for information from credible news sources. Returns JSON format."""
194
+ response = ToolResponse(
195
+ success=False,
196
+ tool_name="search_credible_sources",
197
+ query=topic,
198
+ results_count=0,
199
+ results=[],
200
+ metadata={
201
+ "search_type": "credible_sources",
202
+ "target_sites": ["g1.com.br", "folha.uol.com.br", "estadao.com.br", "bbc.com"]
203
+ }
204
+ )
205
+
206
+ try:
207
+ # Focus on credible Brazilian news sources
208
+ credible_query = f"{topic} site:g1.com.br OR site:folha.uol.com.br OR site:estadao.com.br OR site:bbc.com"
209
+
210
+ with DDGS() as ddgs:
211
+ results = list(ddgs.text(credible_query, max_results=3))
212
+
213
+ if not results:
214
+ response.error_message = f"No results from credible sources found for '{topic}'"
215
+ return response.model_dump_json(indent=2)
216
+
217
+ formatted_results = []
218
+ for i, result in enumerate(results, 1):
219
+ url = result.get('href', '').lower()
220
+
221
+ # Determine which credible source this is from
222
+ source_site = "unknown"
223
+ for site in ["g1.com.br", "folha.uol.com.br", "estadao.com.br", "bbc.com"]:
224
+ if site in url:
225
+ source_site = site
226
+ break
227
+
228
+ formatted_results.append({
229
+ "rank": i,
230
+ "title": result.get('title', 'No title'),
231
+ "snippet": result.get('body', 'No description'),
232
+ "url": result.get('href', 'No URL'),
233
+
234
+ "source": "duckduckgo",
235
+ "news_source": source_site,
236
+ "is_credible": True,
237
+ "verification_type": "credible_news"
238
+ })
239
+
240
+ response.success = True
241
+ response.results_count = len(formatted_results)
242
+ response.results = formatted_results
243
+
244
+ except Exception as e:
245
+ response.error_message = f"Error searching credible sources: {str(e)}"
246
+
247
+ return response.model_dump_json(indent=2)
248
+
249
+
250
+ def fetch_url_content(url: str, timeout: int = 10) -> str:
251
+ """
252
+ Fetches the content of a given URL. Returns JSON format with status and content.
253
+ """
254
+ response = ToolResponse(
255
+ success=False,
256
+ tool_name="fetch_url_content",
257
+ query=url,
258
+ results_count=0,
259
+ results=[],
260
+ metadata={"timeout": timeout}
261
+ )
262
+ try:
263
+ r = requests.get(url, timeout=timeout)
264
+ r.raise_for_status()
265
+ # Limit content size for safety
266
+ content = r.text[:5000]
267
+ response.success = True
268
+ response.results_count = 1
269
+ response.results = [{
270
+ "url": url,
271
+ "content": content,
272
+ "status_code": r.status_code
273
+ }]
274
+ except Exception as e:
275
+ response.error_message = f"Error fetching URL '{url}': {str(e)}"
276
+ return response.model_dump_json(indent=2)
277
+
278
+ # Configurar um secret OPENAI_API_KEY com a api_key
279
+ # On Hugging Face Spaces, the API key will be available as an environment variable
280
+ OPENAI_API_KEY = os.getenv('OPENAI_API_KEY')
281
+
282
+ if not OPENAI_API_KEY:
283
+ # Fallback for local development if not using secrets
284
+ try:
285
+ OPENAI_API_KEY = userdata.get('OPENAI_API_KEY')
286
+ except:
287
+ print("Warning: OPENAI_API_KEY not found in environment variables or Colab secrets.")
288
+ print("Please set the OPENAI_API_KEY environment variable.")
289
+
290
+
291
+ lm = dspy.LM('openai/gpt-4o-mini', api_key=OPENAI_API_KEY)
292
+ dspy.configure(lm=lm)
293
+
294
+ class DSPySigFakeNews(dspy.Signature):
295
+ f"""Você é um agente especializado em detectar notícias falsas. Use as ferramentas disponíveis para verificar a veracidade de todo o conteúdo fornecido.
296
+ Em conteúdos falsos, geralmente alguns fatos são verdadeiros, então muita atenção na classificação que deve ser de toda a notícia.
297
+ Interaja com todas as ferramentas fornecidas para obter uma variedade de fontes.
298
+ Itere diversas vezes, caso julgue necessário, combinando informações de diferentes ferramentas para formar uma análise abrangente.
299
+ Analise passo a passso o que o conteúdo está noticiando. O resultado final deve ser um detalhado processo de investigação e checagem.
300
+ Faça referências ao longo do texto também. E ao final relacione todas as referências novamente.
301
+ SEMPRE gere uma análise com referências/links de todas as evidências utilizadas para chegar no veredito final.
302
+ A análise deve ser um texto explicando detalhadamente o processo de investigação com as fontes utilizadas. Foque em um consumidor que quer entender tudo que está envolvido no conteúdo com o máximo de detalhes e quer reproduzir a investigação. Não poupe palavras.
303
+ Trace uma linha do tempo dos fatos também.
304
+ Sempre que considerar que a análise e a classificação estão se repetindo, finalize a investigação. Caso contrário sinalize para continuar investigando.
305
+ Na reflexão sempre informe um indice_confianca entre 0 e 1, sobre a confiança sobre a classificação. Queremos um índice de confiança acima de {threshold_perc_confianca}. Se estiver abaixo, continue investigando.
306
+ É PRECISO MÁXIMA ACURÁCIA! Muito cuidado.
307
+ """
308
+ news_content: str = dspy.InputField()
309
+ analise: str = dspy.OutputField(desc=("Análise final detalhada sobre a veracidade da notícia, incluindo evidências e referências. Não esqueça de incluir as referências para consulta nas analises. Em formato markdown. Faça referências ao longo do texto também. E ao final relacione todas as referências novamente."))
310
+ indice_confianca: float = dspy.OutputField(desc=(f"Índice de confiança sobre a classificação de 0 a 1. Queremos um indice_confianca acima de {threshold_perc_confianca}. Se estiver abaixo, continue investigando."))
311
+ conteudo_verdadeiro: bool = dspy.OutputField(
312
+ desc=("Diga se o conteúdo é verdadeiro. True se for verdadeiro e False se for falso/fake")
313
+ )
314
+
315
+
316
+ fake_news_agent = dspy.ReAct(
317
+ DSPySigFakeNews,
318
+ tools=[search_credible_sources, search_wikipedia, fetch_url_content],
319
+ max_iters=20 # Quantidade máxima de iterações do agente
320
+ )
321
+
322
+ def predict_fake_news(news_content: str):
323
+ """
324
+ Predicts fake news using the fake_news_agent.
325
+
326
+ Args:
327
+ news_content: The content of the news to analyze.
328
+
329
+ Returns:
330
+ A tuple containing the analysis, confidence score, and classification as text.
331
+ """
332
+ if not OPENAI_API_KEY:
333
+ return "Error: OPENAI_API_KEY not set.", 0.0, "N/A"
334
+
335
+ result = fake_news_agent(news_content=news_content)
336
+ analise = result.analise
337
+ indice_confianca = result.indice_confianca
338
+ conteudo_verdadeiro_text = "Verdadeiro" if result.conteudo_verdadeiro else "Falso"
339
+ return analise, indice_confianca, conteudo_verdadeiro_text
340
+
341
+ iface = gr.Interface(
342
+ fn=predict_fake_news,
343
+ inputs=gr.Textbox(label="Conteúdo da Notícia", lines=10),
344
+ outputs=[
345
+ gr.Markdown(label="Análise"),
346
+ gr.Number(label="Índice de Confiança"),
347
+ gr.Textbox(label="Conteúdo Verdadeiro?")
348
+ ],
349
+ title="Detector de Fake News - Veritas"
350
+ )
351
+
352
+ if __name__ == "__main__":
353
+ iface.launch()
requirements.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ dspy
2
+ ddgs
3
+ wikipedia
4
+ requests
5
+ gradio
6
+ openai
7
+ pydantic