mirror of
https://github.com/x1xhlol/system-prompts-and-models-of-ai-tools.git
synced 2026-06-17 23:09:35 +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
|
||||
- SQLAlchemy 2.0 async with PostgreSQL 16
|
||||
- Celery 5.x with Redis broker
|
||||
- JWT authentication (python-jose)
|
||||
- JWT authentication (PyJWT)
|
||||
- Multi-tenant data isolation via `tenant_id`
|
||||
|
||||
### Frontend (`salesflow-saas/frontend/`)
|
||||
|
||||
@ -26,6 +26,12 @@ docker-compose up --build
|
||||
Backend: `http://localhost:8000/docs`
|
||||
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: **مسار التشغيل مع العميل**.
|
||||
|
||||
**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,17 +4,23 @@ import json
|
||||
|
||||
try:
|
||||
from mem0 import Memory
|
||||
from mem0.configs.base import MemoryConfig
|
||||
except ImportError:
|
||||
# Fallback mock for testing environments where mem0ai isn't available yet
|
||||
class Memory:
|
||||
def __init__(self, config=None):
|
||||
self.store = []
|
||||
|
||||
def search(self, query: str, user_id: str, **kwargs):
|
||||
return [{"text": "Mocked memory context."}]
|
||||
|
||||
def add(self, text: str, user_id: str, metadata: dict = None, **kwargs):
|
||||
self.store.append({"text": text, "user_id": user_id, "metadata": metadata})
|
||||
Memory = None # type: ignore[misc, assignment]
|
||||
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):
|
||||
self.store = []
|
||||
|
||||
def search(self, query: str, user_id: str, **kwargs):
|
||||
return [{"text": "Mocked memory context."}]
|
||||
|
||||
def add(self, text: str, user_id: str, metadata: dict = None, **kwargs):
|
||||
self.store.append({"text": text, "user_id": user_id, "metadata": metadata})
|
||||
|
||||
class SelfHealingMemory:
|
||||
"""
|
||||
@ -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:
|
||||
"""
|
||||
|
||||
@ -101,7 +101,7 @@ class SequenceEvent(BaseModel):
|
||||
channel = Column(String(50), nullable=False)
|
||||
status = Column(String(50), nullable=False, default=SequenceEventStatus.SENT.value)
|
||||
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")
|
||||
step = relationship("SequenceStep", back_populates="events")
|
||||
|
||||
@ -8,7 +8,8 @@ from datetime import datetime, timedelta, timezone
|
||||
from typing import Optional
|
||||
from uuid import UUID
|
||||
|
||||
from jose import JWTError, jwt
|
||||
import jwt
|
||||
from jwt.exceptions import PyJWTError
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
@ -75,7 +76,7 @@ class AuthService:
|
||||
token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM]
|
||||
)
|
||||
return payload
|
||||
except JWTError:
|
||||
except PyJWTError:
|
||||
return None
|
||||
|
||||
# ── OTP ───────────────────────────────────────
|
||||
|
||||
@ -216,7 +216,7 @@ class SequenceEngine:
|
||||
self.db.add(SequenceEvent(
|
||||
enrollment_id=enrollment.id, step_id=step.id, channel=step.channel,
|
||||
status=SequenceEventStatus.FAILED.value,
|
||||
metadata={"reason": "no_consent", "detail": cr.message},
|
||||
event_metadata={"reason": "no_consent", "detail": cr.message},
|
||||
))
|
||||
enrollment.status = SequenceStatus.STOPPED.value
|
||||
await self.db.flush()
|
||||
@ -226,7 +226,7 @@ class SequenceEngine:
|
||||
self.db.add(SequenceEvent(
|
||||
enrollment_id=enrollment.id, step_id=step.id, channel=step.channel,
|
||||
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
|
||||
if enrollment.current_step >= len(steps):
|
||||
|
||||
@ -2,7 +2,9 @@ from datetime import datetime, timedelta, timezone
|
||||
from typing import Optional
|
||||
|
||||
import bcrypt
|
||||
from jose import JWTError, jwt
|
||||
import jwt
|
||||
|
||||
from jwt.exceptions import PyJWTError
|
||||
|
||||
from app.config import get_settings
|
||||
|
||||
@ -46,5 +48,5 @@ def decode_token(token: str) -> Optional[dict]:
|
||||
try:
|
||||
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
|
||||
return payload
|
||||
except JWTError:
|
||||
except PyJWTError:
|
||||
return None
|
||||
|
||||
@ -92,7 +92,7 @@ def process_pending_sequences(self):
|
||||
channel=step.channel,
|
||||
status="failed",
|
||||
sent_at=datetime.now(timezone.utc),
|
||||
metadata={"reason": "pdpl_consent_missing"},
|
||||
event_metadata={"reason": "pdpl_consent_missing"},
|
||||
)
|
||||
db.add(event)
|
||||
enrollment.current_step += 1
|
||||
@ -181,7 +181,7 @@ def execute_sequence_step(self, enrollment_id, step_id, lead_id, channel, conten
|
||||
channel=channel,
|
||||
status="sent" if success else "failed",
|
||||
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)
|
||||
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
# === Core Framework ===
|
||||
fastapi==0.115.5
|
||||
uvicorn[standard]==0.32.1
|
||||
pydantic==2.9.2
|
||||
pydantic-settings==2.6.1
|
||||
pydantic>=2.10.0,<3
|
||||
pydantic-settings>=2.10.1,<3
|
||||
pydantic-extra-types[phonenumbers]>=2.0.0 # Saudi phone validation (+966)
|
||||
python-multipart==0.0.12
|
||||
|
||||
@ -14,17 +14,17 @@ alembic==1.14.0
|
||||
pgvector==0.3.6
|
||||
|
||||
# === 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
|
||||
groq==0.12.0
|
||||
openai==1.57.0
|
||||
langchain==0.3.9
|
||||
openai>=2.8.0,<3 # litellm 1.8x+ requires openai>=2.8; mem0ai 1.x supports it
|
||||
langchain==0.3.28
|
||||
langchain-groq==0.2.1
|
||||
langchain-community==0.3.9
|
||||
langchain-community==0.3.28
|
||||
langchain-anthropic==0.2.0
|
||||
langgraph==0.2.53
|
||||
crewai==0.80.0
|
||||
mem0ai==0.1.18
|
||||
crewai>=0.95.0,<1 # aligns with litellm/langchain stack; 0.80 pulled incompatible crewai-tools
|
||||
mem0ai>=1.0.0,<2 # 0.1.x capped openai<2; incompatible with current litellm
|
||||
|
||||
# === Arabic NLP ===
|
||||
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
|
||||
|
||||
# === Communication ===
|
||||
httpx==0.27.2
|
||||
httpx>=0.28.1,<0.29.0
|
||||
resend>=2.0.0 # Transactional email API (free tier, FastAPI-native)
|
||||
|
||||
# === Saudi-specific ===
|
||||
@ -73,10 +73,10 @@ statsforecast>=1.7.0 # Fast statistical time-series forecasting
|
||||
# === Data & Utilities ===
|
||||
beautifulsoup4==4.12.3
|
||||
lxml==5.3.0
|
||||
requests==2.32.3
|
||||
requests>=2.32.5,<3
|
||||
python-dateutil==2.9.0
|
||||
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
|
||||
paramiko==3.5.0
|
||||
qrcode==8.0
|
||||
|
||||
@ -13,7 +13,7 @@ test.describe("Subscriber journey (public shell)", () => {
|
||||
test("home shows Dealix value and navigation affordances", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
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 }) => {
|
||||
|
||||
@ -24,6 +24,10 @@ import {
|
||||
Receipt,
|
||||
Layers,
|
||||
LogOut,
|
||||
MousePointerClick,
|
||||
UserCheck,
|
||||
TrendingUp,
|
||||
Crosshair,
|
||||
} from "lucide-react";
|
||||
|
||||
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 { 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() {
|
||||
const auth = useRequireAuth();
|
||||
const [activeTab, setActiveTab] = useState("overview");
|
||||
@ -128,7 +143,7 @@ export default function DashboardPage() {
|
||||
case "inbox":
|
||||
return <UnifiedInbox />;
|
||||
case "scoring":
|
||||
return <LeadScoreCard score={82} breakdown={{ engagement: 24, profile: 20, behavior: 22, intent: 16 }} recommendation="عميل واعد — تابع خلال ٢٤ ساعة" />;
|
||||
return <LeadScoreCard data={dashboardLeadScoreDemo} />;
|
||||
case "onboarding":
|
||||
return <OnboardingView />;
|
||||
default:
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Noto_Kufi_Arabic } from "next/font/google";
|
||||
import "./globals.css";
|
||||
import { I18nProvider } from "@/i18n";
|
||||
|
||||
const kufi = Noto_Kufi_Arabic({
|
||||
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 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>
|
||||
</html>
|
||||
);
|
||||
|
||||
@ -40,15 +40,18 @@ const stagger = {
|
||||
function Section({
|
||||
children,
|
||||
className = "",
|
||||
id,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
id?: string;
|
||||
}) {
|
||||
const ref = useRef(null);
|
||||
const inView = useInView(ref, { once: true, margin: "-60px" });
|
||||
return (
|
||||
<motion.section
|
||||
ref={ref}
|
||||
id={id}
|
||||
initial="hidden"
|
||||
animate={inView ? "visible" : "hidden"}
|
||||
variants={stagger}
|
||||
|
||||
@ -19,7 +19,7 @@ interface KpiCardProps {
|
||||
|
||||
function useCountUp(target: number, duration: number = 1200) {
|
||||
const [current, setCurrent] = useState(0);
|
||||
const frameRef = useRef<number>();
|
||||
const frameRef = useRef<number | undefined>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
const start = performance.now();
|
||||
|
||||
@ -108,6 +108,7 @@
|
||||
"guarantee": "Full refund within 30 days if not satisfied",
|
||||
"plan": {
|
||||
"name": "Dealix All-in-One",
|
||||
"nameAr": "ديلكس الشاملة",
|
||||
"priceMonthly": "1,499",
|
||||
"priceYearly": "14,999",
|
||||
"priceYearlySave": "Save 2 months",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user