@cachly-dev/mcp-server 0.10.65 → 0.10.67

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (277) hide show
  1. package/dist/src/embeddings.js +10 -0
  2. package/dist/src/embeddings.js.map +1 -1
  3. package/dist/src/handlers/brain.js +2 -1
  4. package/dist/src/handlers/brain.js.map +1 -1
  5. package/dist/src/handlers/cache.js +4 -0
  6. package/dist/src/handlers/cache.js.map +1 -1
  7. package/dist/src/handlers/tco.js +1 -0
  8. package/dist/src/handlers/tco.js.map +1 -1
  9. package/dist/src/index.js +2 -2
  10. package/dist/src/index.js.map +1 -1
  11. package/package.json +7 -1
  12. package/server.json +2 -2
  13. package/.env.test.example +0 -23
  14. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -50
  15. package/.github/ISSUE_TEMPLATE/config.yml +0 -8
  16. package/.github/ISSUE_TEMPLATE/feature_request.md +0 -25
  17. package/.github/workflows/eas-build-android.yml +0 -58
  18. package/.github/workflows/eas-update.yml +0 -55
  19. package/.github/workflows/publish.yml +0 -72
  20. package/BENCH.md +0 -113
  21. package/CHANGELOG.md +0 -653
  22. package/Dockerfile +0 -16
  23. package/LAUNCH_TEXTS.md +0 -182
  24. package/PROGRESS.md +0 -141
  25. package/STRATEGY.md +0 -228
  26. package/VISION_10X.md +0 -133
  27. package/apps/travel-chaos-organizer/.env.example +0 -49
  28. package/apps/travel-chaos-organizer/DEPLOY.md +0 -328
  29. package/apps/travel-chaos-organizer/Makefile +0 -57
  30. package/apps/travel-chaos-organizer/README.md +0 -159
  31. package/apps/travel-chaos-organizer/backend/.env.example +0 -10
  32. package/apps/travel-chaos-organizer/backend/Dockerfile +0 -25
  33. package/apps/travel-chaos-organizer/backend/alembic/env.py +0 -56
  34. package/apps/travel-chaos-organizer/backend/alembic/script.py.mako +0 -24
  35. package/apps/travel-chaos-organizer/backend/alembic/versions/.gitkeep +0 -0
  36. package/apps/travel-chaos-organizer/backend/alembic/versions/0001_stripe_customer_id.py +0 -24
  37. package/apps/travel-chaos-organizer/backend/alembic/versions/0002_telegram_chat_id.py +0 -24
  38. package/apps/travel-chaos-organizer/backend/alembic.ini +0 -39
  39. package/apps/travel-chaos-organizer/backend/app/__init__.py +0 -0
  40. package/apps/travel-chaos-organizer/backend/app/auth/__init__.py +0 -0
  41. package/apps/travel-chaos-organizer/backend/app/auth/keycloak.py +0 -81
  42. package/apps/travel-chaos-organizer/backend/app/config.py +0 -66
  43. package/apps/travel-chaos-organizer/backend/app/db/__init__.py +0 -0
  44. package/apps/travel-chaos-organizer/backend/app/db/database.py +0 -20
  45. package/apps/travel-chaos-organizer/backend/app/db/migrations/001_init.sql +0 -71
  46. package/apps/travel-chaos-organizer/backend/app/db/migrations/002_telemetry_freemium.sql +0 -31
  47. package/apps/travel-chaos-organizer/backend/app/db/migrations/003_telegram_chat_id.sql +0 -4
  48. package/apps/travel-chaos-organizer/backend/app/db/migrations/004_stripe_and_email.sql +0 -5
  49. package/apps/travel-chaos-organizer/backend/app/limiter.py +0 -4
  50. package/apps/travel-chaos-organizer/backend/app/logging_config.py +0 -25
  51. package/apps/travel-chaos-organizer/backend/app/main.py +0 -77
  52. package/apps/travel-chaos-organizer/backend/app/models/__init__.py +0 -0
  53. package/apps/travel-chaos-organizer/backend/app/models/schemas.py +0 -142
  54. package/apps/travel-chaos-organizer/backend/app/routers/__init__.py +0 -0
  55. package/apps/travel-chaos-organizer/backend/app/routers/admin.py +0 -237
  56. package/apps/travel-chaos-organizer/backend/app/routers/cache.py +0 -13
  57. package/apps/travel-chaos-organizer/backend/app/routers/events.py +0 -29
  58. package/apps/travel-chaos-organizer/backend/app/routers/export.py +0 -122
  59. package/apps/travel-chaos-organizer/backend/app/routers/inbox.py +0 -122
  60. package/apps/travel-chaos-organizer/backend/app/routers/items.py +0 -130
  61. package/apps/travel-chaos-organizer/backend/app/routers/mail.py +0 -108
  62. package/apps/travel-chaos-organizer/backend/app/routers/parse.py +0 -303
  63. package/apps/travel-chaos-organizer/backend/app/routers/payments.py +0 -179
  64. package/apps/travel-chaos-organizer/backend/app/routers/trips.py +0 -134
  65. package/apps/travel-chaos-organizer/backend/app/routers/users.py +0 -169
  66. package/apps/travel-chaos-organizer/backend/app/routers/waitlist.py +0 -37
  67. package/apps/travel-chaos-organizer/backend/app/services/__init__.py +0 -0
  68. package/apps/travel-chaos-organizer/backend/app/services/cache.py +0 -131
  69. package/apps/travel-chaos-organizer/backend/app/services/drip.py +0 -128
  70. package/apps/travel-chaos-organizer/backend/app/services/email.py +0 -61
  71. package/apps/travel-chaos-organizer/backend/app/services/errors.py +0 -44
  72. package/apps/travel-chaos-organizer/backend/app/services/notifier.py +0 -10
  73. package/apps/travel-chaos-organizer/backend/app/services/ollama.py +0 -101
  74. package/apps/travel-chaos-organizer/backend/app/services/parser.py +0 -22
  75. package/apps/travel-chaos-organizer/backend/app/services/quota.py +0 -85
  76. package/apps/travel-chaos-organizer/backend/app/services/sentry.py +0 -11
  77. package/apps/travel-chaos-organizer/backend/app/services/telemetry.py +0 -35
  78. package/apps/travel-chaos-organizer/backend/pytest.ini +0 -4
  79. package/apps/travel-chaos-organizer/backend/requirements-dev.txt +0 -6
  80. package/apps/travel-chaos-organizer/backend/requirements.txt +0 -25
  81. package/apps/travel-chaos-organizer/backend/tests/__init__.py +0 -0
  82. package/apps/travel-chaos-organizer/backend/tests/conftest.py +0 -223
  83. package/apps/travel-chaos-organizer/backend/tests/test_admin.py +0 -99
  84. package/apps/travel-chaos-organizer/backend/tests/test_cache.py +0 -142
  85. package/apps/travel-chaos-organizer/backend/tests/test_drip.py +0 -32
  86. package/apps/travel-chaos-organizer/backend/tests/test_e2e_chain.py +0 -121
  87. package/apps/travel-chaos-organizer/backend/tests/test_export.py +0 -48
  88. package/apps/travel-chaos-organizer/backend/tests/test_inbox.py +0 -107
  89. package/apps/travel-chaos-organizer/backend/tests/test_items.py +0 -138
  90. package/apps/travel-chaos-organizer/backend/tests/test_mail.py +0 -113
  91. package/apps/travel-chaos-organizer/backend/tests/test_ollama.py +0 -145
  92. package/apps/travel-chaos-organizer/backend/tests/test_parse_url.py +0 -98
  93. package/apps/travel-chaos-organizer/backend/tests/test_parser.py +0 -72
  94. package/apps/travel-chaos-organizer/backend/tests/test_payments.py +0 -163
  95. package/apps/travel-chaos-organizer/backend/tests/test_quota.py +0 -46
  96. package/apps/travel-chaos-organizer/backend/tests/test_search.py +0 -67
  97. package/apps/travel-chaos-organizer/backend/tests/test_trips.py +0 -123
  98. package/apps/travel-chaos-organizer/backend/tests/test_users.py +0 -52
  99. package/apps/travel-chaos-organizer/backups/.gitkeep +0 -0
  100. package/apps/travel-chaos-organizer/caddy/Caddyfile +0 -27
  101. package/apps/travel-chaos-organizer/docker-compose.node1.yml +0 -31
  102. package/apps/travel-chaos-organizer/docker-compose.override.yml +0 -14
  103. package/apps/travel-chaos-organizer/docker-compose.yml +0 -128
  104. package/apps/travel-chaos-organizer/frontend/.env.example +0 -13
  105. package/apps/travel-chaos-organizer/frontend/app/(app)/_layout.tsx +0 -80
  106. package/apps/travel-chaos-organizer/frontend/app/(app)/inbox.tsx +0 -166
  107. package/apps/travel-chaos-organizer/frontend/app/(app)/search.tsx +0 -130
  108. package/apps/travel-chaos-organizer/frontend/app/(app)/settings.tsx +0 -381
  109. package/apps/travel-chaos-organizer/frontend/app/(app)/trips/[id].tsx +0 -346
  110. package/apps/travel-chaos-organizer/frontend/app/(app)/trips.tsx +0 -363
  111. package/apps/travel-chaos-organizer/frontend/app/(app)/upgrade/cancel.tsx +0 -34
  112. package/apps/travel-chaos-organizer/frontend/app/(app)/upgrade/success.tsx +0 -35
  113. package/apps/travel-chaos-organizer/frontend/app/(auth)/login.tsx +0 -91
  114. package/apps/travel-chaos-organizer/frontend/app/_layout.tsx +0 -71
  115. package/apps/travel-chaos-organizer/frontend/app/auth.tsx +0 -29
  116. package/apps/travel-chaos-organizer/frontend/app/index.tsx +0 -16
  117. package/apps/travel-chaos-organizer/frontend/app/onboarding.tsx +0 -119
  118. package/apps/travel-chaos-organizer/frontend/app.config.ts +0 -54
  119. package/apps/travel-chaos-organizer/frontend/assets/adaptive-icon.svg +0 -66
  120. package/apps/travel-chaos-organizer/frontend/assets/icon.svg +0 -79
  121. package/apps/travel-chaos-organizer/frontend/assets/splash.svg +0 -15
  122. package/apps/travel-chaos-organizer/frontend/components/BottomSheet.tsx +0 -102
  123. package/apps/travel-chaos-organizer/frontend/components/ConfettiCannon.tsx +0 -107
  124. package/apps/travel-chaos-organizer/frontend/components/ConfirmDialog.tsx +0 -109
  125. package/apps/travel-chaos-organizer/frontend/components/ErrorBoundary.tsx +0 -45
  126. package/apps/travel-chaos-organizer/frontend/components/FileUploadButton.tsx +0 -19
  127. package/apps/travel-chaos-organizer/frontend/components/ImportSheet.tsx +0 -155
  128. package/apps/travel-chaos-organizer/frontend/components/OfflineBadge.tsx +0 -55
  129. package/apps/travel-chaos-organizer/frontend/components/QRCodeViewer.tsx +0 -80
  130. package/apps/travel-chaos-organizer/frontend/components/ScoreRing.tsx +0 -39
  131. package/apps/travel-chaos-organizer/frontend/components/Screen.tsx +0 -39
  132. package/apps/travel-chaos-organizer/frontend/components/Skeleton.tsx +0 -70
  133. package/apps/travel-chaos-organizer/frontend/components/SwipeableInboxItem.tsx +0 -121
  134. package/apps/travel-chaos-organizer/frontend/components/SwipeableTimelineItem.tsx +0 -70
  135. package/apps/travel-chaos-organizer/frontend/components/TimelineItem.tsx +0 -73
  136. package/apps/travel-chaos-organizer/frontend/components/TimelineItemDetail.tsx +0 -130
  137. package/apps/travel-chaos-organizer/frontend/components/Toast.tsx +0 -60
  138. package/apps/travel-chaos-organizer/frontend/components/ToastContext.tsx +0 -42
  139. package/apps/travel-chaos-organizer/frontend/components/TripCard.tsx +0 -146
  140. package/apps/travel-chaos-organizer/frontend/components/TripQRModal.tsx +0 -70
  141. package/apps/travel-chaos-organizer/frontend/components/UpgradeBanner.tsx +0 -44
  142. package/apps/travel-chaos-organizer/frontend/eas.json +0 -22
  143. package/apps/travel-chaos-organizer/frontend/hooks/useInbox.ts +0 -24
  144. package/apps/travel-chaos-organizer/frontend/hooks/useNetworkSync.ts +0 -60
  145. package/apps/travel-chaos-organizer/frontend/hooks/useTrips.ts +0 -112
  146. package/apps/travel-chaos-organizer/frontend/lib/api.ts +0 -223
  147. package/apps/travel-chaos-organizer/frontend/lib/auth.ts +0 -93
  148. package/apps/travel-chaos-organizer/frontend/lib/db.ts +0 -86
  149. package/apps/travel-chaos-organizer/frontend/lib/fileCache.ts +0 -54
  150. package/apps/travel-chaos-organizer/frontend/lib/haptics.ts +0 -13
  151. package/apps/travel-chaos-organizer/frontend/lib/notifications.ts +0 -112
  152. package/apps/travel-chaos-organizer/frontend/lib/offlineQueue.ts +0 -145
  153. package/apps/travel-chaos-organizer/frontend/lib/quota.tsx +0 -43
  154. package/apps/travel-chaos-organizer/frontend/lib/sentry.ts +0 -28
  155. package/apps/travel-chaos-organizer/frontend/lib/shareIntent.ts +0 -44
  156. package/apps/travel-chaos-organizer/frontend/package-lock.json +0 -13221
  157. package/apps/travel-chaos-organizer/frontend/package.json +0 -44
  158. package/apps/travel-chaos-organizer/frontend/tsconfig.json +0 -23
  159. package/apps/travel-chaos-organizer/keycloak/realm-export.json +0 -59
  160. package/apps/travel-chaos-organizer/landing/index.html +0 -773
  161. package/apps/travel-chaos-organizer/scripts/backup.sh +0 -14
  162. package/apps/travel-chaos-organizer/scripts/configure-env.sh +0 -82
  163. package/apps/travel-chaos-organizer/scripts/deploy.sh +0 -54
  164. package/apps/travel-chaos-organizer/scripts/health-check.sh +0 -36
  165. package/apps/travel-chaos-organizer/scripts/migrate.sh +0 -7
  166. package/apps/travel-chaos-organizer/scripts/server-setup.sh +0 -85
  167. package/apps/travel-chaos-organizer/scripts/smoke-test.sh +0 -29
  168. package/apps/travel-chaos-organizer/scripts/verify-tracking.sh +0 -71
  169. package/apps/travel-chaos-organizer/telegram-bot/.env.example +0 -3
  170. package/apps/travel-chaos-organizer/telegram-bot/Dockerfile +0 -6
  171. package/apps/travel-chaos-organizer/telegram-bot/bot.py +0 -504
  172. package/apps/travel-chaos-organizer/telegram-bot/requirements.txt +0 -3
  173. package/dist/src/__tests__/auth.test.d.ts +0 -1
  174. package/dist/src/__tests__/auth.test.js +0 -107
  175. package/dist/src/__tests__/auth.test.js.map +0 -1
  176. package/dist/src/__tests__/brain-flow.test.d.ts +0 -12
  177. package/dist/src/__tests__/brain-flow.test.js +0 -1241
  178. package/dist/src/__tests__/brain-flow.test.js.map +0 -1
  179. package/dist/src/__tests__/confidence.test.d.ts +0 -13
  180. package/dist/src/__tests__/confidence.test.js +0 -233
  181. package/dist/src/__tests__/confidence.test.js.map +0 -1
  182. package/dist/src/__tests__/e2e/api-auth.test.d.ts +0 -7
  183. package/dist/src/__tests__/e2e/api-auth.test.js +0 -162
  184. package/dist/src/__tests__/e2e/api-auth.test.js.map +0 -1
  185. package/dist/src/__tests__/e2e/api-health.test.d.ts +0 -7
  186. package/dist/src/__tests__/e2e/api-health.test.js +0 -95
  187. package/dist/src/__tests__/e2e/api-health.test.js.map +0 -1
  188. package/dist/src/__tests__/e2e/cli.test.d.ts +0 -10
  189. package/dist/src/__tests__/e2e/cli.test.js +0 -204
  190. package/dist/src/__tests__/e2e/cli.test.js.map +0 -1
  191. package/dist/src/__tests__/e2e/config.d.ts +0 -35
  192. package/dist/src/__tests__/e2e/config.js +0 -53
  193. package/dist/src/__tests__/e2e/config.js.map +0 -1
  194. package/dist/src/__tests__/e2e/funnel.test.d.ts +0 -13
  195. package/dist/src/__tests__/e2e/funnel.test.js +0 -90
  196. package/dist/src/__tests__/e2e/funnel.test.js.map +0 -1
  197. package/dist/src/__tests__/e2e/mcp-tools.test.d.ts +0 -7
  198. package/dist/src/__tests__/e2e/mcp-tools.test.js +0 -129
  199. package/dist/src/__tests__/e2e/mcp-tools.test.js.map +0 -1
  200. package/dist/src/__tests__/e2e/provisioning.test.d.ts +0 -15
  201. package/dist/src/__tests__/e2e/provisioning.test.js +0 -123
  202. package/dist/src/__tests__/e2e/provisioning.test.js.map +0 -1
  203. package/dist/src/__tests__/handlers-unit.test.d.ts +0 -12
  204. package/dist/src/__tests__/handlers-unit.test.js +0 -505
  205. package/dist/src/__tests__/handlers-unit.test.js.map +0 -1
  206. package/dist/src/__tests__/namespace.test.d.ts +0 -1
  207. package/dist/src/__tests__/namespace.test.js +0 -114
  208. package/dist/src/__tests__/namespace.test.js.map +0 -1
  209. package/dist/src/__tests__/rerank.test.d.ts +0 -1
  210. package/dist/src/__tests__/rerank.test.js +0 -125
  211. package/dist/src/__tests__/rerank.test.js.map +0 -1
  212. package/dist/src/__tests__/utils.test.d.ts +0 -1
  213. package/dist/src/__tests__/utils.test.js +0 -83
  214. package/dist/src/__tests__/utils.test.js.map +0 -1
  215. package/dist/src/bench/cachly-bench.d.ts +0 -39
  216. package/dist/src/bench/cachly-bench.js +0 -223
  217. package/dist/src/bench/cachly-bench.js.map +0 -1
  218. package/dist/src/bench/fixtures.d.ts +0 -37
  219. package/dist/src/bench/fixtures.js +0 -225
  220. package/dist/src/bench/fixtures.js.map +0 -1
  221. package/dist/src/search.test.d.ts +0 -1
  222. package/dist/src/search.test.js +0 -713
  223. package/dist/src/search.test.js.map +0 -1
  224. package/eslint.config.js +0 -16
  225. package/glama.json +0 -36
  226. package/llms.txt +0 -155
  227. package/packages/telegram-notify/SPLIT_PLAN.md +0 -106
  228. package/packages/telegram-notify/client.ts +0 -88
  229. package/packages/telegram-notify/emojis.json +0 -22
  230. package/packages/telegram-notify/pyproject.toml +0 -17
  231. package/packages/telegram-notify/telegram_notify/__init__.py +0 -3
  232. package/packages/telegram-notify/telegram_notify/client.py +0 -87
  233. package/packages/telegram-notify/telegram_notify/emojis.json +0 -22
  234. package/scripts/e2e-run.sh +0 -160
  235. package/scripts/generate-icons.js +0 -28
  236. package/smithery.yaml +0 -45
  237. package/src/__tests__/auth.test.ts +0 -147
  238. package/src/__tests__/brain-flow.test.ts +0 -1472
  239. package/src/__tests__/confidence.test.ts +0 -285
  240. package/src/__tests__/e2e/api-auth.test.ts +0 -182
  241. package/src/__tests__/e2e/api-health.test.ts +0 -103
  242. package/src/__tests__/e2e/cli.test.ts +0 -228
  243. package/src/__tests__/e2e/config.ts +0 -53
  244. package/src/__tests__/e2e/funnel.test.ts +0 -99
  245. package/src/__tests__/e2e/mcp-tools.test.ts +0 -191
  246. package/src/__tests__/e2e/provisioning.test.ts +0 -128
  247. package/src/__tests__/handlers-unit.test.ts +0 -677
  248. package/src/__tests__/namespace.test.ts +0 -126
  249. package/src/__tests__/rerank.test.ts +0 -141
  250. package/src/__tests__/utils.test.ts +0 -81
  251. package/src/auth.ts +0 -46
  252. package/src/bench/cachly-bench.ts +0 -263
  253. package/src/bench/fixtures.ts +0 -256
  254. package/src/ckg.ts +0 -103
  255. package/src/confidence.ts +0 -38
  256. package/src/embeddings.ts +0 -217
  257. package/src/handlers/advanced.ts +0 -647
  258. package/src/handlers/brain.ts +0 -2622
  259. package/src/handlers/cache.ts +0 -633
  260. package/src/handlers/context.ts +0 -222
  261. package/src/handlers/fedbrain.ts +0 -1232
  262. package/src/handlers/instances.ts +0 -180
  263. package/src/handlers/roadmap.ts +0 -201
  264. package/src/handlers/syndicate.ts +0 -460
  265. package/src/handlers/tco.ts +0 -345
  266. package/src/handlers/team.ts +0 -1249
  267. package/src/index.ts +0 -2815
  268. package/src/namespace.ts +0 -128
  269. package/src/notifier.ts +0 -5
  270. package/src/rerank.ts +0 -148
  271. package/src/search.test.ts +0 -848
  272. package/src/search.ts +0 -1781
  273. package/src/tools.ts +0 -2056
  274. package/src/utils.ts +0 -90
  275. package/test-features.cjs +0 -23
  276. package/tsconfig.json +0 -16
  277. package/vitest.config.ts +0 -14
@@ -1,122 +0,0 @@
1
- import json
2
- from datetime import datetime
3
- from typing import Annotated
4
- from uuid import UUID
5
- from fastapi import APIRouter, Depends, HTTPException, status
6
- from sqlalchemy import text
7
- from sqlalchemy.ext.asyncio import AsyncSession
8
- from app.auth.keycloak import user_id
9
- from app.db.database import get_db
10
- from app.models.schemas import InboxItemOut, InboxAssign
11
-
12
-
13
- def _safe_dt(val: str | None) -> datetime | None:
14
- """Convert an ISO 8601 string to datetime, returning None on failure."""
15
- if not val or not isinstance(val, str):
16
- return None
17
- try:
18
- return datetime.fromisoformat(val.replace("Z", "+00:00"))
19
- except ValueError:
20
- return None
21
-
22
- router = APIRouter(prefix="/inbox", tags=["inbox"])
23
-
24
-
25
- def _parse_inbox_row(row: dict) -> dict:
26
- if isinstance(row.get("parsed_data"), str):
27
- try:
28
- row["parsed_data"] = json.loads(row["parsed_data"])
29
- except (json.JSONDecodeError, TypeError):
30
- row["parsed_data"] = None
31
- return row
32
-
33
-
34
- @router.get("", response_model=list[InboxItemOut])
35
- async def list_inbox(
36
- uid: Annotated[str, Depends(user_id)],
37
- db: Annotated[AsyncSession, Depends(get_db)],
38
- status_filter: str = "pending",
39
- ):
40
- result = await db.execute(
41
- text("SELECT * FROM chaos_inbox WHERE user_id = :uid AND status = :status ORDER BY created_at DESC"),
42
- {"uid": uid, "status": status_filter},
43
- )
44
- return [_parse_inbox_row(dict(r._mapping)) for r in result.fetchall()]
45
-
46
-
47
- @router.post("/{inbox_id}/assign", response_model=dict)
48
- async def assign_to_trip(
49
- inbox_id: UUID,
50
- body: InboxAssign,
51
- uid: Annotated[str, Depends(user_id)],
52
- db: Annotated[AsyncSession, Depends(get_db)],
53
- ):
54
- iid = str(inbox_id)
55
- row = await db.execute(
56
- text("SELECT * FROM chaos_inbox WHERE id = :id AND user_id = :uid"),
57
- {"id": iid, "uid": uid},
58
- )
59
- inbox_item = row.fetchone()
60
- if not inbox_item:
61
- raise HTTPException(status_code=404, detail="Inbox item not found")
62
-
63
- # Verify the target trip belongs to this user (prevents assigning to other users' trips)
64
- trip_check = await db.execute(
65
- text("SELECT id FROM trips WHERE id = :tid AND user_id = :uid"),
66
- {"tid": str(body.trip_id), "uid": uid},
67
- )
68
- if not trip_check.fetchone():
69
- raise HTTPException(status_code=403, detail="Trip not found or access denied")
70
-
71
- item = dict(inbox_item._mapping)
72
- raw_pd = item.get("parsed_data") or {}
73
- parsed = json.loads(raw_pd) if isinstance(raw_pd, str) else raw_pd
74
- title = parsed.get("title", "Imported item") if isinstance(parsed, dict) else "Imported item"
75
-
76
- result = await db.execute(
77
- text("""
78
- INSERT INTO trip_items (trip_id, user_id, type, title, raw_text, parsed_data,
79
- event_at, event_end_at, booking_ref, provider)
80
- VALUES (:trip_id, :uid, :type, :title, :raw, :pd,
81
- :event_at, :event_end_at, :booking_ref, :provider)
82
- RETURNING id, title, event_at
83
- """),
84
- {
85
- "trip_id": str(body.trip_id), "uid": uid, "type": body.type, "title": title,
86
- "raw": item.get("raw_content"), "pd": json.dumps(parsed),
87
- "event_at": _safe_dt(parsed.get("event_at") if isinstance(parsed, dict) else None),
88
- "event_end_at": _safe_dt(parsed.get("event_end_at") if isinstance(parsed, dict) else None),
89
- "booking_ref": parsed.get("booking_ref") if isinstance(parsed, dict) else None,
90
- "provider": parsed.get("provider") if isinstance(parsed, dict) else None,
91
- },
92
- )
93
- row = result.fetchone()
94
- trip_item_id = row[0]
95
- item_title = row[1]
96
- event_at = row[2].isoformat() if row[2] else None
97
-
98
- await db.execute(
99
- text("UPDATE chaos_inbox SET status = 'assigned', updated_at = CURRENT_TIMESTAMP WHERE id = :id"),
100
- {"id": iid},
101
- )
102
- await db.execute(
103
- text("UPDATE attachments SET trip_item_id = :tid, inbox_id = NULL WHERE inbox_id = :iid"),
104
- {"tid": trip_item_id, "iid": iid},
105
- )
106
- await db.commit()
107
- return {"trip_item_id": str(trip_item_id), "title": item_title, "event_at": event_at}
108
-
109
-
110
- @router.delete("/{inbox_id}", status_code=status.HTTP_204_NO_CONTENT)
111
- async def reject_inbox_item(
112
- inbox_id: UUID,
113
- uid: Annotated[str, Depends(user_id)],
114
- db: Annotated[AsyncSession, Depends(get_db)],
115
- ):
116
- result = await db.execute(
117
- text("UPDATE chaos_inbox SET status = 'rejected', updated_at = CURRENT_TIMESTAMP WHERE id = :id AND user_id = :uid"),
118
- {"id": str(inbox_id), "uid": uid},
119
- )
120
- await db.commit()
121
- if result.rowcount == 0:
122
- raise HTTPException(status_code=404, detail="Inbox item not found")
@@ -1,130 +0,0 @@
1
- import json
2
- from typing import Annotated
3
- from uuid import UUID
4
- from fastapi import APIRouter, Depends, HTTPException, Request, status
5
- from sqlalchemy import text
6
- from sqlalchemy.ext.asyncio import AsyncSession
7
- from app.auth.keycloak import user_id
8
- from app.db.database import get_db
9
- from app.limiter import limiter
10
- from app.models.schemas import TripItemCreate, TripItemOut, TripItemUpdate
11
-
12
- router = APIRouter(prefix="/trips/{trip_id}/items", tags=["items"])
13
-
14
-
15
- async def _assert_trip_owner(trip_id: str, uid: str, db: AsyncSession) -> None:
16
- result = await db.execute(
17
- text("SELECT id FROM trips WHERE id = :id AND user_id = :uid"),
18
- {"id": trip_id, "uid": uid},
19
- )
20
- if not result.fetchone():
21
- raise HTTPException(status_code=404, detail="Trip not found")
22
-
23
-
24
- def _parse_item_row(row: dict) -> dict:
25
- """Deserialize parsed_data from JSON string (SQLite) or dict (PostgreSQL)."""
26
- if isinstance(row.get("parsed_data"), str):
27
- try:
28
- row["parsed_data"] = json.loads(row["parsed_data"])
29
- except (json.JSONDecodeError, TypeError):
30
- row["parsed_data"] = None
31
- return row
32
-
33
-
34
- @router.get("", response_model=list[TripItemOut])
35
- async def list_items(
36
- trip_id: UUID,
37
- uid: Annotated[str, Depends(user_id)],
38
- db: Annotated[AsyncSession, Depends(get_db)],
39
- ):
40
- tid = str(trip_id)
41
- await _assert_trip_owner(tid, uid, db)
42
- result = await db.execute(
43
- text("SELECT * FROM trip_items WHERE trip_id = :tid ORDER BY event_at ASC"),
44
- {"tid": tid},
45
- )
46
- return [_parse_item_row(dict(r._mapping)) for r in result.fetchall()]
47
-
48
-
49
- @limiter.limit("30/minute")
50
- @router.post("", response_model=TripItemOut, status_code=status.HTTP_201_CREATED)
51
- async def create_item(
52
- request: Request,
53
- trip_id: UUID,
54
- body: TripItemCreate,
55
- uid: Annotated[str, Depends(user_id)],
56
- db: Annotated[AsyncSession, Depends(get_db)],
57
- ):
58
- tid = str(trip_id)
59
- await _assert_trip_owner(tid, uid, db)
60
- pd_json = json.dumps(body.parsed_data) if body.parsed_data else None
61
- result = await db.execute(
62
- text("""
63
- INSERT INTO trip_items
64
- (trip_id, user_id, type, title, raw_text, parsed_data,
65
- event_at, event_end_at, booking_ref, provider)
66
- VALUES
67
- (:trip_id, :uid, :type, :title, :raw_text, :parsed_data,
68
- :event_at, :event_end_at, :booking_ref, :provider)
69
- RETURNING *
70
- """),
71
- {
72
- "trip_id": tid, "uid": uid, "type": body.type, "title": body.title,
73
- "raw_text": body.raw_text, "parsed_data": pd_json,
74
- "event_at": body.event_at, "event_end_at": body.event_end_at,
75
- "booking_ref": body.booking_ref, "provider": body.provider,
76
- },
77
- )
78
- await db.commit()
79
- return _parse_item_row(dict(result.fetchone()._mapping))
80
-
81
-
82
- @limiter.limit("30/minute")
83
- @router.patch("/{item_id}", response_model=TripItemOut)
84
- async def update_item(
85
- request: Request,
86
- trip_id: UUID,
87
- item_id: UUID,
88
- body: TripItemUpdate,
89
- uid: Annotated[str, Depends(user_id)],
90
- db: Annotated[AsyncSession, Depends(get_db)],
91
- ):
92
- tid, iid = str(trip_id), str(item_id)
93
- await _assert_trip_owner(tid, uid, db)
94
- updates = body.model_dump(exclude_none=True)
95
- if not updates:
96
- raise HTTPException(status_code=400, detail="Nothing to update")
97
- if "parsed_data" in updates:
98
- updates["parsed_data"] = json.dumps(updates["parsed_data"])
99
-
100
- set_clauses = ", ".join(f"{k} = :{k}" for k in updates)
101
- updates.update({"item_id": iid, "trip_id": tid})
102
- result = await db.execute(
103
- text(f"UPDATE trip_items SET {set_clauses}, updated_at = CURRENT_TIMESTAMP WHERE id = :item_id AND trip_id = :trip_id RETURNING *"),
104
- updates,
105
- )
106
- row = result.fetchone()
107
- if not row:
108
- raise HTTPException(status_code=404, detail="Item not found")
109
- await db.commit()
110
- return _parse_item_row(dict(row._mapping))
111
-
112
-
113
- @limiter.limit("30/minute")
114
- @router.delete("/{item_id}", status_code=status.HTTP_204_NO_CONTENT)
115
- async def delete_item(
116
- request: Request,
117
- trip_id: UUID,
118
- item_id: UUID,
119
- uid: Annotated[str, Depends(user_id)],
120
- db: Annotated[AsyncSession, Depends(get_db)],
121
- ):
122
- tid, iid = str(trip_id), str(item_id)
123
- await _assert_trip_owner(tid, uid, db)
124
- result = await db.execute(
125
- text("DELETE FROM trip_items WHERE id = :item_id AND trip_id = :trip_id"),
126
- {"item_id": iid, "trip_id": tid},
127
- )
128
- await db.commit()
129
- if result.rowcount == 0:
130
- raise HTTPException(status_code=404, detail="Item not found")
@@ -1,108 +0,0 @@
1
- """
2
- Mail import endpoint — paste raw email text or forward as plain text.
3
- Gmail / Outlook share-to-app flows send the body as plain text;
4
- we strip headers and run it through the same Ollama parsing pipeline.
5
- Results are Cachly-cached by content hash when CACHLY_REDIS_URL is configured.
6
- """
7
- import json
8
- import re
9
- from datetime import datetime
10
- from typing import Annotated
11
- from uuid import UUID
12
-
13
- from fastapi import APIRouter, Body, Depends, Query
14
- from pydantic import ValidationError
15
- from sqlalchemy import text
16
- from sqlalchemy.ext.asyncio import AsyncSession
17
-
18
- from app.auth.keycloak import user_id
19
- from app.config import get_settings
20
- from app.db.database import get_db
21
- from app.models.schemas import ParseResponse, ParsedTravelData
22
- from app.services import ollama as ollama_svc
23
-
24
- router = APIRouter(prefix="/mail", tags=["mail"])
25
- settings = get_settings()
26
-
27
- _HEADER_RE = re.compile(
28
- r"^(From|To|Cc|Bcc|Subject|Date|Message-ID|MIME-Version|Content-Type"
29
- r"|Content-Transfer-Encoding|Delivered-To|Received|Return-Path|X-[^:]+)"
30
- r":\s.*$",
31
- re.MULTILINE | re.IGNORECASE,
32
- )
33
- _BLANK_LINES_RE = re.compile(r"\n{3,}")
34
-
35
-
36
- def _safe_dt(val: str | None) -> datetime | None:
37
- """Parse an Ollama-produced datetime string to a proper datetime object.
38
- asyncpg requires a datetime object (not a string) for TIMESTAMPTZ columns.
39
- Returns None on missing or unparseable input."""
40
- if val is None:
41
- return None
42
- try:
43
- return datetime.fromisoformat(val.replace("Z", "+00:00"))
44
- except (ValueError, AttributeError):
45
- return None
46
-
47
-
48
- def _build_parsed(parsed_dict: dict) -> ParsedTravelData:
49
- """Build a ParsedTravelData, falling back to safe defaults on ValidationError.
50
- Matches the same pattern as parse.py to avoid a double-fail scenario."""
51
- try:
52
- return ParsedTravelData(**{k: parsed_dict.get(k) for k in ParsedTravelData.model_fields})
53
- except ValidationError:
54
- return ParsedTravelData(
55
- type="other",
56
- title=str(parsed_dict.get("title") or "E-Mail"),
57
- booking_ref=parsed_dict.get("booking_ref"),
58
- provider=parsed_dict.get("provider"),
59
- event_at=parsed_dict.get("event_at"),
60
- raw_summary=parsed_dict.get("raw_summary"),
61
- )
62
-
63
-
64
- def strip_email_headers(raw: str) -> str:
65
- cleaned = _HEADER_RE.sub("", raw)
66
- cleaned = _BLANK_LINES_RE.sub("\n\n", cleaned)
67
- return cleaned.strip()
68
-
69
-
70
- @router.post("/import", response_model=ParseResponse)
71
- async def import_mail(
72
- uid: Annotated[str, Depends(user_id)],
73
- db: Annotated[AsyncSession, Depends(get_db)],
74
- raw_email: str = Body(..., media_type="text/plain", description="Raw email body (paste or share)"),
75
- trip_id: UUID | None = Query(None, description="Assign directly to this trip (optional)"),
76
- ):
77
- body_text = strip_email_headers(raw_email)
78
- parsed_dict, _was_cached = await ollama_svc.parse_text(body_text)
79
- parsed = _build_parsed(parsed_dict)
80
-
81
- if trip_id:
82
- await db.execute(
83
- text("""
84
- INSERT INTO trip_items
85
- (trip_id, user_id, type, title, raw_text, parsed_data,
86
- event_at, event_end_at, booking_ref, provider)
87
- VALUES (:trip_id, :uid, :type, :title, :raw, :pd,
88
- :event_at, :event_end_at, :booking_ref, :provider)
89
- """),
90
- {
91
- "trip_id": str(trip_id), "uid": uid, "type": parsed.type, "title": parsed.title,
92
- "raw": body_text, "pd": json.dumps(parsed.model_dump()),
93
- "event_at": _safe_dt(parsed.event_at),
94
- "event_end_at": _safe_dt(parsed.event_end_at),
95
- "booking_ref": parsed.booking_ref, "provider": parsed.provider,
96
- },
97
- )
98
- else:
99
- await db.execute(
100
- text("""
101
- INSERT INTO chaos_inbox (user_id, raw_content, parsed_data, source)
102
- VALUES (:uid, :raw, :pd, 'mail')
103
- """),
104
- {"uid": uid, "raw": body_text, "pd": json.dumps(parsed.model_dump())},
105
- )
106
- await db.commit()
107
-
108
- return ParseResponse(parsed=parsed, raw_text=body_text, model_used=settings.ollama_model)
@@ -1,303 +0,0 @@
1
- """
2
- Parse endpoint — accepts file upload, raw text, or a public URL.
3
- Returns structured travel data via Ollama AI.
4
- Results are deduplicated via Cachly Redis when CACHLY_REDIS_URL is configured.
5
- """
6
- from datetime import datetime, timezone
7
- from typing import Annotated
8
- from uuid import UUID
9
- import aiofiles
10
- import os
11
- from pathlib import Path
12
- from fastapi import APIRouter, Depends, File, Form, HTTPException, Request, UploadFile, status
13
- from pydantic import BaseModel, ValidationError
14
- from sqlalchemy import text
15
- from sqlalchemy.ext.asyncio import AsyncSession
16
- from app.auth.keycloak import user_id, user_id_or_bot
17
- from app.config import get_settings
18
- from app.db.database import get_db
19
- from app.models.schemas import ParseResponse, ParsedTravelData
20
- from app.services import ollama as ollama_svc
21
- from app.services.parser import extract_text_from_pdf, is_image, is_pdf, is_text
22
- from app.services.cache import cachly_configured
23
- from app.services import quota
24
- from app.limiter import limiter
25
-
26
- router = APIRouter(prefix="/parse", tags=["parse"])
27
- settings = get_settings()
28
-
29
- # ── helpers ────────────────────────────────────────────────────────────────────
30
-
31
- def _safe_dt(val: str | None) -> datetime | None:
32
- """Parse an ISO 8601 datetime string from Ollama to a proper datetime.
33
- Returns None if the value is missing, empty, or not parseable.
34
- asyncpg requires a proper datetime object or None for TIMESTAMPTZ columns.
35
- """
36
- if not val or not isinstance(val, str):
37
- return None
38
- try:
39
- # Python 3.11+ fromisoformat handles most ISO 8601 variants.
40
- # Replace trailing Z (UTC) so older code paths also work.
41
- return datetime.fromisoformat(val.replace("Z", "+00:00"))
42
- except ValueError:
43
- return None
44
-
45
-
46
- def _build_parsed(parsed_dict: dict) -> ParsedTravelData:
47
- """Construct ParsedTravelData from Ollama output.
48
- Falls back to safe defaults if Ollama returns an unexpected 'type' value
49
- (e.g. 'taxi', 'parking') that is not in the Literal enum — which would
50
- otherwise raise an unhandled Pydantic ValidationError and return a 500.
51
- """
52
- try:
53
- return ParsedTravelData(**{k: parsed_dict.get(k) for k in ParsedTravelData.model_fields})
54
- except ValidationError:
55
- return ParsedTravelData(
56
- type="other",
57
- title=str(parsed_dict.get("title") or "Dokument"),
58
- booking_ref=parsed_dict.get("booking_ref"),
59
- provider=parsed_dict.get("provider"),
60
- event_at=parsed_dict.get("event_at"),
61
- raw_summary=parsed_dict.get("raw_summary"),
62
- )
63
-
64
-
65
- async def _save_file(content: bytes, filename: str, uid: str) -> str:
66
- dest_dir = Path(settings.upload_dir) / uid
67
- dest_dir.mkdir(parents=True, exist_ok=True)
68
- dest = dest_dir / filename
69
- async with aiofiles.open(dest, "wb") as f:
70
- await f.write(content)
71
- return str(dest)
72
-
73
-
74
- async def _insert_item_from_parsed(
75
- uid: str, trip_id: UUID, parsed: ParsedTravelData,
76
- raw_text: str | None, file_path: str | None,
77
- mime: str | None, filename: str | None,
78
- db: AsyncSession,
79
- ) -> None:
80
- import json
81
- result = await db.execute(
82
- text("""
83
- INSERT INTO trip_items
84
- (trip_id, user_id, type, title, raw_text, parsed_data,
85
- event_at, event_end_at, booking_ref, provider)
86
- VALUES (:trip_id, :uid, :type, :title, :raw_text, :pd,
87
- :event_at, :event_end_at, :booking_ref, :provider)
88
- RETURNING id
89
- """),
90
- {
91
- "trip_id": str(trip_id), "uid": uid, "type": parsed.type, "title": parsed.title,
92
- "raw_text": raw_text, "pd": json.dumps(parsed.model_dump()),
93
- # _safe_dt converts Ollama's freeform date strings to proper datetime objects.
94
- # asyncpg / PostgreSQL rejects non-ISO strings for TIMESTAMPTZ columns,
95
- # which would crash the INSERT and roll back the whole transaction.
96
- "event_at": _safe_dt(parsed.event_at), "event_end_at": _safe_dt(parsed.event_end_at),
97
- "booking_ref": parsed.booking_ref, "provider": parsed.provider,
98
- },
99
- )
100
- item_id = result.fetchone()[0]
101
-
102
- if file_path:
103
- await db.execute(
104
- text("""
105
- INSERT INTO attachments (trip_item_id, user_id, file_path, file_name, mime_type)
106
- VALUES (:item_id, :uid, :fp, :fn, :mime)
107
- """),
108
- # file_name is NOT NULL in the schema. Use a fallback to avoid a constraint
109
- # violation that would roll back the trip_item insert in the same transaction.
110
- {"item_id": item_id, "uid": uid, "fp": file_path, "fn": filename or "attachment", "mime": mime or "application/octet-stream"},
111
- )
112
- await db.commit()
113
-
114
-
115
- async def _insert_inbox(
116
- uid: str, raw_text: str | None, parsed_dict: dict,
117
- file_path: str | None, mime: str | None, filename: str | None,
118
- db: AsyncSession,
119
- ) -> dict:
120
- import json
121
- result = await db.execute(
122
- text("""
123
- INSERT INTO chaos_inbox (user_id, raw_content, parsed_data, source)
124
- VALUES (:uid, :raw, :pd, :source)
125
- RETURNING id
126
- """),
127
- {"uid": uid, "raw": raw_text, "pd": json.dumps(parsed_dict), "source": filename or "text"},
128
- )
129
- inbox_id = result.fetchone()[0]
130
-
131
- if file_path:
132
- await db.execute(
133
- text("""
134
- INSERT INTO attachments (inbox_id, user_id, file_path, file_name, mime_type)
135
- VALUES (:inbox_id, :uid, :fp, :fn, :mime)
136
- """),
137
- {"inbox_id": inbox_id, "uid": uid, "fp": file_path, "fn": filename or "attachment", "mime": mime or "application/octet-stream"},
138
- )
139
- await db.commit()
140
- return {"inbox_id": str(inbox_id)}
141
-
142
- # ── endpoints ──────────────────────────────────────────────────────────────────
143
-
144
- @limiter.limit("10/minute")
145
- @router.post("/file", response_model=ParseResponse)
146
- async def parse_file(
147
- request: Request,
148
- uid: Annotated[str, Depends(user_id_or_bot)],
149
- db: Annotated[AsyncSession, Depends(get_db)],
150
- file: UploadFile = File(...),
151
- trip_id: UUID | None = Form(None),
152
- ):
153
- await quota.check_parse(db, uid)
154
- _s = get_settings()
155
- content = await file.read()
156
- if len(content) > _s.max_upload_bytes:
157
- raise HTTPException(status_code=413, detail=f"File too large. Max {_s.max_upload_bytes // 1024 // 1024} MB.")
158
- mime = file.content_type or "application/octet-stream"
159
- raw_text: str | None = None
160
- parsed_dict: dict = {}
161
-
162
- was_cached = False
163
- if is_pdf(mime):
164
- raw_text = extract_text_from_pdf(content)
165
- parsed_dict, was_cached = await ollama_svc.parse_text(raw_text)
166
- elif is_image(mime):
167
- parsed_dict, raw_text, was_cached = await ollama_svc.parse_image(content, mime)
168
- elif is_text(mime):
169
- raw_text = content.decode("utf-8", errors="replace")
170
- parsed_dict, was_cached = await ollama_svc.parse_text(raw_text)
171
- else:
172
- raise HTTPException(status_code=415, detail=f"Unsupported file type: {mime}")
173
-
174
- file_path = await _save_file(content, file.filename or "upload", uid)
175
- parsed = _build_parsed(parsed_dict)
176
-
177
- if trip_id:
178
- await _insert_item_from_parsed(uid, trip_id, parsed, raw_text, file_path, mime, file.filename, db)
179
- else:
180
- await _insert_inbox(uid, raw_text, parsed_dict, file_path, mime, file.filename, db)
181
-
182
- from app.services import telemetry, notifier as notifier_svc
183
- await telemetry.track(db, uid, "parse_file", {"mime": mime, "trip_id": str(trip_id) if trip_id else None, "cached": was_cached})
184
- if not was_cached:
185
- dest = f"Trip zugewiesen" if trip_id else "Chaos Inbox"
186
- await notifier_svc.notify_user(db, uid, f"✅ *{parsed.title or 'Dokument'}* geparst\nTyp: {parsed.type} · {dest}")
187
- return ParseResponse(parsed=parsed, raw_text=raw_text, model_used=settings.ollama_model)
188
-
189
-
190
- @limiter.limit("20/minute")
191
- @router.post("/text", response_model=ParseResponse)
192
- async def parse_text_endpoint(
193
- request: Request,
194
- uid: Annotated[str, Depends(user_id_or_bot)],
195
- db: Annotated[AsyncSession, Depends(get_db)],
196
- raw_text: str = Form(...),
197
- trip_id: UUID | None = Form(None),
198
- ):
199
- await quota.check_parse(db, uid)
200
- parsed_dict, was_cached = await ollama_svc.parse_text(raw_text)
201
- parsed = _build_parsed(parsed_dict)
202
-
203
- if trip_id:
204
- await _insert_item_from_parsed(uid, trip_id, parsed, raw_text, None, None, None, db)
205
- else:
206
- await _insert_inbox(uid, raw_text, parsed_dict, None, None, None, db)
207
-
208
- from app.services import telemetry
209
- await telemetry.track(db, uid, "parse_text", {"mime": None, "trip_id": str(trip_id) if trip_id else None, "cached": was_cached})
210
- return ParseResponse(parsed=parsed, raw_text=raw_text, model_used=settings.ollama_model)
211
-
212
-
213
- class ParseUrlRequest(BaseModel):
214
- url: str
215
- trip_id: UUID | None = None
216
-
217
-
218
- @limiter.limit("5/minute")
219
- @router.post("/url")
220
- async def parse_url(
221
- request: Request,
222
- body: ParseUrlRequest,
223
- uid: Annotated[str, Depends(user_id)],
224
- db: Annotated[AsyncSession, Depends(get_db)],
225
- ):
226
- """
227
- Fetch a public URL and parse its content with Ollama.
228
- Useful for online booking confirmation pages.
229
- Cached in Cachly Redis when CACHLY_REDIS_URL is configured.
230
- """
231
- import httpx as _httpx
232
- from html.parser import HTMLParser
233
-
234
- class _TextExtractor(HTMLParser):
235
- def __init__(self):
236
- super().__init__()
237
- self._parts: list[str] = []
238
- self._skip = False
239
- def handle_starttag(self, tag, attrs):
240
- if tag in ("script", "style", "nav", "footer"):
241
- self._skip = True
242
- def handle_endtag(self, tag):
243
- if tag in ("script", "style", "nav", "footer"):
244
- self._skip = False
245
- def handle_data(self, data):
246
- if not self._skip and data.strip():
247
- self._parts.append(data.strip())
248
- def text(self) -> str:
249
- return " ".join(self._parts)[:8000]
250
-
251
- # SSRF protection: only allow http/https, block private/loopback IP ranges
252
- import ipaddress as _ip
253
- try:
254
- _parsed = _httpx.URL(body.url)
255
- if _parsed.scheme not in ("http", "https"):
256
- raise HTTPException(status_code=422, detail="Only http/https URLs are allowed")
257
- import socket as _socket
258
- try:
259
- _addr = _socket.gethostbyname(_parsed.host)
260
- _net = _ip.ip_address(_addr)
261
- if _net.is_private or _net.is_loopback or _net.is_link_local or _net.is_reserved:
262
- raise HTTPException(status_code=422, detail="URL resolves to a private/internal address")
263
- except HTTPException:
264
- raise
265
- except Exception:
266
- pass # DNS failure handled below
267
- except HTTPException:
268
- raise
269
- except Exception:
270
- raise HTTPException(status_code=422, detail="Invalid URL")
271
-
272
- try:
273
- async with _httpx.AsyncClient(timeout=20, follow_redirects=True) as client:
274
- resp = await client.get(body.url, headers={"User-Agent": "Mozilla/5.0 TCO/0.1"})
275
- resp.raise_for_status()
276
- except HTTPException:
277
- raise
278
- except Exception:
279
- raise HTTPException(status_code=422, detail="Could not fetch URL")
280
-
281
- await quota.check_parse(db, uid)
282
- ct = resp.headers.get("content-type", "")
283
- if "html" in ct:
284
- extractor = _TextExtractor()
285
- extractor.feed(resp.text)
286
- raw_text = extractor.text()
287
- elif "pdf" in ct:
288
- raw_text = extract_text_from_pdf(resp.content)
289
- else:
290
- raw_text = resp.text[:8000]
291
-
292
- parsed_dict, was_cached = await ollama_svc.parse_text(raw_text)
293
- parsed = _build_parsed(parsed_dict)
294
-
295
- from app.services import telemetry
296
- if body.trip_id:
297
- await _insert_item_from_parsed(uid, body.trip_id, parsed, raw_text, None, None, body.url, db)
298
- await telemetry.track(db, uid, "parse_url", {"mime": None, "trip_id": str(body.trip_id) if body.trip_id else None, "cached": was_cached})
299
- return ParseResponse(parsed=parsed, raw_text=raw_text, model_used=settings.ollama_model)
300
- else:
301
- result = await _insert_inbox(uid, raw_text, parsed_dict, None, None, body.url, db)
302
- await telemetry.track(db, uid, "parse_url", {"mime": None, "trip_id": None, "cached": was_cached})
303
- return {**ParseResponse(parsed=parsed, raw_text=raw_text, model_used=settings.ollama_model).model_dump(), **result}