Madras1 commited on
Commit
270c1c7
·
verified ·
1 Parent(s): cf48f8c

Upload 63 files

Browse files
app/api/deps.py ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ API dependencies.
3
+ """
4
+ from typing import Generator, Optional
5
+
6
+ from fastapi import Cookie, Header
7
+ from sqlalchemy.orm import Session
8
+
9
+ from app.core.database import get_db_for_session, get_default_session
10
+
11
+
12
+ def get_session_id(
13
+ x_session_id: Optional[str] = Header(None),
14
+ numidium_session: Optional[str] = Cookie(None)
15
+ ) -> Optional[str]:
16
+ """Return the session id from header or cookie."""
17
+ return x_session_id or numidium_session
18
+
19
+
20
+ def get_scoped_db(
21
+ x_session_id: Optional[str] = Header(None),
22
+ numidium_session: Optional[str] = Cookie(None)
23
+ ) -> Generator[Session, None, None]:
24
+ """
25
+ Provide a session-scoped DB if available, otherwise the default DB.
26
+ """
27
+ session_id = x_session_id or numidium_session
28
+ if session_id:
29
+ db = get_db_for_session(session_id)
30
+ else:
31
+ db = get_default_session()
32
+ try:
33
+ yield db
34
+ finally:
35
+ db.close()
app/api/routes/analyze.py CHANGED
@@ -1,17 +1,17 @@
1
  """
2
  Analyze API Routes - LLM-based text analysis
3
  """
4
- from fastapi import APIRouter, HTTPException
5
- from pydantic import BaseModel, Field
6
- from typing import Optional, List
7
- from sqlalchemy.orm import Session
8
- import traceback
9
-
10
- from app.core.database import get_db
11
- from app.services.nlp import entity_extractor
12
- from app.services.geocoding import geocode
13
- from app.models.entity import Entity, Relationship, Event
14
- from app.config import settings
15
 
16
 
17
  router = APIRouter(prefix="/analyze", tags=["Analysis"])
@@ -62,8 +62,8 @@ class AnalyzeResponse(BaseModel):
62
  stats: dict
63
 
64
 
65
- @router.post("", response_model=AnalyzeResponse)
66
- async def analyze_text(request: AnalyzeRequest):
67
  """
68
  Analyze text using LLM to extract entities, relationships, and events.
69
 
@@ -89,9 +89,7 @@ async def analyze_text(request: AnalyzeRequest):
89
  created_relationships = 0
90
  created_events = 0
91
 
92
- db = next(get_db())
93
-
94
- # Helper function to parse date strings
95
  def parse_date(date_str):
96
  if not date_str:
97
  return None
 
1
  """
2
  Analyze API Routes - LLM-based text analysis
3
  """
4
+ from fastapi import APIRouter, Depends, HTTPException
5
+ from pydantic import BaseModel, Field
6
+ from typing import Optional, List
7
+ from sqlalchemy.orm import Session
8
+ import traceback
9
+
10
+ from app.api.deps import get_scoped_db
11
+ from app.services.nlp import entity_extractor
12
+ from app.services.geocoding import geocode
13
+ from app.models.entity import Entity, Relationship, Event
14
+ from app.config import settings
15
 
16
 
17
  router = APIRouter(prefix="/analyze", tags=["Analysis"])
 
62
  stats: dict
63
 
64
 
65
+ @router.post("", response_model=AnalyzeResponse)
66
+ async def analyze_text(request: AnalyzeRequest, db: Session = Depends(get_scoped_db)):
67
  """
68
  Analyze text using LLM to extract entities, relationships, and events.
69
 
 
89
  created_relationships = 0
90
  created_events = 0
91
 
92
+ # Helper function to parse date strings
 
 
93
  def parse_date(date_str):
94
  if not date_str:
95
  return None
app/api/routes/chat.py CHANGED
@@ -1,58 +1,63 @@
1
- """
2
- Chat API Routes - Intelligent chat with RAG
3
- """
4
- from fastapi import APIRouter, Depends, HTTPException
5
- from pydantic import BaseModel, Field
6
- from typing import Optional
7
- from sqlalchemy.orm import Session
8
-
9
- from app.core.database import get_db
10
- from app.services.chat import chat_service
11
-
12
-
13
- router = APIRouter(prefix="/chat", tags=["Chat"])
14
-
15
-
16
- class ChatRequest(BaseModel):
17
- """Chat request model"""
18
- message: str = Field(..., min_length=1, description="User message")
19
- use_web: bool = Field(default=True, description="Include web search")
20
- use_history: bool = Field(default=True, description="Use conversation history")
21
-
22
-
23
- class ChatResponse(BaseModel):
24
- """Chat response model"""
25
- answer: str
26
- local_context_used: bool
27
- web_context_used: bool
28
- entities_found: int
29
-
30
-
31
- @router.post("", response_model=ChatResponse)
32
- async def chat(request: ChatRequest, db: Session = Depends(get_db)):
33
- """
34
- Send a message and get an intelligent response.
35
-
36
- Uses:
37
- - Local NUMIDIUM knowledge (entities/relationships)
38
- - Lancer web search (if enabled)
39
- - Cerebras LLM for synthesis
40
- """
41
- try:
42
- result = await chat_service.chat(
43
- message=request.message,
44
- db=db,
45
- use_web=request.use_web,
46
- use_history=request.use_history
47
- )
48
- return ChatResponse(**result)
49
-
50
- except Exception as e:
51
- raise HTTPException(status_code=500, detail=str(e))
52
-
53
-
54
- @router.post("/clear")
55
- async def clear_history():
56
- """Clear conversation history"""
57
- chat_service.clear_history()
58
- return {"message": "Histórico limpo"}
 
 
 
 
 
 
1
+ """
2
+ Chat API Routes - Intelligent chat with RAG
3
+ """
4
+ from fastapi import APIRouter, Depends, HTTPException
5
+ from pydantic import BaseModel, Field
6
+ from typing import Optional
7
+ from sqlalchemy.orm import Session
8
+
9
+ from app.api.deps import get_scoped_db, get_session_id
10
+ from app.services.chat import chat_service
11
+
12
+
13
+ router = APIRouter(prefix="/chat", tags=["Chat"])
14
+
15
+
16
+ class ChatRequest(BaseModel):
17
+ """Chat request model"""
18
+ message: str = Field(..., min_length=1, description="User message")
19
+ use_web: bool = Field(default=True, description="Include web search")
20
+ use_history: bool = Field(default=True, description="Use conversation history")
21
+
22
+
23
+ class ChatResponse(BaseModel):
24
+ """Chat response model"""
25
+ answer: str
26
+ local_context_used: bool
27
+ web_context_used: bool
28
+ entities_found: int
29
+
30
+
31
+ @router.post("", response_model=ChatResponse)
32
+ async def chat(
33
+ request: ChatRequest,
34
+ db: Session = Depends(get_scoped_db),
35
+ session_id: Optional[str] = Depends(get_session_id)
36
+ ):
37
+ """
38
+ Send a message and get an intelligent response.
39
+
40
+ Uses:
41
+ - Local NUMIDIUM knowledge (entities/relationships)
42
+ - Lancer web search (if enabled)
43
+ - Cerebras LLM for synthesis
44
+ """
45
+ try:
46
+ result = await chat_service.chat(
47
+ message=request.message,
48
+ db=db,
49
+ use_web=request.use_web,
50
+ use_history=request.use_history,
51
+ session_id=session_id
52
+ )
53
+ return ChatResponse(**result)
54
+
55
+ except Exception as e:
56
+ raise HTTPException(status_code=500, detail=str(e))
57
+
58
+
59
+ @router.post("/clear")
60
+ async def clear_history(session_id: Optional[str] = Depends(get_session_id)):
61
+ """Clear conversation history"""
62
+ chat_service.clear_history(session_id=session_id)
63
+ return {"message": "Historico limpo"}
app/api/routes/entities.py CHANGED
@@ -1,77 +1,57 @@
1
- """
2
- Entity CRUD Routes
3
- """
4
- from fastapi import APIRouter, Depends, HTTPException, Query, Header, Cookie
5
- from sqlalchemy.orm import Session
6
- from sqlalchemy import or_
7
- from typing import List, Optional
8
-
9
- from app.core.database import get_db, get_db_for_session
10
- from app.models import Entity, Relationship
11
- from app.schemas import EntityCreate, EntityUpdate, EntityResponse, GraphData, GraphNode, GraphEdge
12
-
13
- router = APIRouter(prefix="/entities", tags=["Entities"])
14
-
15
-
16
- def get_session_or_default(
17
- x_session_id: Optional[str] = Header(None),
18
- numidium_session: Optional[str] = Cookie(None)
19
- ) -> Optional[str]:
20
- """Get session ID from header or cookie"""
21
- return x_session_id or numidium_session
22
-
23
-
24
- @router.get("", response_model=List[EntityResponse])
25
- def list_entities(
26
- type: Optional[str] = None,
27
- search: Optional[str] = None,
28
- project_id: Optional[str] = None,
29
- limit: int = Query(default=50, le=200),
30
- offset: int = 0,
31
- x_session_id: Optional[str] = Header(None),
32
- numidium_session: Optional[str] = Cookie(None)
33
- ):
34
- """Lista todas as entidades com filtros opcionais"""
35
- session_id = x_session_id or numidium_session
36
-
37
- # Use session-specific database if session exists
38
- if session_id:
39
- db = get_db_for_session(session_id)
40
- else:
41
- db = next(get_db())
42
-
43
- try:
44
- query = db.query(Entity)
45
-
46
- if project_id:
47
- query = query.filter(Entity.project_id == project_id)
48
-
49
- if type:
50
- query = query.filter(Entity.type == type)
51
-
52
- if search:
53
- query = query.filter(
54
- or_(
55
- Entity.name.ilike(f"%{search}%"),
56
- Entity.description.ilike(f"%{search}%")
57
- )
58
- )
59
-
60
- query = query.order_by(Entity.created_at.desc())
61
- return query.offset(offset).limit(limit).all()
62
- finally:
63
- db.close()
64
 
65
 
66
- @router.get("/types")
67
- def get_entity_types(db: Session = Depends(get_db)):
68
  """Retorna todos os tipos de entidade únicos"""
69
  types = db.query(Entity.type).distinct().all()
70
  return [t[0] for t in types]
71
 
72
 
73
- @router.get("/{entity_id}", response_model=EntityResponse)
74
- def get_entity(entity_id: str, db: Session = Depends(get_db)):
75
  """Busca uma entidade por ID"""
76
  entity = db.query(Entity).filter(Entity.id == entity_id).first()
77
  if not entity:
@@ -79,8 +59,8 @@ def get_entity(entity_id: str, db: Session = Depends(get_db)):
79
  return entity
80
 
81
 
82
- @router.post("", response_model=EntityResponse, status_code=201)
83
- def create_entity(entity: EntityCreate, db: Session = Depends(get_db)):
84
  """Cria uma nova entidade"""
85
  db_entity = Entity(**entity.model_dump())
86
  db.add(db_entity)
@@ -89,8 +69,8 @@ def create_entity(entity: EntityCreate, db: Session = Depends(get_db)):
89
  return db_entity
90
 
91
 
92
- @router.put("/{entity_id}", response_model=EntityResponse)
93
- def update_entity(entity_id: str, entity: EntityUpdate, db: Session = Depends(get_db)):
94
  """Atualiza uma entidade existente"""
95
  db_entity = db.query(Entity).filter(Entity.id == entity_id).first()
96
  if not db_entity:
@@ -105,8 +85,8 @@ def update_entity(entity_id: str, entity: EntityUpdate, db: Session = Depends(ge
105
  return db_entity
106
 
107
 
108
- @router.delete("/{entity_id}")
109
- def delete_entity(entity_id: str, db: Session = Depends(get_db)):
110
  """Deleta uma entidade"""
111
  db_entity = db.query(Entity).filter(Entity.id == entity_id).first()
112
  if not db_entity:
@@ -125,12 +105,12 @@ def delete_entity(entity_id: str, db: Session = Depends(get_db)):
125
  return {"message": "Entity deleted"}
126
 
127
 
128
- @router.get("/{entity_id}/connections", response_model=GraphData)
129
- def get_entity_connections(
130
- entity_id: str,
131
- depth: int = Query(default=1, le=3),
132
- db: Session = Depends(get_db)
133
- ):
134
  """
135
  Retorna o grafo de conexões de uma entidade
136
  Usado para visualização de rede no frontend
@@ -187,12 +167,12 @@ def get_entity_connections(
187
  )
188
 
189
 
190
- @router.post("/merge")
191
- def merge_entities(
192
- primary_id: str,
193
- secondary_id: str,
194
- db: Session = Depends(get_db)
195
- ):
196
  """
197
  Merge two entities into one.
198
  The primary entity is kept, the secondary is deleted.
@@ -267,11 +247,11 @@ def merge_entities(
267
  }
268
 
269
 
270
- @router.get("/suggest-merge")
271
- async def suggest_merge_candidates(
272
- limit: int = Query(default=10, le=50),
273
- db: Session = Depends(get_db)
274
- ):
275
  """
276
  Use LLM to find potential duplicate entities that could be merged.
277
  Returns pairs of entities that might be the same.
 
1
+ """
2
+ Entity CRUD Routes
3
+ """
4
+ from fastapi import APIRouter, Depends, HTTPException, Query
5
+ from sqlalchemy.orm import Session
6
+ from sqlalchemy import or_
7
+ from typing import List, Optional
8
+
9
+ from app.api.deps import get_scoped_db
10
+ from app.models import Entity, Relationship
11
+ from app.schemas import EntityCreate, EntityUpdate, EntityResponse, GraphData, GraphNode, GraphEdge
12
+
13
+ router = APIRouter(prefix="/entities", tags=["Entities"])
14
+
15
+
16
+ @router.get("", response_model=List[EntityResponse])
17
+ def list_entities(
18
+ type: Optional[str] = None,
19
+ search: Optional[str] = None,
20
+ project_id: Optional[str] = None,
21
+ limit: int = Query(default=50, le=200),
22
+ offset: int = 0,
23
+ db: Session = Depends(get_scoped_db)
24
+ ):
25
+ """Lista todas as entidades com filtros opcionais"""
26
+ query = db.query(Entity)
27
+
28
+ if project_id:
29
+ query = query.filter(Entity.project_id == project_id)
30
+
31
+ if type:
32
+ query = query.filter(Entity.type == type)
33
+
34
+ if search:
35
+ query = query.filter(
36
+ or_(
37
+ Entity.name.ilike(f"%{search}%"),
38
+ Entity.description.ilike(f"%{search}%")
39
+ )
40
+ )
41
+
42
+ query = query.order_by(Entity.created_at.desc())
43
+ return query.offset(offset).limit(limit).all()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
 
45
 
46
+ @router.get("/types")
47
+ def get_entity_types(db: Session = Depends(get_scoped_db)):
48
  """Retorna todos os tipos de entidade únicos"""
49
  types = db.query(Entity.type).distinct().all()
50
  return [t[0] for t in types]
51
 
52
 
53
+ @router.get("/{entity_id}", response_model=EntityResponse)
54
+ def get_entity(entity_id: str, db: Session = Depends(get_scoped_db)):
55
  """Busca uma entidade por ID"""
56
  entity = db.query(Entity).filter(Entity.id == entity_id).first()
57
  if not entity:
 
59
  return entity
60
 
61
 
62
+ @router.post("", response_model=EntityResponse, status_code=201)
63
+ def create_entity(entity: EntityCreate, db: Session = Depends(get_scoped_db)):
64
  """Cria uma nova entidade"""
65
  db_entity = Entity(**entity.model_dump())
66
  db.add(db_entity)
 
69
  return db_entity
70
 
71
 
72
+ @router.put("/{entity_id}", response_model=EntityResponse)
73
+ def update_entity(entity_id: str, entity: EntityUpdate, db: Session = Depends(get_scoped_db)):
74
  """Atualiza uma entidade existente"""
75
  db_entity = db.query(Entity).filter(Entity.id == entity_id).first()
76
  if not db_entity:
 
85
  return db_entity
86
 
87
 
88
+ @router.delete("/{entity_id}")
89
+ def delete_entity(entity_id: str, db: Session = Depends(get_scoped_db)):
90
  """Deleta uma entidade"""
91
  db_entity = db.query(Entity).filter(Entity.id == entity_id).first()
92
  if not db_entity:
 
105
  return {"message": "Entity deleted"}
106
 
107
 
108
+ @router.get("/{entity_id}/connections", response_model=GraphData)
109
+ def get_entity_connections(
110
+ entity_id: str,
111
+ depth: int = Query(default=1, le=3),
112
+ db: Session = Depends(get_scoped_db)
113
+ ):
114
  """
115
  Retorna o grafo de conexões de uma entidade
116
  Usado para visualização de rede no frontend
 
167
  )
168
 
169
 
170
+ @router.post("/merge")
171
+ def merge_entities(
172
+ primary_id: str,
173
+ secondary_id: str,
174
+ db: Session = Depends(get_scoped_db)
175
+ ):
176
  """
177
  Merge two entities into one.
178
  The primary entity is kept, the secondary is deleted.
 
247
  }
248
 
249
 
250
+ @router.get("/suggest-merge")
251
+ async def suggest_merge_candidates(
252
+ limit: int = Query(default=10, le=50),
253
+ db: Session = Depends(get_scoped_db)
254
+ ):
255
  """
256
  Use LLM to find potential duplicate entities that could be merged.
257
  Returns pairs of entities that might be the same.
app/api/routes/events.py CHANGED
@@ -1,116 +1,113 @@
1
- """
2
- Events CRUD Routes
3
- """
4
- from fastapi import APIRouter, Depends, HTTPException, Query
5
- from sqlalchemy.orm import Session
6
- from sqlalchemy import or_
7
- from typing import List, Optional
8
- from datetime import datetime
9
-
10
- from app.core.database import get_db
11
- from app.models import Event
12
- from app.schemas import EventCreate, EventResponse
13
-
14
- router = APIRouter(prefix="/events", tags=["Events"])
15
-
16
-
17
- @router.get("/", response_model=List[EventResponse])
18
- def list_events(
19
- type: Optional[str] = None,
20
- search: Optional[str] = None,
21
- start_date: Optional[datetime] = None,
22
- end_date: Optional[datetime] = None,
23
- limit: int = Query(default=50, le=200),
24
- offset: int = 0,
25
- db: Session = Depends(get_db)
26
- ):
27
- """Lista eventos com filtros opcionais"""
28
- query = db.query(Event)
29
-
30
- if type:
31
- query = query.filter(Event.type == type)
32
-
33
- if search:
34
- query = query.filter(
35
- or_(
36
- Event.title.ilike(f"%{search}%"),
37
- Event.description.ilike(f"%{search}%")
38
- )
39
- )
40
-
41
- if start_date:
42
- query = query.filter(Event.event_date >= start_date)
43
- if end_date:
44
- query = query.filter(Event.event_date <= end_date)
45
-
46
- query = query.order_by(Event.event_date.desc().nullslast())
47
- return query.offset(offset).limit(limit).all()
48
-
49
-
50
- @router.get("/types")
51
- def get_event_types(db: Session = Depends(get_db)):
52
- """Retorna todos os tipos de evento únicos"""
53
- types = db.query(Event.type).distinct().all()
54
- return [t[0] for t in types]
55
-
56
-
57
- @router.get("/timeline")
58
- def get_timeline(
59
- entity_id: Optional[str] = None,
60
- limit: int = Query(default=50, le=200),
61
- db: Session = Depends(get_db)
62
- ):
63
- """
64
- Retorna eventos em formato timeline
65
- Útil para visualização no frontend
66
- """
67
- query = db.query(Event).filter(Event.event_date.isnot(None))
68
-
69
- if entity_id:
70
- # Filter events that include this entity
71
- # Note: This is a simple implementation; for production, use a proper JSON query
72
- query = query.filter(Event.entity_ids.contains([entity_id]))
73
-
74
- events = query.order_by(Event.event_date.asc()).limit(limit).all()
75
-
76
- return [
77
- {
78
- "id": e.id,
79
- "title": e.title,
80
- "date": e.event_date.isoformat() if e.event_date else None,
81
- "type": e.type,
82
- "location": e.location_name
83
- }
84
- for e in events
85
- ]
86
-
87
-
88
- @router.get("/{event_id}", response_model=EventResponse)
89
- def get_event(event_id: str, db: Session = Depends(get_db)):
90
- """Busca um evento por ID"""
91
- event = db.query(Event).filter(Event.id == event_id).first()
92
- if not event:
93
- raise HTTPException(status_code=404, detail="Event not found")
94
- return event
95
-
96
-
97
- @router.post("/", response_model=EventResponse, status_code=201)
98
- def create_event(event: EventCreate, db: Session = Depends(get_db)):
99
- """Cria um novo evento"""
100
- db_event = Event(**event.model_dump())
101
- db.add(db_event)
102
- db.commit()
103
- db.refresh(db_event)
104
- return db_event
105
-
106
-
107
- @router.delete("/{event_id}")
108
- def delete_event(event_id: str, db: Session = Depends(get_db)):
109
- """Deleta um evento"""
110
- db_event = db.query(Event).filter(Event.id == event_id).first()
111
- if not db_event:
112
- raise HTTPException(status_code=404, detail="Event not found")
113
-
114
- db.delete(db_event)
115
- db.commit()
116
- return {"message": "Event deleted"}
 
1
+ """
2
+ Events CRUD Routes
3
+ """
4
+ from fastapi import APIRouter, Depends, HTTPException, Query
5
+ from sqlalchemy.orm import Session
6
+ from sqlalchemy import or_
7
+ from typing import List, Optional
8
+ from datetime import datetime
9
+
10
+ from app.api.deps import get_scoped_db
11
+ from app.models import Event
12
+ from app.schemas import EventCreate, EventResponse
13
+
14
+ router = APIRouter(prefix="/events", tags=["Events"])
15
+
16
+
17
+ @router.get("/", response_model=List[EventResponse])
18
+ def list_events(
19
+ type: Optional[str] = None,
20
+ search: Optional[str] = None,
21
+ start_date: Optional[datetime] = None,
22
+ end_date: Optional[datetime] = None,
23
+ limit: int = Query(default=50, le=200),
24
+ offset: int = 0,
25
+ db: Session = Depends(get_scoped_db)
26
+ ):
27
+ """Lista eventos com filtros opcionais"""
28
+ query = db.query(Event)
29
+
30
+ if type:
31
+ query = query.filter(Event.type == type)
32
+
33
+ if search:
34
+ query = query.filter(
35
+ or_(
36
+ Event.title.ilike(f"%{search}%"),
37
+ Event.description.ilike(f"%{search}%")
38
+ )
39
+ )
40
+
41
+ if start_date:
42
+ query = query.filter(Event.event_date >= start_date)
43
+ if end_date:
44
+ query = query.filter(Event.event_date <= end_date)
45
+
46
+ query = query.order_by(Event.event_date.desc().nullslast())
47
+ return query.offset(offset).limit(limit).all()
48
+
49
+
50
+ @router.get("/types")
51
+ def get_event_types(db: Session = Depends(get_scoped_db)):
52
+ """Retorna todos os tipos de evento unicos"""
53
+ types = db.query(Event.type).distinct().all()
54
+ return [t[0] for t in types]
55
+
56
+
57
+ @router.get("/timeline")
58
+ def get_timeline(
59
+ entity_id: Optional[str] = None,
60
+ limit: int = Query(default=50, le=200),
61
+ db: Session = Depends(get_scoped_db)
62
+ ):
63
+ """
64
+ Retorna eventos em formato timeline.
65
+ """
66
+ query = db.query(Event).filter(Event.event_date.isnot(None))
67
+
68
+ if entity_id:
69
+ query = query.filter(Event.entity_ids.contains([entity_id]))
70
+
71
+ events = query.order_by(Event.event_date.asc()).limit(limit).all()
72
+
73
+ return [
74
+ {
75
+ "id": e.id,
76
+ "title": e.title,
77
+ "date": e.event_date.isoformat() if e.event_date else None,
78
+ "type": e.type,
79
+ "location": e.location_name
80
+ }
81
+ for e in events
82
+ ]
83
+
84
+
85
+ @router.get("/{event_id}", response_model=EventResponse)
86
+ def get_event(event_id: str, db: Session = Depends(get_scoped_db)):
87
+ """Busca um evento por ID"""
88
+ event = db.query(Event).filter(Event.id == event_id).first()
89
+ if not event:
90
+ raise HTTPException(status_code=404, detail="Event not found")
91
+ return event
92
+
93
+
94
+ @router.post("/", response_model=EventResponse, status_code=201)
95
+ def create_event(event: EventCreate, db: Session = Depends(get_scoped_db)):
96
+ """Cria um novo evento"""
97
+ db_event = Event(**event.model_dump())
98
+ db.add(db_event)
99
+ db.commit()
100
+ db.refresh(db_event)
101
+ return db_event
102
+
103
+
104
+ @router.delete("/{event_id}")
105
+ def delete_event(event_id: str, db: Session = Depends(get_scoped_db)):
106
+ """Deleta um evento"""
107
+ db_event = db.query(Event).filter(Event.id == event_id).first()
108
+ if not db_event:
109
+ raise HTTPException(status_code=404, detail="Event not found")
110
+
111
+ db.delete(db_event)
112
+ db.commit()
113
+ return {"message": "Event deleted"}
 
 
 
app/api/routes/graph.py CHANGED
@@ -1,29 +1,29 @@
1
  """
2
  Graph API Routes - Network visualization endpoints
3
  """
4
- from fastapi import APIRouter, HTTPException, Query
5
- from typing import Optional, List
6
- from sqlalchemy import or_
7
-
8
- from app.core.database import get_db
9
- from app.models.entity import Entity, Relationship
10
-
11
-
12
- router = APIRouter(prefix="/graph", tags=["Graph"])
 
13
 
14
 
15
  @router.get("")
16
- async def get_graph(
17
- entity_type: Optional[str] = Query(None, description="Filter by entity type"),
18
- limit: int = Query(100, le=500, description="Maximum number of entities")
19
- ):
 
20
  """
21
  Get graph data for visualization.
22
  Returns nodes (entities) and edges (relationships).
23
  """
24
- try:
25
- db = next(get_db())
26
-
27
  # Get entities
28
  query = db.query(Entity)
29
  if entity_type:
@@ -56,7 +56,6 @@ async def get_graph(
56
 
57
  edges = []
58
  for r in relationships:
59
- # Only include edges where both nodes exist
60
  if r.source_id in entity_ids and r.target_id in entity_ids:
61
  edges.append({
62
  "data": {
@@ -82,16 +81,15 @@ async def get_graph(
82
 
83
 
84
  @router.get("/entity/{entity_id}")
85
- async def get_entity_graph(
86
- entity_id: str,
87
- depth: int = Query(1, ge=1, le=3, description="How many levels of connections to include")
88
- ):
 
89
  """
90
  Get graph centered on a specific entity.
91
  """
92
- try:
93
- db = next(get_db())
94
-
95
  # Get the central entity
96
  central = db.query(Entity).filter(Entity.id == entity_id).first()
97
  if not central:
@@ -102,7 +100,6 @@ async def get_entity_graph(
102
  current_level = {entity_id}
103
 
104
  for _ in range(depth):
105
- # Get relationships for current level entities
106
  rels = db.query(Relationship).filter(
107
  or_(
108
  Relationship.source_id.in_(current_level),
@@ -110,13 +107,11 @@ async def get_entity_graph(
110
  )
111
  ).all()
112
 
113
- # Collect new entity IDs
114
  next_level = set()
115
  for r in rels:
116
  next_level.add(r.source_id)
117
  next_level.add(r.target_id)
118
 
119
- # Update for next iteration
120
  current_level = next_level - collected_ids
121
  collected_ids.update(next_level)
122
 
@@ -175,3 +170,4 @@ async def get_entity_graph(
175
  raise
176
  except Exception as e:
177
  raise HTTPException(status_code=500, detail=f"Failed to get entity graph: {str(e)}")
 
 
1
  """
2
  Graph API Routes - Network visualization endpoints
3
  """
4
+ from fastapi import APIRouter, Depends, HTTPException, Query
5
+ from typing import Optional, List
6
+ from sqlalchemy.orm import Session
7
+ from sqlalchemy import or_
8
+
9
+ from app.api.deps import get_scoped_db
10
+ from app.models.entity import Entity, Relationship
11
+
12
+
13
+ router = APIRouter(prefix="/graph", tags=["Graph"])
14
 
15
 
16
  @router.get("")
17
+ async def get_graph(
18
+ entity_type: Optional[str] = Query(None, description="Filter by entity type"),
19
+ limit: int = Query(100, le=500, description="Maximum number of entities"),
20
+ db: Session = Depends(get_scoped_db)
21
+ ):
22
  """
23
  Get graph data for visualization.
24
  Returns nodes (entities) and edges (relationships).
25
  """
26
+ try:
 
 
27
  # Get entities
28
  query = db.query(Entity)
29
  if entity_type:
 
56
 
57
  edges = []
58
  for r in relationships:
 
59
  if r.source_id in entity_ids and r.target_id in entity_ids:
60
  edges.append({
61
  "data": {
 
81
 
82
 
83
  @router.get("/entity/{entity_id}")
84
+ async def get_entity_graph(
85
+ entity_id: str,
86
+ depth: int = Query(1, ge=1, le=3, description="How many levels of connections to include"),
87
+ db: Session = Depends(get_scoped_db)
88
+ ):
89
  """
90
  Get graph centered on a specific entity.
91
  """
92
+ try:
 
 
93
  # Get the central entity
94
  central = db.query(Entity).filter(Entity.id == entity_id).first()
95
  if not central:
 
100
  current_level = {entity_id}
101
 
102
  for _ in range(depth):
 
103
  rels = db.query(Relationship).filter(
104
  or_(
105
  Relationship.source_id.in_(current_level),
 
107
  )
108
  ).all()
109
 
 
110
  next_level = set()
111
  for r in rels:
112
  next_level.add(r.source_id)
113
  next_level.add(r.target_id)
114
 
 
115
  current_level = next_level - collected_ids
116
  collected_ids.update(next_level)
117
 
 
170
  raise
171
  except Exception as e:
172
  raise HTTPException(status_code=500, detail=f"Failed to get entity graph: {str(e)}")
173
+
app/api/routes/ingest.py CHANGED
@@ -2,15 +2,15 @@
2
  Data Ingestion Routes
3
  Endpoints para importar dados de fontes externas
4
  """
5
- from fastapi import APIRouter, Depends, HTTPException, BackgroundTasks
6
- from sqlalchemy.orm import Session
7
- from typing import Optional, List
8
- from datetime import datetime
9
- import asyncio
10
-
11
- from app.core.database import get_db
12
- from app.models import Entity, Document, Relationship
13
- from app.schemas import EntityResponse, DocumentResponse
14
  from app.services.ingestion import wikipedia_scraper, news_service
15
  from app.services.nlp import entity_extractor
16
  from app.services.geocoding import geocode
@@ -43,20 +43,20 @@ def search_wikipedia(q: str, limit: int = 10):
43
 
44
 
45
  @router.post("/wikipedia/entity", response_model=EntityResponse)
46
- async def import_from_wikipedia(
47
- title: str,
48
- entity_type: str = "person",
49
- project_id: Optional[str] = None,
50
- auto_extract: bool = True,
51
- db: Session = Depends(get_db)
52
- ):
53
  """
54
  Importa uma entidade da Wikipedia
55
  entity_type: person, organization, location
56
  project_id: ID do projeto para associar a entidade
57
  auto_extract: Se True, usa LLM para extrair entidades relacionadas
58
  """
59
- # Check if entity already exists
60
  existing = db.query(Entity).filter(
61
  Entity.name == title,
62
  Entity.source == "wikipedia"
@@ -201,12 +201,12 @@ def search_news(q: str):
201
 
202
 
203
  @router.post("/news/import")
204
- async def import_news(
205
- query: Optional[str] = None,
206
- feed: Optional[str] = None,
207
- auto_extract: bool = True,
208
- db: Session = Depends(get_db)
209
- ):
210
  """
211
  Importa notícias como documentos no sistema
212
  auto_extract: Se True, usa LLM para extrair entidades de cada notícia
@@ -314,10 +314,10 @@ async def import_news(
314
  # ========== Manual Import ==========
315
 
316
  @router.post("/bulk/entities")
317
- def bulk_import_entities(
318
- entities: List[dict],
319
- db: Session = Depends(get_db)
320
- ):
321
  """
322
  Importa múltiplas entidades de uma vez
323
  Útil para importar de CSV/JSON
 
2
  Data Ingestion Routes
3
  Endpoints para importar dados de fontes externas
4
  """
5
+ from fastapi import APIRouter, Depends, HTTPException, BackgroundTasks
6
+ from sqlalchemy.orm import Session
7
+ from typing import Optional, List
8
+ from datetime import datetime
9
+ import asyncio
10
+
11
+ from app.api.deps import get_scoped_db
12
+ from app.models import Entity, Document, Relationship
13
+ from app.schemas import EntityResponse, DocumentResponse
14
  from app.services.ingestion import wikipedia_scraper, news_service
15
  from app.services.nlp import entity_extractor
16
  from app.services.geocoding import geocode
 
43
 
44
 
45
  @router.post("/wikipedia/entity", response_model=EntityResponse)
46
+ async def import_from_wikipedia(
47
+ title: str,
48
+ entity_type: str = "person",
49
+ project_id: Optional[str] = None,
50
+ auto_extract: bool = True,
51
+ db: Session = Depends(get_scoped_db)
52
+ ):
53
  """
54
  Importa uma entidade da Wikipedia
55
  entity_type: person, organization, location
56
  project_id: ID do projeto para associar a entidade
57
  auto_extract: Se True, usa LLM para extrair entidades relacionadas
58
  """
59
+ # Check if entity already exists
60
  existing = db.query(Entity).filter(
61
  Entity.name == title,
62
  Entity.source == "wikipedia"
 
201
 
202
 
203
  @router.post("/news/import")
204
+ async def import_news(
205
+ query: Optional[str] = None,
206
+ feed: Optional[str] = None,
207
+ auto_extract: bool = True,
208
+ db: Session = Depends(get_scoped_db)
209
+ ):
210
  """
211
  Importa notícias como documentos no sistema
212
  auto_extract: Se True, usa LLM para extrair entidades de cada notícia
 
314
  # ========== Manual Import ==========
315
 
316
  @router.post("/bulk/entities")
317
+ def bulk_import_entities(
318
+ entities: List[dict],
319
+ db: Session = Depends(get_scoped_db)
320
+ ):
321
  """
322
  Importa múltiplas entidades de uma vez
323
  Útil para importar de CSV/JSON
app/api/routes/projects.py CHANGED
@@ -1,13 +1,14 @@
1
  """
2
  Projects API Routes - Workspace management
3
  """
4
- from fastapi import APIRouter, HTTPException
5
- from pydantic import BaseModel
6
- from typing import Optional, List
7
- from datetime import datetime
8
-
9
- from app.core.database import get_db
10
- from app.models import Project, Entity, Relationship
 
11
 
12
 
13
  router = APIRouter(prefix="/projects", tags=["Projects"])
@@ -33,11 +34,10 @@ class ProjectResponse(BaseModel):
33
  from_attributes = True
34
 
35
 
36
- @router.get("", response_model=List[ProjectResponse])
37
- def list_projects():
38
- """List all projects"""
39
- db = next(get_db())
40
- projects = db.query(Project).order_by(Project.created_at.desc()).all()
41
 
42
  result = []
43
  for p in projects:
@@ -55,14 +55,12 @@ def list_projects():
55
  return result
56
 
57
 
58
- @router.post("", response_model=ProjectResponse)
59
- def create_project(project: ProjectCreate):
60
- """Create a new project"""
61
- db = next(get_db())
62
-
63
- new_project = Project(
64
- name=project.name,
65
- description=project.description,
66
  color=project.color,
67
  icon=project.icon
68
  )
@@ -81,11 +79,10 @@ def create_project(project: ProjectCreate):
81
  )
82
 
83
 
84
- @router.get("/{project_id}", response_model=ProjectResponse)
85
- def get_project(project_id: str):
86
- """Get project by ID"""
87
- db = next(get_db())
88
- project = db.query(Project).filter(Project.id == project_id).first()
89
 
90
  if not project:
91
  raise HTTPException(status_code=404, detail="Project not found")
@@ -103,11 +100,10 @@ def get_project(project_id: str):
103
  )
104
 
105
 
106
- @router.delete("/{project_id}")
107
- def delete_project(project_id: str):
108
- """Delete project and optionally its entities"""
109
- db = next(get_db())
110
- project = db.query(Project).filter(Project.id == project_id).first()
111
 
112
  if not project:
113
  raise HTTPException(status_code=404, detail="Project not found")
@@ -122,11 +118,10 @@ def delete_project(project_id: str):
122
  return {"message": f"Project '{project.name}' deleted"}
123
 
124
 
125
- @router.put("/{project_id}")
126
- def update_project(project_id: str, project: ProjectCreate):
127
- """Update project"""
128
- db = next(get_db())
129
- existing = db.query(Project).filter(Project.id == project_id).first()
130
 
131
  if not existing:
132
  raise HTTPException(status_code=404, detail="Project not found")
 
1
  """
2
  Projects API Routes - Workspace management
3
  """
4
+ from fastapi import APIRouter, Depends, HTTPException
5
+ from pydantic import BaseModel
6
+ from typing import Optional, List
7
+ from datetime import datetime
8
+ from sqlalchemy.orm import Session
9
+
10
+ from app.api.deps import get_scoped_db
11
+ from app.models import Project, Entity, Relationship
12
 
13
 
14
  router = APIRouter(prefix="/projects", tags=["Projects"])
 
34
  from_attributes = True
35
 
36
 
37
+ @router.get("", response_model=List[ProjectResponse])
38
+ def list_projects(db: Session = Depends(get_scoped_db)):
39
+ """List all projects"""
40
+ projects = db.query(Project).order_by(Project.created_at.desc()).all()
 
41
 
42
  result = []
43
  for p in projects:
 
55
  return result
56
 
57
 
58
+ @router.post("", response_model=ProjectResponse)
59
+ def create_project(project: ProjectCreate, db: Session = Depends(get_scoped_db)):
60
+ """Create a new project"""
61
+ new_project = Project(
62
+ name=project.name,
63
+ description=project.description,
 
 
64
  color=project.color,
65
  icon=project.icon
66
  )
 
79
  )
80
 
81
 
82
+ @router.get("/{project_id}", response_model=ProjectResponse)
83
+ def get_project(project_id: str, db: Session = Depends(get_scoped_db)):
84
+ """Get project by ID"""
85
+ project = db.query(Project).filter(Project.id == project_id).first()
 
86
 
87
  if not project:
88
  raise HTTPException(status_code=404, detail="Project not found")
 
100
  )
101
 
102
 
103
+ @router.delete("/{project_id}")
104
+ def delete_project(project_id: str, db: Session = Depends(get_scoped_db)):
105
+ """Delete project and optionally its entities"""
106
+ project = db.query(Project).filter(Project.id == project_id).first()
 
107
 
108
  if not project:
109
  raise HTTPException(status_code=404, detail="Project not found")
 
118
  return {"message": f"Project '{project.name}' deleted"}
119
 
120
 
121
+ @router.put("/{project_id}")
122
+ def update_project(project_id: str, project: ProjectCreate, db: Session = Depends(get_scoped_db)):
123
+ """Update project"""
124
+ existing = db.query(Project).filter(Project.id == project_id).first()
 
125
 
126
  if not existing:
127
  raise HTTPException(status_code=404, detail="Project not found")
app/api/routes/relationships.py CHANGED
@@ -1,71 +1,76 @@
1
- """
2
- Relationship CRUD Routes
3
- """
4
- from fastapi import APIRouter, Depends, HTTPException, Query
5
- from sqlalchemy.orm import Session
6
- from typing import List, Optional
7
-
8
- from app.core.database import get_db
9
- from app.models import Relationship, Entity
10
- from app.schemas import RelationshipCreate, RelationshipResponse
11
-
12
- router = APIRouter(prefix="/relationships", tags=["Relationships"])
13
-
14
-
15
- @router.get("/", response_model=List[RelationshipResponse])
16
- def list_relationships(
17
- type: Optional[str] = None,
18
- source_id: Optional[str] = None,
19
- target_id: Optional[str] = None,
20
- limit: int = Query(default=50, le=200),
21
- db: Session = Depends(get_db)
22
- ):
23
- """Lista relacionamentos com filtros opcionais"""
24
- query = db.query(Relationship)
25
-
26
- if type:
27
- query = query.filter(Relationship.type == type)
28
- if source_id:
29
- query = query.filter(Relationship.source_id == source_id)
30
- if target_id:
31
- query = query.filter(Relationship.target_id == target_id)
32
-
33
- return query.limit(limit).all()
34
-
35
-
36
- @router.get("/types")
37
- def get_relationship_types(db: Session = Depends(get_db)):
38
- """Retorna todos os tipos de relacionamento únicos"""
39
- types = db.query(Relationship.type).distinct().all()
40
- return [t[0] for t in types]
41
-
42
-
43
- @router.post("/", response_model=RelationshipResponse, status_code=201)
44
- def create_relationship(rel: RelationshipCreate, db: Session = Depends(get_db)):
45
- """Cria um novo relacionamento entre entidades"""
46
- # Verify both entities exist
47
- source = db.query(Entity).filter(Entity.id == rel.source_id).first()
48
- target = db.query(Entity).filter(Entity.id == rel.target_id).first()
49
-
50
- if not source:
51
- raise HTTPException(status_code=404, detail="Source entity not found")
52
- if not target:
53
- raise HTTPException(status_code=404, detail="Target entity not found")
54
-
55
- db_rel = Relationship(**rel.model_dump())
56
- db.add(db_rel)
57
- db.commit()
58
- db.refresh(db_rel)
59
- return db_rel
60
-
61
-
62
- @router.delete("/{relationship_id}")
63
- def delete_relationship(relationship_id: str, db: Session = Depends(get_db)):
64
- """Deleta um relacionamento"""
65
- db_rel = db.query(Relationship).filter(Relationship.id == relationship_id).first()
66
- if not db_rel:
67
- raise HTTPException(status_code=404, detail="Relationship not found")
68
-
69
- db.delete(db_rel)
70
- db.commit()
71
- return {"message": "Relationship deleted"}
 
 
 
 
 
 
1
+ """
2
+ Relationship CRUD Routes
3
+ """
4
+ from fastapi import APIRouter, Depends, HTTPException, Query
5
+ from sqlalchemy.orm import Session
6
+ from typing import List, Optional
7
+
8
+ from app.api.deps import get_scoped_db
9
+ from app.models import Relationship, Entity
10
+ from app.schemas import RelationshipCreate, RelationshipResponse
11
+
12
+ router = APIRouter(prefix="/relationships", tags=["Relationships"])
13
+
14
+
15
+ @router.get("/", response_model=List[RelationshipResponse])
16
+ def list_relationships(
17
+ type: Optional[str] = None,
18
+ source_id: Optional[str] = None,
19
+ target_id: Optional[str] = None,
20
+ limit: int = Query(default=50, le=200),
21
+ db: Session = Depends(get_scoped_db)
22
+ ):
23
+ """Lista relacionamentos com filtros opcionais"""
24
+ query = db.query(Relationship)
25
+
26
+ if type:
27
+ query = query.filter(Relationship.type == type)
28
+ if source_id:
29
+ query = query.filter(Relationship.source_id == source_id)
30
+ if target_id:
31
+ query = query.filter(Relationship.target_id == target_id)
32
+
33
+ return query.limit(limit).all()
34
+
35
+
36
+ @router.get("/types")
37
+ def get_relationship_types(db: Session = Depends(get_scoped_db)):
38
+ """Retorna todos os tipos de relacionamento unicos"""
39
+ types = db.query(Relationship.type).distinct().all()
40
+ return [t[0] for t in types]
41
+
42
+
43
+ @router.post("/", response_model=RelationshipResponse, status_code=201)
44
+ def create_relationship(
45
+ rel: RelationshipCreate,
46
+ db: Session = Depends(get_scoped_db)
47
+ ):
48
+ """Cria um novo relacionamento entre entidades"""
49
+ source = db.query(Entity).filter(Entity.id == rel.source_id).first()
50
+ target = db.query(Entity).filter(Entity.id == rel.target_id).first()
51
+
52
+ if not source:
53
+ raise HTTPException(status_code=404, detail="Source entity not found")
54
+ if not target:
55
+ raise HTTPException(status_code=404, detail="Target entity not found")
56
+
57
+ db_rel = Relationship(**rel.model_dump())
58
+ db.add(db_rel)
59
+ db.commit()
60
+ db.refresh(db_rel)
61
+ return db_rel
62
+
63
+
64
+ @router.delete("/{relationship_id}")
65
+ def delete_relationship(
66
+ relationship_id: str,
67
+ db: Session = Depends(get_scoped_db)
68
+ ):
69
+ """Deleta um relacionamento"""
70
+ db_rel = db.query(Relationship).filter(Relationship.id == relationship_id).first()
71
+ if not db_rel:
72
+ raise HTTPException(status_code=404, detail="Relationship not found")
73
+
74
+ db.delete(db_rel)
75
+ db.commit()
76
+ return {"message": "Relationship deleted"}
app/api/routes/research.py CHANGED
@@ -1,16 +1,17 @@
1
  """
2
  Research API Routes - Deep research with automatic entity extraction
3
  """
4
- from fastapi import APIRouter, HTTPException
5
- from pydantic import BaseModel, Field
6
- from typing import Optional, List
7
- import traceback
8
-
9
- from app.core.database import get_db
10
- from app.services import lancer
11
- from app.services.nlp import entity_extractor
12
- from app.services.geocoding import geocode
13
- from app.models.entity import Entity, Relationship
 
14
 
15
 
16
  router = APIRouter(prefix="/research", tags=["Research"])
@@ -35,8 +36,8 @@ class ResearchResponse(BaseModel):
35
  processing_time_ms: float
36
 
37
 
38
- @router.post("", response_model=ResearchResponse)
39
- async def research(request: ResearchRequest):
40
  """
41
  Perform AI-powered research using Lancer API and optionally extract entities.
42
 
@@ -60,9 +61,7 @@ async def research(request: ResearchRequest):
60
  # Extract entities if enabled
61
  if request.auto_extract and result.raw_text:
62
  try:
63
- db = next(get_db())
64
-
65
- # Limit text to avoid token limits
66
  text_to_analyze = result.raw_text[:5000]
67
  ner_result = await entity_extractor.extract(text_to_analyze)
68
 
 
1
  """
2
  Research API Routes - Deep research with automatic entity extraction
3
  """
4
+ from fastapi import APIRouter, Depends, HTTPException
5
+ from pydantic import BaseModel, Field
6
+ from typing import Optional, List
7
+ import traceback
8
+ from sqlalchemy.orm import Session
9
+
10
+ from app.api.deps import get_scoped_db
11
+ from app.services import lancer
12
+ from app.services.nlp import entity_extractor
13
+ from app.services.geocoding import geocode
14
+ from app.models.entity import Entity, Relationship
15
 
16
 
17
  router = APIRouter(prefix="/research", tags=["Research"])
 
36
  processing_time_ms: float
37
 
38
 
39
+ @router.post("", response_model=ResearchResponse)
40
+ async def research(request: ResearchRequest, db: Session = Depends(get_scoped_db)):
41
  """
42
  Perform AI-powered research using Lancer API and optionally extract entities.
43
 
 
61
  # Extract entities if enabled
62
  if request.auto_extract and result.raw_text:
63
  try:
64
+ # Limit text to avoid token limits
 
 
65
  text_to_analyze = result.raw_text[:5000]
66
  ner_result = await entity_extractor.extract(text_to_analyze)
67
 
app/api/routes/search.py CHANGED
@@ -1,133 +1,126 @@
1
- """
2
- Search and Analytics Routes
3
- """
4
- from fastapi import APIRouter, Depends, Query
5
- from sqlalchemy.orm import Session
6
- from sqlalchemy import or_, func
7
- from typing import Optional, List
8
-
9
- from app.core.database import get_db
10
- from app.models import Entity, Relationship, Event, Document
11
- from app.schemas import SearchResult, SystemStats, EntityResponse, EventResponse, DocumentResponse
12
-
13
- router = APIRouter(prefix="/search", tags=["Search"])
14
-
15
-
16
- @router.get("", response_model=SearchResult)
17
- def global_search(
18
- q: str = Query(..., min_length=2, description="Search query"),
19
- types: Optional[str] = Query(None, description="Entity types (comma-separated)"),
20
- limit: int = Query(default=20, le=100),
21
- db: Session = Depends(get_db)
22
- ):
23
- """
24
- Busca global em todas as entidades, eventos e documentos
25
- """
26
- search_term = f"%{q}%"
27
- type_filter = types.split(",") if types else None
28
-
29
- # Search entities
30
- entity_query = db.query(Entity).filter(
31
- or_(
32
- Entity.name.ilike(search_term),
33
- Entity.description.ilike(search_term)
34
- )
35
- )
36
- if type_filter:
37
- entity_query = entity_query.filter(Entity.type.in_(type_filter))
38
- entities = entity_query.limit(limit).all()
39
-
40
- # Search events
41
- events = db.query(Event).filter(
42
- or_(
43
- Event.title.ilike(search_term),
44
- Event.description.ilike(search_term)
45
- )
46
- ).limit(limit).all()
47
-
48
- # Search documents
49
- documents = db.query(Document).filter(
50
- or_(
51
- Document.title.ilike(search_term),
52
- Document.content.ilike(search_term)
53
- )
54
- ).limit(limit).all()
55
-
56
- return SearchResult(
57
- entities=entities,
58
- events=events,
59
- documents=documents
60
- )
61
-
62
-
63
- @router.get("/stats", response_model=SystemStats)
64
- def get_system_stats(db: Session = Depends(get_db)):
65
- """
66
- Retorna estatísticas gerais do sistema
67
- Usado no dashboard do VANTAGE
68
- """
69
- total_entities = db.query(Entity).count()
70
- total_relationships = db.query(Relationship).count()
71
- total_events = db.query(Event).count()
72
- total_documents = db.query(Document).count()
73
-
74
- # Count by entity type
75
- type_counts = db.query(
76
- Entity.type,
77
- func.count(Entity.id)
78
- ).group_by(Entity.type).all()
79
-
80
- entities_by_type = {t: c for t, c in type_counts}
81
-
82
- # Recent activity (last 10 entities created)
83
- recent = db.query(Entity).order_by(Entity.created_at.desc()).limit(10).all()
84
- recent_activity = [
85
- {
86
- "id": e.id,
87
- "type": e.type,
88
- "name": e.name,
89
- "created_at": e.created_at.isoformat()
90
- }
91
- for e in recent
92
- ]
93
-
94
- return SystemStats(
95
- total_entities=total_entities,
96
- total_relationships=total_relationships,
97
- total_events=total_events,
98
- total_documents=total_documents,
99
- entities_by_type=entities_by_type,
100
- recent_activity=recent_activity
101
- )
102
-
103
-
104
- @router.get("/geo")
105
- def get_geo_data(
106
- entity_type: Optional[str] = None,
107
- db: Session = Depends(get_db)
108
- ):
109
- """
110
- Retorna entidades com geolocalização
111
- Usado para visualização em mapa
112
- """
113
- query = db.query(Entity).filter(
114
- Entity.latitude.isnot(None),
115
- Entity.longitude.isnot(None)
116
- )
117
-
118
- if entity_type:
119
- query = query.filter(Entity.type == entity_type)
120
-
121
- entities = query.all()
122
-
123
- return [
124
- {
125
- "id": e.id,
126
- "type": e.type,
127
- "name": e.name,
128
- "lat": e.latitude,
129
- "lng": e.longitude,
130
- "properties": e.properties
131
- }
132
- for e in entities
133
- ]
 
1
+ """
2
+ Search and Analytics Routes
3
+ """
4
+ from fastapi import APIRouter, Depends, Query
5
+ from sqlalchemy.orm import Session
6
+ from sqlalchemy import or_, func
7
+ from typing import Optional
8
+
9
+ from app.api.deps import get_scoped_db
10
+ from app.models import Entity, Relationship, Event, Document
11
+ from app.schemas import SearchResult, SystemStats
12
+
13
+ router = APIRouter(prefix="/search", tags=["Search"])
14
+
15
+
16
+ @router.get("", response_model=SearchResult)
17
+ def global_search(
18
+ q: str = Query(..., min_length=2, description="Search query"),
19
+ types: Optional[str] = Query(None, description="Entity types (comma-separated)"),
20
+ limit: int = Query(default=20, le=100),
21
+ db: Session = Depends(get_scoped_db)
22
+ ):
23
+ """
24
+ Busca global em todas as entidades, eventos e documentos.
25
+ """
26
+ search_term = f"%{q}%"
27
+ type_filter = types.split(",") if types else None
28
+
29
+ entity_query = db.query(Entity).filter(
30
+ or_(
31
+ Entity.name.ilike(search_term),
32
+ Entity.description.ilike(search_term)
33
+ )
34
+ )
35
+ if type_filter:
36
+ entity_query = entity_query.filter(Entity.type.in_(type_filter))
37
+ entities = entity_query.limit(limit).all()
38
+
39
+ events = db.query(Event).filter(
40
+ or_(
41
+ Event.title.ilike(search_term),
42
+ Event.description.ilike(search_term)
43
+ )
44
+ ).limit(limit).all()
45
+
46
+ documents = db.query(Document).filter(
47
+ or_(
48
+ Document.title.ilike(search_term),
49
+ Document.content.ilike(search_term)
50
+ )
51
+ ).limit(limit).all()
52
+
53
+ return SearchResult(
54
+ entities=entities,
55
+ events=events,
56
+ documents=documents
57
+ )
58
+
59
+
60
+ @router.get("/stats", response_model=SystemStats)
61
+ def get_system_stats(db: Session = Depends(get_scoped_db)):
62
+ """
63
+ Retorna estatisticas gerais do sistema.
64
+ """
65
+ total_entities = db.query(Entity).count()
66
+ total_relationships = db.query(Relationship).count()
67
+ total_events = db.query(Event).count()
68
+ total_documents = db.query(Document).count()
69
+
70
+ type_counts = db.query(
71
+ Entity.type,
72
+ func.count(Entity.id)
73
+ ).group_by(Entity.type).all()
74
+
75
+ entities_by_type = {t: c for t, c in type_counts}
76
+
77
+ recent = db.query(Entity).order_by(Entity.created_at.desc()).limit(10).all()
78
+ recent_activity = [
79
+ {
80
+ "id": e.id,
81
+ "type": e.type,
82
+ "name": e.name,
83
+ "created_at": e.created_at.isoformat()
84
+ }
85
+ for e in recent
86
+ ]
87
+
88
+ return SystemStats(
89
+ total_entities=total_entities,
90
+ total_relationships=total_relationships,
91
+ total_events=total_events,
92
+ total_documents=total_documents,
93
+ entities_by_type=entities_by_type,
94
+ recent_activity=recent_activity
95
+ )
96
+
97
+
98
+ @router.get("/geo")
99
+ def get_geo_data(
100
+ entity_type: Optional[str] = None,
101
+ db: Session = Depends(get_scoped_db)
102
+ ):
103
+ """
104
+ Retorna entidades com geolocalizacao.
105
+ """
106
+ query = db.query(Entity).filter(
107
+ Entity.latitude.isnot(None),
108
+ Entity.longitude.isnot(None)
109
+ )
110
+
111
+ if entity_type:
112
+ query = query.filter(Entity.type == entity_type)
113
+
114
+ entities = query.all()
115
+
116
+ return [
117
+ {
118
+ "id": e.id,
119
+ "type": e.type,
120
+ "name": e.name,
121
+ "lat": e.latitude,
122
+ "lng": e.longitude,
123
+ "properties": e.properties
124
+ }
125
+ for e in entities
126
+ ]
 
 
 
 
 
 
 
app/api/routes/session.py CHANGED
@@ -1,28 +1,35 @@
1
- """
2
- Session management routes
3
- """
4
- from fastapi import APIRouter, Header, Cookie, Response
5
- from typing import Optional
6
- import uuid
7
-
8
- from app.core.database import create_new_session_id
 
9
 
10
  router = APIRouter(prefix="/session", tags=["Session"])
11
-
12
-
13
- @router.post("/create")
14
- def create_session(response: Response):
15
- """Create a new session and return session_id"""
16
- session_id = create_new_session_id()
17
- response.set_cookie(
18
- key="numidium_session",
19
- value=session_id,
20
- max_age=60*60*24*365, # 1 year
21
- httponly=True,
22
- samesite="none",
23
- secure=True
24
- )
25
- return {"session_id": session_id}
 
 
 
 
 
 
26
 
27
 
28
  @router.get("/current")
 
1
+ """
2
+ Session management routes
3
+ """
4
+ from fastapi import APIRouter, Header, Cookie, Response, Request
5
+ from typing import Optional
6
+ import uuid
7
+
8
+ from app.core.database import create_new_session_id
9
+ from app.config import settings
10
 
11
  router = APIRouter(prefix="/session", tags=["Session"])
12
+
13
+
14
+ @router.post("/create")
15
+ def create_session(response: Response, request: Request):
16
+ """Create a new session and return session_id"""
17
+ session_id = create_new_session_id()
18
+ secure = settings.cookie_secure
19
+ samesite = settings.cookie_samesite
20
+ proto = request.headers.get("x-forwarded-proto", request.url.scheme)
21
+ if proto != "https" and secure:
22
+ secure = False
23
+ samesite = "lax"
24
+ response.set_cookie(
25
+ key="numidium_session",
26
+ value=session_id,
27
+ max_age=60*60*24*365, # 1 year
28
+ httponly=True,
29
+ samesite=samesite,
30
+ secure=secure
31
+ )
32
+ return {"session_id": session_id}
33
 
34
 
35
  @router.get("/current")
app/api/routes/timeline.py CHANGED
@@ -1,14 +1,15 @@
1
  """
2
  Timeline API Routes - Temporal view of entities and relationships
3
  """
4
- from fastapi import APIRouter, Query
5
- from pydantic import BaseModel
6
- from typing import Optional, List, Dict, Any
7
- from datetime import datetime, timedelta
8
- from collections import defaultdict
9
-
10
- from app.core.database import get_db
11
- from app.models.entity import Entity, Relationship
 
12
 
13
 
14
  router = APIRouter(prefix="/timeline", tags=["Timeline"])
@@ -35,19 +36,18 @@ class TimelineResponse(BaseModel):
35
  total_events: int
36
 
37
 
38
- @router.get("", response_model=TimelineResponse)
39
- async def get_timeline(
40
- days: int = Query(default=30, ge=1, le=365),
41
- entity_type: Optional[str] = None,
42
- limit: int = Query(default=100, ge=1, le=500)
43
- ):
 
44
  """
45
  Get timeline of recent entities and relationships.
46
  Groups events by date.
47
  """
48
- db = next(get_db())
49
-
50
- # Calculate date range
51
  end_date = datetime.now()
52
  start_date = end_date - timedelta(days=days)
53
 
@@ -136,10 +136,9 @@ async def get_timeline(
136
  )
137
 
138
 
139
- @router.get("/stats")
140
- async def get_timeline_stats():
141
- """Get statistics for timeline visualization"""
142
- db = next(get_db())
143
 
144
  # Count entities by type
145
  entity_counts = {}
 
1
  """
2
  Timeline API Routes - Temporal view of entities and relationships
3
  """
4
+ from fastapi import APIRouter, Depends, Query
5
+ from pydantic import BaseModel
6
+ from typing import Optional, List, Dict, Any
7
+ from datetime import datetime, timedelta
8
+ from collections import defaultdict
9
+ from sqlalchemy.orm import Session
10
+
11
+ from app.api.deps import get_scoped_db
12
+ from app.models.entity import Entity, Relationship
13
 
14
 
15
  router = APIRouter(prefix="/timeline", tags=["Timeline"])
 
36
  total_events: int
37
 
38
 
39
+ @router.get("", response_model=TimelineResponse)
40
+ async def get_timeline(
41
+ days: int = Query(default=30, ge=1, le=365),
42
+ entity_type: Optional[str] = None,
43
+ limit: int = Query(default=100, ge=1, le=500),
44
+ db: Session = Depends(get_scoped_db)
45
+ ):
46
  """
47
  Get timeline of recent entities and relationships.
48
  Groups events by date.
49
  """
50
+ # Calculate date range
 
 
51
  end_date = datetime.now()
52
  start_date = end_date - timedelta(days=days)
53
 
 
136
  )
137
 
138
 
139
+ @router.get("/stats")
140
+ async def get_timeline_stats(db: Session = Depends(get_scoped_db)):
141
+ """Get statistics for timeline visualization"""
 
142
 
143
  # Count entities by type
144
  entity_counts = {}
app/config.py CHANGED
@@ -23,8 +23,12 @@ class Settings(BaseSettings):
23
  # Cerebras API for LLM-based entity extraction
24
  cerebras_api_key: str = ""
25
 
26
- # CORS
27
- cors_origins: list[str] = ["*"]
 
 
 
 
28
 
29
  class Config:
30
  env_file = ".env"
 
23
  # Cerebras API for LLM-based entity extraction
24
  cerebras_api_key: str = ""
25
 
26
+ # CORS
27
+ cors_origins: list[str] = ["*"]
28
+
29
+ # Session cookie
30
+ cookie_secure: bool = True
31
+ cookie_samesite: str = "none"
32
 
33
  class Config:
34
  env_file = ".env"
app/core/database.py CHANGED
@@ -62,16 +62,21 @@ engine = create_engine(
62
  settings.database_url,
63
  connect_args={"check_same_thread": False}
64
  )
65
- SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
66
-
67
-
68
- def get_db():
69
- """Legacy: Default database session"""
70
- db = SessionLocal()
71
- try:
72
- yield db
73
- finally:
74
- db.close()
 
 
 
 
 
75
 
76
 
77
  def _run_migrations(eng):
 
62
  settings.database_url,
63
  connect_args={"check_same_thread": False}
64
  )
65
+ SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
66
+
67
+
68
+ def get_default_session() -> Session:
69
+ """Create a new session for the default database."""
70
+ return SessionLocal()
71
+
72
+
73
+ def get_db():
74
+ """Legacy: Default database session"""
75
+ db = get_default_session()
76
+ try:
77
+ yield db
78
+ finally:
79
+ db.close()
80
 
81
 
82
  def _run_migrations(eng):
app/services/chat.py CHANGED
@@ -7,8 +7,7 @@ from typing import Optional, List, Dict, Any
7
  from sqlalchemy.orm import Session
8
 
9
  from app.config import settings
10
- from app.core.database import get_db
11
- from app.models.entity import Entity, Relationship
12
 
13
 
14
  LANCER_URL = "https://madras1-lancer.hf.space/api/v1"
@@ -24,13 +23,20 @@ Se não tiver certeza, diga que não sabe em vez de inventar."""
24
  class ChatService:
25
  """Chat service with RAG using local database and Lancer"""
26
 
27
- def __init__(self):
28
- self.api_url = "https://api.cerebras.ai/v1/chat/completions"
29
- self.conversation_history: List[Dict[str, str]] = []
30
-
31
- def clear_history(self):
32
- """Clear conversation history"""
33
- self.conversation_history = []
 
 
 
 
 
 
 
34
 
35
  def _get_local_context(self, query: str, db: Session, limit: int = 5) -> str:
36
  """Get relevant entities from local database"""
@@ -146,17 +152,19 @@ class ChatService:
146
  except Exception as e:
147
  return f"Erro: {str(e)}"
148
 
149
- async def chat(
150
- self,
151
- message: str,
152
- db: Session,
153
- use_web: bool = True,
154
- use_history: bool = True
155
- ) -> Dict[str, Any]:
156
- """Process chat message with RAG"""
157
-
158
- # Get local context
159
- local_context = self._get_local_context(message, db)
 
 
160
 
161
  # Get web context if enabled
162
  web_context = ""
@@ -172,11 +180,11 @@ class ChatService:
172
 
173
  context = "\n\n".join(context_parts) if context_parts else "Nenhum contexto disponível."
174
 
175
- # Build messages
176
- messages = [{"role": "system", "content": SYSTEM_PROMPT}]
177
-
178
- if use_history and self.conversation_history:
179
- messages.extend(self.conversation_history[-6:])
180
 
181
  user_message = f"""Contexto:
182
  {context}
@@ -185,12 +193,13 @@ Pergunta: {message}"""
185
 
186
  messages.append({"role": "user", "content": user_message})
187
 
188
- # Call LLM
189
- response = await self._call_llm(messages)
190
-
191
- # Store history
192
- self.conversation_history.append({"role": "user", "content": message})
193
- self.conversation_history.append({"role": "assistant", "content": response})
 
194
 
195
  return {
196
  "answer": response,
 
7
  from sqlalchemy.orm import Session
8
 
9
  from app.config import settings
10
+ from app.models.entity import Entity, Relationship
 
11
 
12
 
13
  LANCER_URL = "https://madras1-lancer.hf.space/api/v1"
 
23
  class ChatService:
24
  """Chat service with RAG using local database and Lancer"""
25
 
26
+ def __init__(self):
27
+ self.api_url = "https://api.cerebras.ai/v1/chat/completions"
28
+ self.conversation_history: Dict[str, List[Dict[str, str]]] = {}
29
+
30
+ def _get_history(self, session_id: Optional[str]) -> List[Dict[str, str]]:
31
+ key = session_id or "default"
32
+ if key not in self.conversation_history:
33
+ self.conversation_history[key] = []
34
+ return self.conversation_history[key]
35
+
36
+ def clear_history(self, session_id: Optional[str] = None):
37
+ """Clear conversation history"""
38
+ key = session_id or "default"
39
+ self.conversation_history.pop(key, None)
40
 
41
  def _get_local_context(self, query: str, db: Session, limit: int = 5) -> str:
42
  """Get relevant entities from local database"""
 
152
  except Exception as e:
153
  return f"Erro: {str(e)}"
154
 
155
+ async def chat(
156
+ self,
157
+ message: str,
158
+ db: Session,
159
+ use_web: bool = True,
160
+ use_history: bool = True,
161
+ session_id: Optional[str] = None
162
+ ) -> Dict[str, Any]:
163
+ """Process chat message with RAG"""
164
+ history = self._get_history(session_id)
165
+
166
+ # Get local context
167
+ local_context = self._get_local_context(message, db)
168
 
169
  # Get web context if enabled
170
  web_context = ""
 
180
 
181
  context = "\n\n".join(context_parts) if context_parts else "Nenhum contexto disponível."
182
 
183
+ # Build messages
184
+ messages = [{"role": "system", "content": SYSTEM_PROMPT}]
185
+
186
+ if use_history and history:
187
+ messages.extend(history[-6:])
188
 
189
  user_message = f"""Contexto:
190
  {context}
 
193
 
194
  messages.append({"role": "user", "content": user_message})
195
 
196
+ # Call LLM
197
+ response = await self._call_llm(messages)
198
+
199
+ # Store history
200
+ if use_history:
201
+ history.append({"role": "user", "content": message})
202
+ history.append({"role": "assistant", "content": response})
203
 
204
  return {
205
  "answer": response,