mirror of
https://github.com/x1xhlol/system-prompts-and-models-of-ai-tools.git
synced 2026-06-18 23:39:34 +00:00
fix(dealix): resolve Python deps, SQLAlchemy metadata, JWT, and frontend CI
- Align httpx, litellm, langchain, openai, mem0ai, crewai, numpy, requests, pydantic - Rename SequenceEvent ORM attribute to event_metadata (DB column stays metadata) - Use PyJWT instead of python-jose in security and auth service - Mem0: MemoryConfig + graceful fallback when init fails (CI without keys) - Frontend: I18nProvider in root layout, fix dashboard LeadScoreCard props, Section id, kpi-card useRef, en.json nameAr parity, e2e assertion for premium landing - README: troubleshooting for connection refused and local E2E Playwright install Co-authored-by: VoXc2 <VoXc2@users.noreply.github.com>
This commit is contained in:
parent
1b2baf6bc8
commit
8c3d91c070
@ -13,7 +13,7 @@
|
|||||||
- FastAPI 0.115.6 on Python 3.12
|
- FastAPI 0.115.6 on Python 3.12
|
||||||
- SQLAlchemy 2.0 async with PostgreSQL 16
|
- SQLAlchemy 2.0 async with PostgreSQL 16
|
||||||
- Celery 5.x with Redis broker
|
- Celery 5.x with Redis broker
|
||||||
- JWT authentication (python-jose)
|
- JWT authentication (PyJWT)
|
||||||
- Multi-tenant data isolation via `tenant_id`
|
- Multi-tenant data isolation via `tenant_id`
|
||||||
|
|
||||||
### Frontend (`salesflow-saas/frontend/`)
|
### Frontend (`salesflow-saas/frontend/`)
|
||||||
|
|||||||
@ -26,6 +26,12 @@ docker-compose up --build
|
|||||||
Backend: `http://localhost:8000/docs`
|
Backend: `http://localhost:8000/docs`
|
||||||
Frontend: `http://localhost:3000`
|
Frontend: `http://localhost:3000`
|
||||||
|
|
||||||
|
**If the browser shows connection refused on `:3000` or `:8000`:** nothing is listening on that port yet. Start the stack (`docker compose up` from this folder) or run `uvicorn` / `npm run dev` manually. Confirm with `curl -sSf http://127.0.0.1:8000/api/v1/health` and ensure the browser is on the same machine as the server (not WSL/remote without port forwarding).
|
||||||
|
|
||||||
|
**Without Docker:** install Python 3.12+ and Node 22+, copy `.env` and `frontend/.env.local`, run Postgres/Redis (or point `DATABASE_URL` / `REDIS_URL` at existing instances), then `cd backend && uvicorn app.main:app --reload --host 0.0.0.0 --port 8000` and `cd frontend && npm run dev`.
|
||||||
|
|
||||||
|
**E2E locally:** after `npm ci`, run `npx playwright install chromium` once, then `npm run test:e2e` (matches CI).
|
||||||
|
|
||||||
**Customer onboarding (B2B):** `GET /api/v1/customer-onboarding/journey` and `docs/CUSTOMER_OS_ONBOARDING_AR.md`. Dashboard tab: **مسار التشغيل مع العميل**.
|
**Customer onboarding (B2B):** `GET /api/v1/customer-onboarding/journey` and `docs/CUSTOMER_OS_ONBOARDING_AR.md`. Dashboard tab: **مسار التشغيل مع العميل**.
|
||||||
|
|
||||||
**Launch verification:** see `docs/LAUNCH_CHECKLIST.md`. From `salesflow-saas`: copy `frontend/.env.example` to `frontend/.env.local` and set `NEXT_PUBLIC_API_URL`. Run `.\verify-launch.ps1 -HttpCheck -SoftReady` (use `-BaseUrl` if the API is not on port 8000).
|
**Launch verification:** see `docs/LAUNCH_CHECKLIST.md`. From `salesflow-saas`: copy `frontend/.env.example` to `frontend/.env.local` and set `NEXT_PUBLIC_API_URL`. Run `.\verify-launch.ps1 -HttpCheck -SoftReady` (use `-BaseUrl` if the API is not on port 8000).
|
||||||
|
|||||||
@ -4,9 +4,15 @@ import json
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
from mem0 import Memory
|
from mem0 import Memory
|
||||||
|
from mem0.configs.base import MemoryConfig
|
||||||
except ImportError:
|
except ImportError:
|
||||||
# Fallback mock for testing environments where mem0ai isn't available yet
|
Memory = None # type: ignore[misc, assignment]
|
||||||
class Memory:
|
MemoryConfig = None # type: ignore[misc, assignment]
|
||||||
|
|
||||||
|
|
||||||
|
class _MockMemory:
|
||||||
|
"""Used when mem0 is unavailable or cannot initialize (CI, missing API keys)."""
|
||||||
|
|
||||||
def __init__(self, config=None):
|
def __init__(self, config=None):
|
||||||
self.store = []
|
self.store = []
|
||||||
|
|
||||||
@ -33,7 +39,12 @@ class SelfHealingMemory:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.memory = Memory(config=self.config)
|
self.memory = _MockMemory()
|
||||||
|
if Memory is not None and MemoryConfig is not None:
|
||||||
|
try:
|
||||||
|
self.memory = Memory(config=MemoryConfig.model_validate(self.config))
|
||||||
|
except Exception:
|
||||||
|
self.memory = _MockMemory()
|
||||||
|
|
||||||
def get_context(self, company_name: str, context_type: str = "general") -> str:
|
def get_context(self, company_name: str, context_type: str = "general") -> str:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@ -101,7 +101,7 @@ class SequenceEvent(BaseModel):
|
|||||||
channel = Column(String(50), nullable=False)
|
channel = Column(String(50), nullable=False)
|
||||||
status = Column(String(50), nullable=False, default=SequenceEventStatus.SENT.value)
|
status = Column(String(50), nullable=False, default=SequenceEventStatus.SENT.value)
|
||||||
sent_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
|
sent_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
|
||||||
metadata = Column(JSONB, default=dict)
|
event_metadata = Column("metadata", JSONB, default=dict)
|
||||||
|
|
||||||
enrollment = relationship("SequenceEnrollment", back_populates="events")
|
enrollment = relationship("SequenceEnrollment", back_populates="events")
|
||||||
step = relationship("SequenceStep", back_populates="events")
|
step = relationship("SequenceStep", back_populates="events")
|
||||||
|
|||||||
@ -8,7 +8,8 @@ from datetime import datetime, timedelta, timezone
|
|||||||
from typing import Optional
|
from typing import Optional
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
from jose import JWTError, jwt
|
import jwt
|
||||||
|
from jwt.exceptions import PyJWTError
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
@ -75,7 +76,7 @@ class AuthService:
|
|||||||
token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM]
|
token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM]
|
||||||
)
|
)
|
||||||
return payload
|
return payload
|
||||||
except JWTError:
|
except PyJWTError:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# ── OTP ───────────────────────────────────────
|
# ── OTP ───────────────────────────────────────
|
||||||
|
|||||||
@ -216,7 +216,7 @@ class SequenceEngine:
|
|||||||
self.db.add(SequenceEvent(
|
self.db.add(SequenceEvent(
|
||||||
enrollment_id=enrollment.id, step_id=step.id, channel=step.channel,
|
enrollment_id=enrollment.id, step_id=step.id, channel=step.channel,
|
||||||
status=SequenceEventStatus.FAILED.value,
|
status=SequenceEventStatus.FAILED.value,
|
||||||
metadata={"reason": "no_consent", "detail": cr.message},
|
event_metadata={"reason": "no_consent", "detail": cr.message},
|
||||||
))
|
))
|
||||||
enrollment.status = SequenceStatus.STOPPED.value
|
enrollment.status = SequenceStatus.STOPPED.value
|
||||||
await self.db.flush()
|
await self.db.flush()
|
||||||
@ -226,7 +226,7 @@ class SequenceEngine:
|
|||||||
self.db.add(SequenceEvent(
|
self.db.add(SequenceEvent(
|
||||||
enrollment_id=enrollment.id, step_id=step.id, channel=step.channel,
|
enrollment_id=enrollment.id, step_id=step.id, channel=step.channel,
|
||||||
status=SequenceEventStatus.SENT.value,
|
status=SequenceEventStatus.SENT.value,
|
||||||
metadata={"variant": step.variant, "preview": step.template_content[:80]},
|
event_metadata={"variant": step.variant, "preview": step.template_content[:80]},
|
||||||
))
|
))
|
||||||
enrollment.current_step = idx + 1
|
enrollment.current_step = idx + 1
|
||||||
if enrollment.current_step >= len(steps):
|
if enrollment.current_step >= len(steps):
|
||||||
|
|||||||
@ -2,7 +2,9 @@ from datetime import datetime, timedelta, timezone
|
|||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
import bcrypt
|
import bcrypt
|
||||||
from jose import JWTError, jwt
|
import jwt
|
||||||
|
|
||||||
|
from jwt.exceptions import PyJWTError
|
||||||
|
|
||||||
from app.config import get_settings
|
from app.config import get_settings
|
||||||
|
|
||||||
@ -46,5 +48,5 @@ def decode_token(token: str) -> Optional[dict]:
|
|||||||
try:
|
try:
|
||||||
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
|
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
|
||||||
return payload
|
return payload
|
||||||
except JWTError:
|
except PyJWTError:
|
||||||
return None
|
return None
|
||||||
|
|||||||
@ -92,7 +92,7 @@ def process_pending_sequences(self):
|
|||||||
channel=step.channel,
|
channel=step.channel,
|
||||||
status="failed",
|
status="failed",
|
||||||
sent_at=datetime.now(timezone.utc),
|
sent_at=datetime.now(timezone.utc),
|
||||||
metadata={"reason": "pdpl_consent_missing"},
|
event_metadata={"reason": "pdpl_consent_missing"},
|
||||||
)
|
)
|
||||||
db.add(event)
|
db.add(event)
|
||||||
enrollment.current_step += 1
|
enrollment.current_step += 1
|
||||||
@ -181,7 +181,7 @@ def execute_sequence_step(self, enrollment_id, step_id, lead_id, channel, conten
|
|||||||
channel=channel,
|
channel=channel,
|
||||||
status="sent" if success else "failed",
|
status="sent" if success else "failed",
|
||||||
sent_at=datetime.now(timezone.utc),
|
sent_at=datetime.now(timezone.utc),
|
||||||
metadata={"content_preview": content[:100] if content else ""},
|
event_metadata={"content_preview": content[:100] if content else ""},
|
||||||
)
|
)
|
||||||
db.add(event)
|
db.add(event)
|
||||||
|
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
# === Core Framework ===
|
# === Core Framework ===
|
||||||
fastapi==0.115.5
|
fastapi==0.115.5
|
||||||
uvicorn[standard]==0.32.1
|
uvicorn[standard]==0.32.1
|
||||||
pydantic==2.9.2
|
pydantic>=2.10.0,<3
|
||||||
pydantic-settings==2.6.1
|
pydantic-settings>=2.10.1,<3
|
||||||
pydantic-extra-types[phonenumbers]>=2.0.0 # Saudi phone validation (+966)
|
pydantic-extra-types[phonenumbers]>=2.0.0 # Saudi phone validation (+966)
|
||||||
python-multipart==0.0.12
|
python-multipart==0.0.12
|
||||||
|
|
||||||
@ -14,17 +14,17 @@ alembic==1.14.0
|
|||||||
pgvector==0.3.6
|
pgvector==0.3.6
|
||||||
|
|
||||||
# === AI / LLM Providers ===
|
# === AI / LLM Providers ===
|
||||||
litellm>=1.40.0 # Unified LLM provider (Groq/OpenAI/Claude/Gemini) with fallback
|
litellm>=1.74.0,<2 # httpx>=0.28 compatible (older litellm capped httpx<0.28)
|
||||||
instructor>=1.14.0 # Structured LLM outputs via Pydantic models
|
instructor>=1.14.0 # Structured LLM outputs via Pydantic models
|
||||||
groq==0.12.0
|
groq==0.12.0
|
||||||
openai==1.57.0
|
openai>=2.8.0,<3 # litellm 1.8x+ requires openai>=2.8; mem0ai 1.x supports it
|
||||||
langchain==0.3.9
|
langchain==0.3.28
|
||||||
langchain-groq==0.2.1
|
langchain-groq==0.2.1
|
||||||
langchain-community==0.3.9
|
langchain-community==0.3.28
|
||||||
langchain-anthropic==0.2.0
|
langchain-anthropic==0.2.0
|
||||||
langgraph==0.2.53
|
langgraph==0.2.53
|
||||||
crewai==0.80.0
|
crewai>=0.95.0,<1 # aligns with litellm/langchain stack; 0.80 pulled incompatible crewai-tools
|
||||||
mem0ai==0.1.18
|
mem0ai>=1.0.0,<2 # 0.1.x capped openai<2; incompatible with current litellm
|
||||||
|
|
||||||
# === Arabic NLP ===
|
# === Arabic NLP ===
|
||||||
camel-tools>=1.5.0 # Arabic morphology, NER, dialect detection (NYU Abu Dhabi)
|
camel-tools>=1.5.0 # Arabic morphology, NER, dialect detection (NYU Abu Dhabi)
|
||||||
@ -35,7 +35,7 @@ pywa>=3.0.0 # Direct WhatsApp Cloud API (async, webhooks, templ
|
|||||||
twilio==9.3.7 # Twilio fallback
|
twilio==9.3.7 # Twilio fallback
|
||||||
|
|
||||||
# === Communication ===
|
# === Communication ===
|
||||||
httpx==0.27.2
|
httpx>=0.28.1,<0.29.0
|
||||||
resend>=2.0.0 # Transactional email API (free tier, FastAPI-native)
|
resend>=2.0.0 # Transactional email API (free tier, FastAPI-native)
|
||||||
|
|
||||||
# === Saudi-specific ===
|
# === Saudi-specific ===
|
||||||
@ -73,10 +73,10 @@ statsforecast>=1.7.0 # Fast statistical time-series forecasting
|
|||||||
# === Data & Utilities ===
|
# === Data & Utilities ===
|
||||||
beautifulsoup4==4.12.3
|
beautifulsoup4==4.12.3
|
||||||
lxml==5.3.0
|
lxml==5.3.0
|
||||||
requests==2.32.3
|
requests>=2.32.5,<3
|
||||||
python-dateutil==2.9.0
|
python-dateutil==2.9.0
|
||||||
pandas==2.2.3
|
pandas==2.2.3
|
||||||
numpy==2.1.3
|
numpy>=1.26.2,<2 # camel-tools requires numpy<2; langchain needs >=1.26.2 on Py3.12
|
||||||
python-decouple==3.8
|
python-decouple==3.8
|
||||||
paramiko==3.5.0
|
paramiko==3.5.0
|
||||||
qrcode==8.0
|
qrcode==8.0
|
||||||
|
|||||||
@ -13,7 +13,7 @@ test.describe("Subscriber journey (public shell)", () => {
|
|||||||
test("home shows Dealix value and navigation affordances", async ({ page }) => {
|
test("home shows Dealix value and navigation affordances", async ({ page }) => {
|
||||||
await page.goto("/");
|
await page.goto("/");
|
||||||
await expect(page.getByText("Dealix", { exact: false }).first()).toBeVisible();
|
await expect(page.getByText("Dealix", { exact: false }).first()).toBeVisible();
|
||||||
await expect(page.getByText(/لماذا Dealix/)).toBeVisible();
|
await expect(page.getByText(/هل تواجه هذه التحديات/)).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("landing page loads CTA toward app", async ({ page }) => {
|
test("landing page loads CTA toward app", async ({ page }) => {
|
||||||
|
|||||||
@ -24,6 +24,10 @@ import {
|
|||||||
Receipt,
|
Receipt,
|
||||||
Layers,
|
Layers,
|
||||||
LogOut,
|
LogOut,
|
||||||
|
MousePointerClick,
|
||||||
|
UserCheck,
|
||||||
|
TrendingUp,
|
||||||
|
Crosshair,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
|
||||||
import { DashboardView } from "../../components/dealix/dashboard-view";
|
import { DashboardView } from "../../components/dealix/dashboard-view";
|
||||||
@ -48,6 +52,17 @@ import { PipelineKanban } from "../../components/dealix/pipeline-kanban";
|
|||||||
import { UnifiedInbox } from "../../components/dealix/unified-inbox";
|
import { UnifiedInbox } from "../../components/dealix/unified-inbox";
|
||||||
import { LeadScoreCard } from "../../components/dealix/lead-score-card";
|
import { LeadScoreCard } from "../../components/dealix/lead-score-card";
|
||||||
|
|
||||||
|
const dashboardLeadScoreDemo = {
|
||||||
|
score: 82,
|
||||||
|
breakdown: [
|
||||||
|
{ key: "engagement", label: "التفاعل", value: 24, icon: MousePointerClick },
|
||||||
|
{ key: "profile", label: "الملف الشخصي", value: 20, icon: UserCheck },
|
||||||
|
{ key: "behavior", label: "السلوك", value: 22, icon: TrendingUp },
|
||||||
|
{ key: "intent", label: "نية الشراء", value: 16, icon: Crosshair },
|
||||||
|
],
|
||||||
|
recommendation: "عميل واعد — تابع خلال ٢٤ ساعة",
|
||||||
|
};
|
||||||
|
|
||||||
export default function DashboardPage() {
|
export default function DashboardPage() {
|
||||||
const auth = useRequireAuth();
|
const auth = useRequireAuth();
|
||||||
const [activeTab, setActiveTab] = useState("overview");
|
const [activeTab, setActiveTab] = useState("overview");
|
||||||
@ -128,7 +143,7 @@ export default function DashboardPage() {
|
|||||||
case "inbox":
|
case "inbox":
|
||||||
return <UnifiedInbox />;
|
return <UnifiedInbox />;
|
||||||
case "scoring":
|
case "scoring":
|
||||||
return <LeadScoreCard score={82} breakdown={{ engagement: 24, profile: 20, behavior: 22, intent: 16 }} recommendation="عميل واعد — تابع خلال ٢٤ ساعة" />;
|
return <LeadScoreCard data={dashboardLeadScoreDemo} />;
|
||||||
case "onboarding":
|
case "onboarding":
|
||||||
return <OnboardingView />;
|
return <OnboardingView />;
|
||||||
default:
|
default:
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import { Noto_Kufi_Arabic } from "next/font/google";
|
import { Noto_Kufi_Arabic } from "next/font/google";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
|
import { I18nProvider } from "@/i18n";
|
||||||
|
|
||||||
const kufi = Noto_Kufi_Arabic({
|
const kufi = Noto_Kufi_Arabic({
|
||||||
subsets: ["arabic", "latin"],
|
subsets: ["arabic", "latin"],
|
||||||
@ -26,7 +27,7 @@ export default function RootLayout({
|
|||||||
<div className="fixed inset-0 z-[-1] bg-[radial-gradient(ellipse_at_top_right,_var(--tw-gradient-stops))] from-primary/10 via-background to-background pointer-events-none" />
|
<div className="fixed inset-0 z-[-1] bg-[radial-gradient(ellipse_at_top_right,_var(--tw-gradient-stops))] from-primary/10 via-background to-background pointer-events-none" />
|
||||||
<div className="fixed top-20 left-10 w-96 h-96 bg-accent/10 rounded-full mix-blend-multiply filter blur-[100px] opacity-50 z-[-1]" />
|
<div className="fixed top-20 left-10 w-96 h-96 bg-accent/10 rounded-full mix-blend-multiply filter blur-[100px] opacity-50 z-[-1]" />
|
||||||
|
|
||||||
{children}
|
<I18nProvider>{children}</I18nProvider>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -40,15 +40,18 @@ const stagger = {
|
|||||||
function Section({
|
function Section({
|
||||||
children,
|
children,
|
||||||
className = "",
|
className = "",
|
||||||
|
id,
|
||||||
}: {
|
}: {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
id?: string;
|
||||||
}) {
|
}) {
|
||||||
const ref = useRef(null);
|
const ref = useRef(null);
|
||||||
const inView = useInView(ref, { once: true, margin: "-60px" });
|
const inView = useInView(ref, { once: true, margin: "-60px" });
|
||||||
return (
|
return (
|
||||||
<motion.section
|
<motion.section
|
||||||
ref={ref}
|
ref={ref}
|
||||||
|
id={id}
|
||||||
initial="hidden"
|
initial="hidden"
|
||||||
animate={inView ? "visible" : "hidden"}
|
animate={inView ? "visible" : "hidden"}
|
||||||
variants={stagger}
|
variants={stagger}
|
||||||
|
|||||||
@ -19,7 +19,7 @@ interface KpiCardProps {
|
|||||||
|
|
||||||
function useCountUp(target: number, duration: number = 1200) {
|
function useCountUp(target: number, duration: number = 1200) {
|
||||||
const [current, setCurrent] = useState(0);
|
const [current, setCurrent] = useState(0);
|
||||||
const frameRef = useRef<number>();
|
const frameRef = useRef<number | undefined>(undefined);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const start = performance.now();
|
const start = performance.now();
|
||||||
|
|||||||
@ -108,6 +108,7 @@
|
|||||||
"guarantee": "Full refund within 30 days if not satisfied",
|
"guarantee": "Full refund within 30 days if not satisfied",
|
||||||
"plan": {
|
"plan": {
|
||||||
"name": "Dealix All-in-One",
|
"name": "Dealix All-in-One",
|
||||||
|
"nameAr": "ديلكس الشاملة",
|
||||||
"priceMonthly": "1,499",
|
"priceMonthly": "1,499",
|
||||||
"priceYearly": "14,999",
|
"priceYearly": "14,999",
|
||||||
"priceYearlySave": "Save 2 months",
|
"priceYearlySave": "Save 2 months",
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user